[FIX] mrp: cost and attributes

When retrieving the BOM lines in `_get_bom_lines`, we check the product
attribute values to know if the BOM line should be taken into account or
not. However, we do not do such a check when we retrieve the price in
`_get_price`.

It can lead to an inconsistency between the sub-lines displayed and the
total price.

We need to apply the same filtering logic when getting the price.

opw-1932437

closes odoo/odoo#30917
This commit is contained in:
Nicolas Martinelli
2019-02-08 13:49:49 +00:00
committed by Arnold Moyaux
parent 288b5e3a41
commit afc5ec8bdf
2 changed files with 216 additions and 3 deletions
@@ -140,7 +140,7 @@ class ReportBomStructure(models.AbstractModel):
price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity
if line.child_bom_id:
factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty
sub_total = self._get_price(line.child_bom_id, factor)
sub_total = self._get_price(line.child_bom_id, factor, line.product_id)
else:
sub_total = price
sub_total = self.env.user.company_id.currency_id.round(sub_total)
@@ -180,7 +180,7 @@ class ReportBomStructure(models.AbstractModel):
})
return operations
def _get_price(self, bom, factor):
def _get_price(self, bom, factor, product):
price = 0
if bom.routing_id:
# routing are defined on a BoM and don't have a concept of quantity.
@@ -194,9 +194,11 @@ class ReportBomStructure(models.AbstractModel):
price += sum([op['total'] for op in operations])
for line in bom.bom_line_ids:
if line._skip_bom_line(product):
continue
if line.child_bom_id:
qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id)
sub_price = self._get_price(line.child_bom_id, qty)
sub_price = self._get_price(line.child_bom_id, qty, line.product_id)
price += sub_price
else:
prod_qty = line.product_qty * factor
+211
View File
@@ -417,3 +417,214 @@ class TestBoM(TestMrpCommon):
self.assertEqual(float_compare(component_line['total'], crumble_cost, precision_digits=2), 0)
# total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost
self.assertEqual(float_compare(report_values['lines']['total'], 27.18 + crumble_cost, precision_digits=2), 0, 'Product Bom Price is not correct')
def test_bom_report_variant(self):
""" Test a sub BoM process with multiple variants.
BOM 1:
product template = car
quantity = 5 units
- red paint 50l -> red car (product.product)
- blue paint 50l -> blue car
- red dashboard with gps -> red car with GPS
- red dashboard w/h gps -> red w/h GPS
- blue dashboard with gps -> blue car with GPS
- blue dashboard w/h gps -> blue w/h GPS
BOM 2:
product_tmpl = dashboard
quantity = 2
- red paint 1l -> red dashboard (product.product)
- blue paint 1l -> blue dashboard
- gps -> dashboard with gps
Check the Price for a Blue Car with GPS -> 910$:
10l of blue paint -> 200$
1 blue dashboard GPS -> 710$:
- 0.5l of blue paint -> 10$
- GPS -> 700$
Check the price for a red car -> 10.5l of red paint -> 210$
"""
# Create a product template car with attributes gps(yes, no), color(red, blue)
self.car = self.env['product.template'].create({
'name': 'Car',
})
self.gps_attribute = self.env['product.attribute'].create({'name': 'GPS', 'sequence': 1})
self.gps_yes = self.env['product.attribute.value'].create({
'name': 'Yes',
'attribute_id': self.gps_attribute.id,
'sequence': 1,
})
self.gps_no = self.env['product.attribute.value'].create({
'name': 'No',
'attribute_id': self.gps_attribute.id,
'sequence': 2,
})
self.car_gps_attribute_lines = self.env['product.template.attribute.line'].create({
'product_tmpl_id': self.car.id,
'attribute_id': self.gps_attribute.id,
'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
})
self.color_attribute = self.env['product.attribute'].create({'name': 'Color', 'sequence': 1})
self.color_red = self.env['product.attribute.value'].create({
'name': 'Red',
'attribute_id': self.color_attribute.id,
'sequence': 1,
})
self.color_blue = self.env['product.attribute.value'].create({
'name': 'Blue',
'attribute_id': self.color_attribute.id,
'sequence': 2,
})
self.car_color_attribute_lines = self.env['product.template.attribute.line'].create({
'product_tmpl_id': self.car.id,
'attribute_id': self.color_attribute.id,
'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
})
self.car.create_variant_ids()
# Blue and red paint
uom_litre = self.env.ref('uom.product_uom_litre')
self.paint = self.env['product.template'].create({
'name': 'Paint',
'uom_id': uom_litre.id,
'uom_po_id': uom_litre.id
})
self.paint_color_attribute_lines = self.env['product.template.attribute.line'].create({
'product_tmpl_id': self.paint.id,
'attribute_id': self.color_attribute.id,
'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
})
self.paint.create_variant_ids()
self.paint.product_variant_ids.write({'standard_price': 20})
self.dashboard = self.env['product.template'].create({
'name': 'Dashboard',
'standard_price': 1000,
})
self.dashboard_gps_attribute_lines = self.env['product.template.attribute.line'].create({
'product_tmpl_id': self.dashboard.id,
'attribute_id': self.gps_attribute.id,
'value_ids': [(6, 0, [self.gps_yes.id, self.gps_no.id])],
})
self.dashboard_color_attribute_lines = self.env['product.template.attribute.line'].create({
'product_tmpl_id': self.dashboard.id,
'attribute_id': self.color_attribute.id,
'value_ids': [(6, 0, [self.color_red.id, self.color_blue.id])],
})
self.dashboard.create_variant_ids()
self.gps = self.env['product.product'].create({
'name': 'GPS',
'standard_price': 700,
})
bom_form_car = Form(self.env['mrp.bom'])
bom_form_car.product_tmpl_id = self.car
bom_form_car.product_qty = 5
with bom_form_car.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.paint.id), ('attribute_value_ids', '=', self.color_red.id)])
line.product_uom_id = uom_litre
line.product_qty = 50
with bom_form_car.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.paint.id), ('attribute_value_ids', '=', self.color_blue.id)])
line.product_uom_id = uom_litre
line.product_qty = 50
with bom_form_car.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.dashboard.id), ('attribute_value_ids', '=', self.gps_yes.id), ('attribute_value_ids', '=', self.color_red.id)])
line.product_qty = 5
with bom_form_car.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.dashboard.id), ('attribute_value_ids', '=', self.gps_yes.id), ('attribute_value_ids', '=', self.color_blue.id)])
line.product_qty = 5
with bom_form_car.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.dashboard.id), ('attribute_value_ids', '=', self.gps_no.id), ('attribute_value_ids', '=', self.color_red.id)])
line.product_qty = 5
with bom_form_car.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.dashboard.id), ('attribute_value_ids', '=', self.gps_no.id), ('attribute_value_ids', '=', self.color_blue.id)])
line.product_qty = 5
bom_car = bom_form_car.save()
for attribute in self.gps_attribute.value_ids | self.color_attribute.value_ids:
bom_car.bom_line_ids.filtered(lambda l: attribute in l.product_id.attribute_value_ids).write({'attribute_value_ids': [(4, attribute.id)]})
bom_dashboard = Form(self.env['mrp.bom'])
bom_dashboard.product_tmpl_id = self.dashboard
bom_dashboard.product_qty = 2
with bom_dashboard.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.paint.id), ('attribute_value_ids', '=', self.color_red.id)])
line.product_uom_id = uom_litre
line.product_qty = 1
with bom_dashboard.bom_line_ids.new() as line:
line.product_id = self.env['product.product'].search([('product_tmpl_id', '=', self.paint.id), ('attribute_value_ids', '=', self.color_blue.id)])
line.product_uom_id = uom_litre
line.product_qty = 1
with bom_dashboard.bom_line_ids.new() as line:
line.product_id = self.gps
line.product_qty = 2
bom_dashboard = bom_dashboard.save()
for attribute in self.color_attribute.value_ids:
bom_dashboard.bom_line_ids.filtered(lambda l: attribute in l.product_id.attribute_value_ids).write({'attribute_value_ids': [(4, attribute.id)]})
bom_dashboard.bom_line_ids.filtered(lambda l: l.product_id == self.gps).write({'attribute_value_ids': [(4, self.gps_yes.id)]})
blue_car_with_gps = self.env['product.product'].search([
('product_tmpl_id', '=', self.car.id),
('attribute_value_ids', '=', self.gps_yes.id),
('attribute_value_ids', '=', self.color_blue.id)
])
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=blue_car_with_gps.id)
# Two lines. blue dashboard with gps and blue paint.
self.assertEqual(len(report_values['lines']['components']), 2)
# 10l of blue paint
blue_paint = self.env['product.product'].search([
('product_tmpl_id', '=', self.paint.id),
('attribute_value_ids', '=', self.color_blue.id)
])
self.assertEqual(blue_paint.id, report_values['lines']['components'][0]['prod_id'])
self.assertEqual(report_values['lines']['components'][0]['prod_qty'], 10)
# 1 blue dashboard with GPS
blue_dashboard_gps = self.env['product.product'].search([
('product_tmpl_id', '=', self.dashboard.id),
('attribute_value_ids', '=', self.gps_yes.id),
('attribute_value_ids', '=', self.color_blue.id)
])
self.assertEqual(blue_dashboard_gps.id, report_values['lines']['components'][1]['prod_id'])
self.assertEqual(report_values['lines']['components'][1]['prod_qty'], 1)
component = report_values['lines']['components'][1]
report_values_dashboad = self.env['report.mrp.report_bom_structure']._get_bom(
component['child_bom'], component['prod_id'], component['prod_qty'],
component['line_id'], component['level'] + 1)
self.assertEqual(len(report_values_dashboad['components']), 2)
self.assertEqual(blue_paint.id, report_values_dashboad['components'][0]['prod_id'])
self.assertEqual(self.gps.id, report_values_dashboad['components'][1]['prod_id'])
# 0.5l of paint at price of 20$/litre -> 10$
self.assertEqual(report_values_dashboad['components'][0]['total'], 10)
# GPS 700$
self.assertEqual(report_values_dashboad['components'][1]['total'], 700)
# Dashboard blue with GPS should have a BoM cost of 710$
self.assertEqual(report_values['lines']['components'][1]['total'], 710)
# 10l of paint at price of 20$/litre -> 200$
self.assertEqual(report_values['lines']['components'][0]['total'], 200)
# Total cost of blue car with GPS: 10 + 700 + 200 = 910
self.assertEqual(report_values['lines']['total'], 910)
red_car_without_gps = self.env['product.product'].search([
('product_tmpl_id', '=', self.car.id),
('attribute_value_ids', '=', self.gps_no.id),
('attribute_value_ids', '=', self.color_red.id)
])
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=red_car_without_gps.id)
# Same math than before but without GPS
self.assertEqual(report_values['lines']['total'], 210)