From 56e1a24be4e75729fc0d6ec720e5f675c6fbf800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 31 Aug 2026 23:21:03 +0200 Subject: [PATCH] Distinct readonly and editable states --- 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 +++++---- .../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 ++- .../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 +- .../tests/scenario_production_set_cost.rst | 4 +- modules/project/work.py | 2 +- modules/purchase/purchase.py | 37 +++++---- modules/purchase_amendment/purchase.py | 8 +- .../purchase_blanket_agreement/purchase.py | 21 +++-- modules/purchase_price_list/purchase.py | 5 +- .../purchase_request_quotation/purchase.py | 2 +- modules/purchase_requisition/purchase.py | 7 +- modules/purchase_secondary_unit/purchase.py | 6 +- modules/sale/sale.py | 45 ++++++----- .../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 +- ...rio_stock_recompute_average_cost_price.rst | 18 +++-- modules/stock_forecast/forecast.py | 6 +- modules/stock_product_location_place/stock.py | 14 +++- ...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(-) diff --git a/demo/production.py b/demo/production.py index 5595be7848..de7a01e0d1 100644 --- a/demo/production.py +++ b/demo/production.py @@ -111,13 +111,11 @@ def setup(config, activated, company): 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 --git a/modules/account/account.py b/modules/account/account.py index 623e8453a5..15e013758f 100644 --- a/modules/account/account.py +++ b/modules/account/account.py @@ -629,8 +629,8 @@ def AccountMixin(template=False): 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 @@ class Account( 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 @@ class Account( ('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 @@ class Account( 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 @@ class Account( 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 @@ class Account( '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 @@ class Account( 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 --git a/modules/account/move.py b/modules/account/move.py index 047092d989..3b47888735 100644 --- a/modules/account/move.py +++ b/modules/account/move.py @@ -942,8 +942,9 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView): 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 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView): default = {} else: default = default.copy() + default.setdefault('state', 'draft') default.setdefault('move', None) default.setdefault('reconciliation', None) default.setdefault('reconciliations_delegated', []) diff --git a/modules/account_asset/asset.py b/modules/account_asset/asset.py index 6ed0e52813..c5877ebab0 100644 --- a/modules/account_asset/asset.py +++ b/modules/account_asset/asset.py @@ -15,7 +15,7 @@ from trytond.modules.account.exceptions import AccountMissing 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 @@ class Asset(Workflow, ModelSQL, ModelView): 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 @@ class Asset(Workflow, ModelSQL, ModelView): ], ], 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 @@ class Asset(Workflow, ModelSQL, ModelView): 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 @@ class Asset(Workflow, ModelSQL, ModelView): ('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 @@ class Asset(Workflow, ModelSQL, ModelView): ('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 @@ class Asset(Workflow, ModelSQL, ModelView): ('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 @@ class Asset(Workflow, ModelSQL, ModelView): ], '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 --git a/modules/account_budget/account.py b/modules/account_budget/account.py index 19b7936764..eeee829b0d 100644 --- a/modules/account_budget/account.py +++ b/modules/account_budget/account.py @@ -93,7 +93,8 @@ class BudgetMixin(ChatMixin): 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 @@ class Budget(BudgetMixin, ModelSQL, ModelView): 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 --git a/modules/account_invoice/invoice.py b/modules/account_invoice/invoice.py index c2a03b95a1..86affedb39 100644 --- a/modules/account_invoice/invoice.py +++ b/modules/account_invoice/invoice.py @@ -75,9 +75,9 @@ class Invoice( 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 @@ class Invoice( ('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 @@ class Invoice( 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 @@ class Invoice( 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 @@ class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin): '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 @@ class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin): 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 @@ class InvoiceTax(sequence_ordered(), ModelSQL, ModelView): 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 @@ class InvoiceTax(sequence_ordered(), ModelSQL, ModelView): ('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 --git a/modules/account_invoice/tests/scenario_invoice_check_source.rst b/modules/account_invoice/tests/scenario_invoice_check_source.rst index f09b67b3e3..9aea8676c2 100644 --- a/modules/account_invoice/tests/scenario_invoice_check_source.rst +++ b/modules/account_invoice/tests/scenario_invoice_check_source.rst @@ -58,7 +58,7 @@ Create invoice:: ... '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 --git a/modules/account_invoice_defer/account.py b/modules/account_invoice_defer/account.py index 30270d6c04..fc92f92b0a 100644 --- a/modules/account_invoice_defer/account.py +++ b/modules/account_invoice_defer/account.py @@ -63,7 +63,8 @@ class InvoiceDeferred(Workflow, ModelSQL, ModelView): 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 --git a/modules/account_product/product.py b/modules/account_product/product.py index 8f78062241..036fece142 100644 --- a/modules/account_product/product.py +++ b/modules/account_product/product.py @@ -57,7 +57,7 @@ class Category(CompanyMultiValueMixin, metaclass=PoolMeta): 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 --git a/modules/account_statement/statement.py b/modules/account_statement/statement.py index 1b63dee398..f266738b42 100644 --- a/modules/account_statement/statement.py +++ b/modules/account_statement/statement.py @@ -79,7 +79,8 @@ class Statement(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class Statement(Workflow, ModelSQL, ModelView, ChatMixin): ('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 @@ class Line(origin_mixin(_states), sequence_ordered(), ModelSQL, ModelView): 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 @@ class Line(origin_mixin(_states), sequence_ordered(), ModelSQL, ModelView): 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 @@ class LineGroup(ModelSQL, ModelView): _states = { - 'readonly': (Eval('statement_state') != 'draft') | Eval('lines', []), + 'readonly': Eval('statement_state') != 'draft', + 'editable': ~Eval('lines', []), } diff --git a/modules/account_stock_eu_excise/product.py b/modules/account_stock_eu_excise/product.py index b30311a1df..b6b385fe91 100644 --- a/modules/account_stock_eu_excise/product.py +++ b/modules/account_stock_eu_excise/product.py @@ -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 @@ class Product_EUExciseTax(ModelSQL, ModelView): country = fields.Many2One( 'country.country', "Country", required=True, states={ - 'readonly': Bool(Eval('excise_tax')), + 'editable': ~Eval('excise_tax'), }) @classmethod diff --git a/modules/account_stock_eu_excise/sale.py b/modules/account_stock_eu_excise/sale.py index fd5a8a3721..acf5be4d4a 100644 --- a/modules/account_stock_eu_excise/sale.py +++ b/modules/account_stock_eu_excise/sale.py @@ -29,7 +29,8 @@ class Sale(metaclass=PoolMeta): ], 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 @@ class Sale(metaclass=PoolMeta): @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 --git a/modules/account_stock_shipment_cost/account.py b/modules/account_stock_shipment_cost/account.py index a940d7668b..a000d0e52a 100644 --- a/modules/account_stock_shipment_cost/account.py +++ b/modules/account_stock_shipment_cost/account.py @@ -448,9 +448,9 @@ class InvoiceLine(metaclass=PoolMeta): __name__ = 'account.invoice.line' shipment_cost = fields.Many2One( 'account.shipment_cost', "Shipment Cost", - readonly=True, states={ 'invisible': ~Eval('shipment_cost'), + 'editable': False, }) @classmethod diff --git a/modules/account_tax_rule_country/purchase.py b/modules/account_tax_rule_country/purchase.py index e2de0d3b0d..5af538ae4c 100644 --- a/modules/account_tax_rule_country/purchase.py +++ b/modules/account_tax_rule_country/purchase.py @@ -13,8 +13,11 @@ class Purchase(metaclass=PoolMeta): 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 --git a/modules/account_tax_rule_country/sale.py b/modules/account_tax_rule_country/sale.py index 39e20aa24c..00a0637cc4 100644 --- a/modules/account_tax_rule_country/sale.py +++ b/modules/account_tax_rule_country/sale.py @@ -13,8 +13,11 @@ class Sale(metaclass=PoolMeta): 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 --git a/modules/analytic_budget/analytic_account.py b/modules/analytic_budget/analytic_account.py index 7a27c2a5ca..e54bfa07a8 100644 --- a/modules/analytic_budget/analytic_account.py +++ b/modules/analytic_budget/analytic_account.py @@ -53,7 +53,7 @@ class Budget(BudgetMixin, ModelSQL, ModelView): ('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 --git a/modules/carrier_weight/carrier.py b/modules/carrier_weight/carrier.py index 8fa94a816a..6810c8499d 100644 --- a/modules/carrier_weight/carrier.py +++ b/modules/carrier_weight/carrier.py @@ -17,14 +17,14 @@ class Carrier(metaclass=PoolMeta): 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 --git a/modules/party/party.py b/modules/party/party.py index a177bc0707..f377c8c31f 100644 --- a/modules/party/party.py +++ b/modules/party/party.py @@ -47,7 +47,8 @@ class Party( 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 --git a/modules/party_relationship/party.py b/modules/party_relationship/party.py index c73869b120..7afc777660 100644 --- a/modules/party_relationship/party.py +++ b/modules/party_relationship/party.py @@ -196,7 +196,8 @@ class RelationAll(Relation, ModelView): 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 --git a/modules/product/product.py b/modules/product/product.py index fc2f158a7e..3bb43b706d 100644 --- a/modules/product/product.py +++ b/modules/product/product.py @@ -153,7 +153,7 @@ class Template( 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 @@ class Product( 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 @@ class Product( 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 @@ class Product( 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 --git a/modules/product_cost_warehouse/product.py b/modules/product_cost_warehouse/product.py index 052125645b..cc04252eb9 100644 --- a/modules/product_cost_warehouse/product.py +++ b/modules/product_cost_warehouse/product.py @@ -40,7 +40,7 @@ class Product(metaclass=PoolMeta): 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 --git a/modules/production/production.py b/modules/production/production.py index adb59ef090..975857dc3b 100644 --- a/modules/production/production.py +++ b/modules/production/production.py @@ -47,16 +47,20 @@ class Production( ('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 @@ class Production( ), ], 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 @@ class Production( ('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 @@ class Production( ('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 --git a/modules/production/stock.py b/modules/production/stock.py index ca6a92b204..ebb511b636 100644 --- a/modules/production/stock.py +++ b/modules/production/stock.py @@ -46,7 +46,7 @@ class Location(metaclass=PoolMeta): 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 @@ class Move(metaclass=PoolMeta): ], 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 @@ class Move(metaclass=PoolMeta): ], states={ 'invisible': ~Eval('production_output'), + 'readonly': Eval('id', -1) >= 0, }) production = fields.Function(fields.Many2One( 'production', "Production", diff --git a/modules/production/tests/scenario_production_set_cost.rst b/modules/production/tests/scenario_production_set_cost.rst index a945d65909..777fe23aea 100644 --- a/modules/production/tests/scenario_production_set_cost.rst +++ b/modules/production/tests/scenario_production_set_cost.rst @@ -82,7 +82,9 @@ Change cost of input:: >>> 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 --git a/modules/project/work.py b/modules/project/work.py index ccca777ac2..9480bc920a 100644 --- a/modules/project/work.py +++ b/modules/project/work.py @@ -161,7 +161,7 @@ class Work( company = fields.Many2One( 'company.company', "Company", required=True, states={ - 'readonly': ( + 'editable': ~( Eval('origin', None) | Eval('parent', None) | Eval('children', []) diff --git a/modules/purchase/purchase.py b/modules/purchase/purchase.py index e18df605f8..d0d0c49ed4 100644 --- a/modules/purchase/purchase.py +++ b/modules/purchase/purchase.py @@ -85,9 +85,9 @@ class Purchase( 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 @@ class Purchase( }) 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 @@ class Purchase( 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 @@ class Purchase( 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 @@ class Line(sequence_ordered(), ModelSQL, ModelView): 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 @@ class Line(sequence_ordered(), ModelSQL, ModelView): 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 @@ class Line(sequence_ordered(), ModelSQL, ModelView): }, 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 @@ class Line(sequence_ordered(), ModelSQL, ModelView): ], states={ 'invisible': Eval('type') != 'line', + 'editable': False }) purchase_state = fields.Function( fields.Selection('get_purchase_states', 'Purchase State'), diff --git a/modules/purchase_amendment/purchase.py b/modules/purchase_amendment/purchase.py index 64474f1fcc..e0e0a3b37c 100644 --- a/modules/purchase_amendment/purchase.py +++ b/modules/purchase_amendment/purchase.py @@ -42,7 +42,8 @@ class Amendment(Workflow, ModelSQL, ModelView): ('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 @@ class AmendmentLine(ModelSQL, ModelView): ('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 --git a/modules/purchase_blanket_agreement/purchase.py b/modules/purchase_blanket_agreement/purchase.py index 770d5b1f2e..f829417112 100644 --- a/modules/purchase_blanket_agreement/purchase.py +++ b/modules/purchase_blanket_agreement/purchase.py @@ -115,9 +115,9 @@ class BlanketAgreement(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class BlanketAgreement(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class BlanketAgreement(Workflow, ModelSQL, ModelView, ChatMixin): 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 --git a/modules/purchase_price_list/purchase.py b/modules/purchase_price_list/purchase.py index fafefea622..fbba99a385 100644 --- a/modules/purchase_price_list/purchase.py +++ b/modules/purchase_price_list/purchase.py @@ -10,10 +10,7 @@ class Purchase(metaclass=PoolMeta): @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 --git a/modules/purchase_request_quotation/purchase.py b/modules/purchase_request_quotation/purchase.py index 6b35ec3c92..3762af6d18 100644 --- a/modules/purchase_request_quotation/purchase.py +++ b/modules/purchase_request_quotation/purchase.py @@ -111,7 +111,7 @@ class Quotation(Workflow, ModelSQL, ModelView, ChatMixin): 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 --git a/modules/purchase_requisition/purchase.py b/modules/purchase_requisition/purchase.py index e7d0455ecc..7b45da195c 100644 --- a/modules/purchase_requisition/purchase.py +++ b/modules/purchase_requisition/purchase.py @@ -78,7 +78,8 @@ class PurchaseRequisition(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class PurchaseRequisition(Workflow, ModelSQL, ModelView, ChatMixin): 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 --git a/modules/purchase_secondary_unit/purchase.py b/modules/purchase_secondary_unit/purchase.py index de14e5fe86..f40de06ef9 100644 --- a/modules/purchase_secondary_unit/purchase.py +++ b/modules/purchase_secondary_unit/purchase.py @@ -43,14 +43,16 @@ class Line(metaclass=PoolMeta): 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 --git a/modules/sale/sale.py b/modules/sale/sale.py index 830c0d9058..e15c594630 100644 --- a/modules/sale/sale.py +++ b/modules/sale/sale.py @@ -81,9 +81,9 @@ class Sale( 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 @@ class Sale( 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 @@ class Sale( 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 @@ class Sale( 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 @@ class Sale( }) 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, @@ -556,6 +553,10 @@ class Sale( def default_invoice_state(): return 'none' + @classmethod + def default_to_invoice(cls): + return False + @classmethod def default_shipment_method(cls, **pattern): Config = Pool().get('sale.configuration') @@ -566,6 +567,10 @@ class Sale( 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 @@ class Sale( 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 @@ class SaleLine(TaxableMixin, sequence_ordered(), ModelSQL, ModelView): 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 --git a/modules/sale/tests/scenario_sale_quotation.rst b/modules/sale/tests/scenario_sale_quotation.rst index e6db14a0bf..e51dd0eb37 100644 --- a/modules/sale/tests/scenario_sale_quotation.rst +++ b/modules/sale/tests/scenario_sale_quotation.rst @@ -46,8 +46,9 @@ Create a quotation:: 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 --git a/modules/sale_amendment/sale.py b/modules/sale_amendment/sale.py index 4aefbef408..9c764ea20f 100644 --- a/modules/sale_amendment/sale.py +++ b/modules/sale_amendment/sale.py @@ -42,7 +42,8 @@ class Amendment(Workflow, ModelSQL, ModelView): ('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 @@ class AmendmentLine(ModelSQL, ModelView): ('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 --git a/modules/sale_blanket_agreement/sale.py b/modules/sale_blanket_agreement/sale.py index b91c8ce841..3b52954105 100644 --- a/modules/sale_blanket_agreement/sale.py +++ b/modules/sale_blanket_agreement/sale.py @@ -115,9 +115,9 @@ class BlanketAgreement(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class BlanketAgreement(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class BlanketAgreement(Workflow, ModelSQL, ModelView, ChatMixin): 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 --git a/modules/sale_complaint/complaint.py b/modules/sale_complaint/complaint.py index fcd573ca68..2c51da3022 100644 --- a/modules/sale_complaint/complaint.py +++ b/modules/sale_complaint/complaint.py @@ -56,7 +56,8 @@ class Complaint(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class Complaint(Workflow, ModelSQL, ModelView, ChatMixin): ], }, 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 @@ class Complaint(Workflow, ModelSQL, ModelView, ChatMixin): 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 --git a/modules/sale_extra/sale.py b/modules/sale_extra/sale.py index 52204fbdb1..ff57210a46 100644 --- a/modules/sale_extra/sale.py +++ b/modules/sale_extra/sale.py @@ -71,7 +71,7 @@ class Extra(DeactivableMixin, ModelSQL, ModelView, MatchMixin): 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 --git a/modules/sale_gift_card/sale.py b/modules/sale_gift_card/sale.py index 95d713f3c4..7e8aad3d40 100644 --- a/modules/sale_gift_card/sale.py +++ b/modules/sale_gift_card/sale.py @@ -11,7 +11,7 @@ from trytond.model import Exclude, ModelSQL, ModelView, Workflow, fields 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 @@ class GiftCard(ModelSQL, ModelView): _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 @@ class GiftCard(ModelSQL, ModelView): 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 --git a/modules/sale_opportunity/opportunity.py b/modules/sale_opportunity/opportunity.py index 8ddb54d785..9d33a22c02 100644 --- a/modules/sale_opportunity/opportunity.py +++ b/modules/sale_opportunity/opportunity.py @@ -65,7 +65,8 @@ class SaleOpportunity( 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 --git a/modules/sale_point/sale.py b/modules/sale_point/sale.py index 42f2496987..03b3ee0142 100644 --- a/modules/sale_point/sale.py +++ b/modules/sale_point/sale.py @@ -111,9 +111,8 @@ class POSSale(Workflow, ModelSQL, ModelView, TaxableMixin, ChatMixin): 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 @@ class POSSaleLine(ModelSQL, ModelView, TaxableMixin): 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 --git a/modules/sale_price_list/sale.py b/modules/sale_price_list/sale.py index 17f81e249b..dd56bb0253 100644 --- a/modules/sale_price_list/sale.py +++ b/modules/sale_price_list/sale.py @@ -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 @@ class Sale(metaclass=PoolMeta): 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 --git a/modules/sale_promotion/sale.py b/modules/sale_promotion/sale.py index 426131f1bf..167cdd011c 100644 --- a/modules/sale_promotion/sale.py +++ b/modules/sale_promotion/sale.py @@ -184,7 +184,7 @@ class Promotion( 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 --git a/modules/sale_rental/sale.py b/modules/sale_rental/sale.py index b518fc0628..967ac90a3f 100644 --- a/modules/sale_rental/sale.py +++ b/modules/sale_rental/sale.py @@ -130,7 +130,8 @@ class Rental( 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 @@ class Rental( 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 @@ class Rental( 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 @@ class Rental( 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 --git a/modules/sale_secondary_unit/sale.py b/modules/sale_secondary_unit/sale.py index 18eaf63554..531543f266 100644 --- a/modules/sale_secondary_unit/sale.py +++ b/modules/sale_secondary_unit/sale.py @@ -43,14 +43,16 @@ class Line(metaclass=PoolMeta): 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 --git a/modules/sale_shipment_cost/stock.py b/modules/sale_shipment_cost/stock.py index 62faf4d7f8..1dfc612eb9 100644 --- a/modules/sale_shipment_cost/stock.py +++ b/modules/sale_shipment_cost/stock.py @@ -17,9 +17,8 @@ class ShipmentCostSaleMixin: '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 @@ class ShipmentCostSaleMixin: "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 --git a/modules/sale_subscription/subscription.py b/modules/sale_subscription/subscription.py index 90c3b2a88f..7f0adfc1f6 100644 --- a/modules/sale_subscription/subscription.py +++ b/modules/sale_subscription/subscription.py @@ -32,9 +32,9 @@ class Subscription(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class Subscription(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class Subscription(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class Subscription(Workflow, ModelSQL, ModelView, ChatMixin): 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 --git a/modules/stock/inventory.py b/modules/stock/inventory.py index 3094fcfcfc..bb915919ba 100644 --- a/modules/stock/inventory.py +++ b/modules/stock/inventory.py @@ -34,12 +34,15 @@ class Inventory(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class Inventory(Workflow, ModelSQL, ModelView, ChatMixin): 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 --git a/modules/stock/move.py b/modules/stock/move.py index 63c08340b2..c3ee60be66 100644 --- a/modules/stock/move.py +++ b/modules/stock/move.py @@ -18,7 +18,7 @@ from trytond.model import ( 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 @@ class Move(Workflow, ModelSQL, ModelView): 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 @@ class Move(Workflow, ModelSQL, ModelView): '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 @@ class Move(Workflow, ModelSQL, ModelView): '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 @@ class Move(Workflow, ModelSQL, ModelView): "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 @@ class Move(Workflow, ModelSQL, ModelView): 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 @@ class Move(Workflow, ModelSQL, ModelView): @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 --git a/modules/stock/shipment.py b/modules/stock/shipment.py index 6631590aea..6ff277910c 100644 --- a/modules/stock/shipment.py +++ b/modules/stock/shipment.py @@ -359,9 +359,10 @@ class ShipmentIn( 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 @@ class ShipmentIn( 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 @@ class ShipmentInReturn( 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 @@ class ShipmentInReturn( ], 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 @@ class ShipmentOut( 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 @@ class ShipmentOut( 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 @@ class ShipmentOutReturn( 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 @@ class ShipmentOutReturn( 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 @@ class ShipmentInternal( 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 --git a/modules/stock/tests/scenario_stock_period.rst b/modules/stock/tests/scenario_stock_period.rst index 90641e85cc..a161b89383 100644 --- a/modules/stock/tests/scenario_stock_period.rst +++ b/modules/stock/tests/scenario_stock_period.rst @@ -107,7 +107,10 @@ Reopen the period:: 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 --git a/modules/stock/tests/scenario_stock_recompute_average_cost_price.rst b/modules/stock/tests/scenario_stock_recompute_average_cost_price.rst index fa2ffce595..e399574615 100644 --- a/modules/stock/tests/scenario_stock_recompute_average_cost_price.rst +++ b/modules/stock/tests/scenario_stock_recompute_average_cost_price.rst @@ -10,7 +10,7 @@ Imports:: >>> 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 @@ Imports:: 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 @@ Get company:: >>> company = get_company() + >>> Move = Model.get('stock.move') + Create product:: >>> ProductUom = Model.get('product.uom') @@ -129,8 +131,10 @@ Update unit price of a move:: ... ]) >>> 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 @@ Update unit price of a move:: 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 --git a/modules/stock_forecast/forecast.py b/modules/stock_forecast/forecast.py index 33bfeebf24..c9dd0c6d00 100644 --- a/modules/stock_forecast/forecast.py +++ b/modules/stock_forecast/forecast.py @@ -32,7 +32,8 @@ class Forecast(Workflow, ModelSQL, ModelView, ChatMixin): 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 @@ class Forecast(Workflow, ModelSQL, ModelView, ChatMixin): '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 --git a/modules/stock_product_location_place/stock.py b/modules/stock_product_location_place/stock.py index 8565158e28..1f05735e1e 100644 --- a/modules/stock_product_location_place/stock.py +++ b/modules/stock_product_location_place/stock.py @@ -70,7 +70,7 @@ class Move(metaclass=PoolMeta): __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 @@ class Move(metaclass=PoolMeta): ], ('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 @@ class Move(metaclass=PoolMeta): ], ('location', '=', Eval('to_location', -1)), ), - ]) + ], + states={ + 'editable': False, + }) @classmethod def __setup__(cls): diff --git a/modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst b/modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst index 431da1b7e6..bccbbedfe5 100644 --- a/modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst +++ b/modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst @@ -93,7 +93,8 @@ Request to ship to first warehouse in 2 and 3 weeks:: >>> 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 @@ Request to ship to first warehouse in 2 and 3 weeks:: >>> 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 --git a/modules/stock_shipment_cost/stock.py b/modules/stock_shipment_cost/stock.py index 5a7a3af0e4..4fb310788c 100644 --- a/modules/stock_shipment_cost/stock.py +++ b/modules/stock_shipment_cost/stock.py @@ -26,6 +26,16 @@ class Move(metaclass=PoolMeta): 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 @@ class ShipmentCostMixin: 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 @@ class ShipmentCostMixin: 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 --git a/modules/stock_shipment_measurements/stock.py b/modules/stock_shipment_measurements/stock.py index ec41b10331..596848a970 100644 --- a/modules/stock_shipment_measurements/stock.py +++ b/modules/stock_shipment_measurements/stock.py @@ -83,6 +83,14 @@ class Move(metaclass=PoolMeta): "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']) @@ -147,6 +155,13 @@ class Move(metaclass=PoolMeta): values['internal_volume'] = internal_volume 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') @@ -229,6 +244,14 @@ class MeasurementsMixin(object): if set_measurements: 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 @@ class MeasurementsMixin(object): 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 --git a/trytond/trytond/ir/cron.py b/trytond/trytond/ir/cron.py index ed5022c3ab..e2de08839b 100644 --- a/trytond/trytond/ir/cron.py +++ b/trytond/trytond/ir/cron.py @@ -36,7 +36,7 @@ def str2bigint(s): 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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView): [('minute', '>=', 0), ('minute', '<=', 59)], ], states={ - 'readonly': _states['readonly'], + 'editable': _states['editable'], 'invisible': Eval('interval_type').in_(['minutes']), }, depends=['interval_type']) @@ -63,14 +63,14 @@ class Cron(DeactivableMixin, ModelSQL, ModelView): [('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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView): ('day', '>=', 0), ], states={ - 'readonly': _states['readonly'], + 'editable': _states['editable'], 'invisible': Eval('interval_type').in_( ['minutes', 'hours', 'days', 'weeks']), }, diff --git a/trytond/trytond/model/modelstorage.py b/trytond/trytond/model/modelstorage.py index 48b01aea06..de8c320041 100644 --- a/trytond/trytond/model/modelstorage.py +++ b/trytond/trytond/model/modelstorage.py @@ -564,7 +564,10 @@ class ModelStorage(Model): 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: