diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py index 0e2bd09d2ee..17e188352e1 100644 --- a/addons/repair/models/repair.py +++ b/addons/repair/models/repair.py @@ -38,6 +38,7 @@ class Repair(models.Model): readonly=True, required=True, states={'draft': [('readonly', False)]}) product_uom = fields.Many2one( 'uom.uom', 'Product Unit of Measure', + compute='_compute_product_uom', store=True, precompute=True, readonly=True, required=True, states={'draft': [('readonly', False)]}, domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') partner_id = fields.Many2one( @@ -68,6 +69,7 @@ class Repair(models.Model): schedule_date = fields.Date("Scheduled Date") location_id = fields.Many2one( 'stock.location', 'Location', + compute="_compute_location_id", store=True, precompute=True, index=True, readonly=True, required=True, check_company=True, help="This is the location where the product to repair is located.", states={'draft': [('readonly', False)], 'confirmed': [('readonly', True)]}) @@ -196,13 +198,17 @@ class Repair(models.Model): 'warning': {'title': "Warning", 'message': "Note that the warehouse of the return and repair locations don't match!"}, } + @api.depends('product_id') + def _compute_product_uom(self): + for repair in self: + if repair.product_id: + repair.product_uom = repair.product_id.uom_id + @api.onchange('product_id') def onchange_product_id(self): self.guarantee_limit = False if (self.product_id and self.lot_id and self.lot_id.product_id != self.product_id) or not self.product_id: self.lot_id = False - if self.product_id: - self.product_uom = self.product_id.uom_id.id @api.onchange('product_uom') def onchange_product_uom(self): @@ -229,14 +235,15 @@ class Repair(models.Model): self.partner_invoice_id = addresses['invoice'] self.pricelist_id = self.partner_id.property_product_pricelist.id - @api.onchange('company_id') - def _onchange_company_id(self): - if self.company_id: - if self.location_id.company_id != self.company_id: - warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)], limit=1) - self.location_id = warehouse.lot_stock_id - else: - self.location_id = False + @api.depends('company_id') + def _compute_location_id(self): + for order in self: + if order.company_id: + if order.location_id.company_id != order.company_id: + warehouse = self.env['stock.warehouse'].search([('company_id', '=', order.company_id.id)], limit=1) + order.location_id = warehouse.lot_stock_id + else: + order.location_id = False @api.ondelete(at_uninstall=False) def _unlink_except_confirmed(self): @@ -676,6 +683,7 @@ class RepairLine(models.Model): digits='Product Unit of Measure', required=True) product_uom = fields.Many2one( 'uom.uom', 'Product Unit of Measure', + compute='_compute_product_uom', store=True, readonly=False, precompute=True, required=True, domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') invoice_line_id = fields.Many2one( @@ -683,9 +691,11 @@ class RepairLine(models.Model): copy=False, readonly=True, check_company=True) location_id = fields.Many2one( 'stock.location', 'Source Location', + compute='_compute_location_id', store=True, readonly=False, precompute=True, index=True, required=True, check_company=True) location_dest_id = fields.Many2one( 'stock.location', 'Dest. Location', + compute='_compute_location_id', store=True, readonly=False, precompute=True, index=True, required=True, check_company=True) move_id = fields.Many2one( 'stock.move', 'Inventory Move', @@ -709,6 +719,26 @@ class RepairLine(models.Model): line.price_subtotal = taxes['total_excluded'] line.price_total = taxes['total_included'] + @api.depends('product_id') + def _compute_product_uom(self): + for line in self: + line.product_uom = line.product_id.uom_id.id + + @api.depends('type') + def _compute_location_id(self): + for line in self: + if not line.type: + line.location_id = False + line.location_dest_id = False + elif line.type == 'add': + args = line.repair_id.company_id and [('company_id', '=', line.repair_id.company_id.id)] or [] + warehouse = line.env['stock.warehouse'].search(args, limit=1) + line.location_id = warehouse.lot_stock_id + line.location_dest_id = line.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', line.repair_id.company_id.id)], limit=1) + else: + line.location_id = line.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', line.repair_id.company_id.id)], limit=1).id + line.location_dest_id = line.env['stock.location'].search([('scrap_location', '=', True), ('company_id', 'in', [line.repair_id.company_id.id, False])], limit=1).id + @api.onchange('type') def onchange_operation_type(self): """ On change of operation type it sets source location, destination location @@ -718,19 +748,12 @@ class RepairLine(models.Model): @return: Dictionary of values. """ if not self.type: - self.location_id = False - self.location_dest_id = False + pass elif self.type == 'add': self.onchange_product_id() - args = self.repair_id.company_id and [('company_id', '=', self.repair_id.company_id.id)] or [] - warehouse = self.env['stock.warehouse'].search(args, limit=1) - self.location_id = warehouse.lot_stock_id - self.location_dest_id = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.repair_id.company_id.id)], limit=1) else: self.price_unit = 0.0 self.tax_id = False - self.location_id = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.repair_id.company_id.id)], limit=1).id - self.location_dest_id = self.env['stock.location'].search([('scrap_location', '=', True), ('company_id', 'in', [self.repair_id.company_id.id, False])], limit=1).id @api.onchange('repair_id', 'product_id', 'product_uom_qty') def onchange_product_id(self): @@ -750,7 +773,6 @@ class RepairLine(models.Model): self.name += '\n' + self.product_id.with_context(lang=partner.lang).description_sale else: self.name += '\n' + self.product_id.description_sale - self.product_uom = product.uom_id.id if self.type != 'remove': if partner: fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id) diff --git a/addons/repair/tests/test_repair.py b/addons/repair/tests/test_repair.py index 2cc9f6154af..3a7bae8013c 100644 --- a/addons/repair/tests/test_repair.py +++ b/addons/repair/tests/test_repair.py @@ -343,3 +343,36 @@ class TestRepair(AccountTestInvoicingCommon): for repair in return_picking.repair_ids: self.assertEqual(repair.location_id, return_picking.location_dest_id, "Repair location should have defaulted to return destination location") self.assertEqual(repair.partner_id, return_picking.partner_id, "Repair customer should have defaulted to return customer") + + def test_repair_compute_product_uom(self): + repair = self.env['repair.order'].create({ + 'product_id': self.product_product_3.id, + 'operations': [ + (0, 0, { + 'name': 'foo', + 'product_id': self.product_product_11.id, + 'price_unit': 50.0, + }) + ], + }) + self.assertEqual(repair.product_uom, self.product_product_3.uom_id) + self.assertEqual(repair.operations[0].product_uom, self.product_product_11.uom_id) + + def test_repair_compute_location(self): + repair = self.env['repair.order'].create({ + 'product_id': self.product_product_3.id, + 'operations': [ + (0, 0, { + 'name': 'foo', + 'product_id': self.product_product_11.id, + 'price_unit': 50.0, + }) + ], + }) + self.assertEqual(repair.location_id, self.stock_warehouse.lot_stock_id) + self.assertEqual(repair.operations[0].location_id, self.stock_warehouse.lot_stock_id) + location_dest_id = self.env['stock.location'].search([ + ('usage', '=', 'production'), + ('company_id', '=', repair.company_id.id), + ], limit=1) + self.assertEqual(repair.operations[0].location_dest_id, location_dest_id) diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index 710b00fa2d9..d563adb4206 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -1577,6 +1577,29 @@ class PrecomputeEditable(models.Model): record.baz2 = record.baz +class PrecomputeReadonly(models.Model): + _name = 'test_new_api.precompute.readonly' + _description = 'a model with precomputed readonly fields' + + foo = fields.Char() + state = fields.Selection([ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ], default='draft') + bar = fields.Char(compute='_compute_bar', precompute=True, store=True, readonly=True) + baz = fields.Char( + compute='_compute_baz', precompute=True, store=True, readonly=True, states={'draft': [('readonly', False)]} + ) + + @api.depends('foo') + def _compute_bar(self): + self.bar = "COMPUTED" + + @api.depends('bar') + def _compute_baz(self): + self.baz = "COMPUTED" + + class PrecomputeRequired(models.Model): _name = 'test_new_api.precompute.required' _description = 'a model with precomputed required fields' diff --git a/odoo/addons/test_new_api/security/ir.model.access.csv b/odoo/addons/test_new_api/security/ir.model.access.csv index 02aa6cb7a37..da38b7734ef 100644 --- a/odoo/addons/test_new_api/security/ir.model.access.csv +++ b/odoo/addons/test_new_api/security/ir.model.access.csv @@ -89,6 +89,7 @@ access_test_new_api_precompute,access_test_new_api_precompute,model_test_new_api access_test_new_api_precompute_line,access_test_new_api_precompute_line,model_test_new_api_precompute_line,,1,0,0,0 access_test_new_api_precompute_combo,access_test_new_api_precompute_combo,model_test_new_api_precompute_combo,,1,0,0,0 access_test_new_api_precompute_editable,access_test_new_api_precompute_editable,model_test_new_api_precompute_editable,,1,0,0,0 +access_test_new_api_precompute_readonly,access_test_new_api_precompute_readonly,model_test_new_api_precompute_readonly,,1,0,0,0 access_test_new_api_precompute_required,access_test_new_api_precompute_required,model_test_new_api_precompute_required,,1,0,0,0 access_test_new_api_precompute_monetary,access_test_new_api_precompute_monetary,model_test_new_api_precompute_monetary,,1,0,0,0 access_test_new_api_prefetch,access_test_new_api_prefetch,model_test_new_api_prefetch,,1,0,0,0 diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 1a57d51bdcd..4e48e5d835f 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -3928,6 +3928,43 @@ class TestPrecompute(common.TransactionCase): self.assertEqual(record.baz, 'baz') self.assertEqual(record.baz2, 'baz') + def test_precompute_readonly(self): + """ + Ensures + - a stored, precomputed, readonly field cannot be altered by the user, + - a stored, precomputed, readonly field, + but with a states attributes changing the readonly of the field according to the state of the record, + can be altered by the user. + The `bar` field is store=True, precompute=True, readonly=True + The `baz` field is store=True, precompute=True, readonly=True, states={'draft': [('readonly', False)]} + """ + model = self.env['test_new_api.precompute.readonly'] + + # no value for bar, no value for baz + record = model.create({'foo': 'foo'}) + self.assertEqual(record.bar, 'COMPUTED') + self.assertEqual(record.baz, 'COMPUTED') + + # value for bar, no value for baz + # bar is readonly, it must ignore the value for bar in the create values + record = model.create({'foo': 'foo', 'bar': 'bar'}) + self.assertEqual(record.bar, 'COMPUTED') + self.assertEqual(record.baz, 'COMPUTED') + + # no value for bar, value for baz + # baz is readonly=True but states={'draft': [('readonly', False)]} + # the value for baz must be taken into account + record = model.create({'foo': 'foo', 'baz': 'baz'}) + self.assertEqual(record.bar, 'COMPUTED') + self.assertEqual(record.baz, 'baz') + + # value for bar, value for baz + # bar must be ignored + # baz must be taken into account + record = model.create({'foo': 'foo', 'bar': 'bar', 'baz': 'baz'}) + self.assertEqual(record.bar, 'COMPUTED') + self.assertEqual(record.baz, 'baz') + def test_precompute_required(self): model = self.env['test_new_api.precompute.required'] diff --git a/odoo/models.py b/odoo/models.py index f9007e1be5f..64773c7202c 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -4233,6 +4233,19 @@ class BaseModel(metaclass=MetaModel): fname for fname, field in self._fields.items() if field.precompute and field.readonly + # ignore `readonly=True` when it's combined with the `states` attribute, + # making the field readonly according to the record state. + # e.g. + # product_uom = fields.Many2one( + # 'uom.uom', 'Product Unit of Measure', + # compute='_compute_product_uom', store=True, precompute=True, + # readonly=True, required=True, states={'draft': [('readonly', False)]}, + # ) + and (not field.states or not any( + modifier == 'readonly' + for modifiers in field.states.values() + for modifier, _value in modifiers + )) ) result_vals_list = []