[FIX] mrp: subproduct price + rounding
On the BoM structure report the price on line with a
child BoM is inchoerent. Also the sum of all lines in
the report is not always equals to the total of the parent
line.
For the first issue, it happens due to commit 17b6256955
that use the number of operation cycle as quantity.
The second issue happens because sometimes the rounding is not done
at the smallest level and thus the rounding of the sum is different than
the sum of the rouding.
This commit is contained in:
@@ -119,16 +119,17 @@ class ReportBomStructure(models.AbstractModel):
|
||||
continue
|
||||
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 = float_round(line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty, precision_rounding=1, rounding_method='UP')
|
||||
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)
|
||||
else:
|
||||
sub_total = price
|
||||
sub_total = self.env.user.company_id.currency_id.round(sub_total)
|
||||
components.append({
|
||||
'prod_id': line.product_id.id,
|
||||
'prod_name': line.product_id.display_name,
|
||||
'prod_qty': line_quantity,
|
||||
'prod_uom': line.product_uom_id.name,
|
||||
'prod_cost': price,
|
||||
'prod_cost': self.env.user.company_id.currency_id.round(price),
|
||||
'parent_id': bom.id,
|
||||
'line_id': line.id,
|
||||
'level': level or 0,
|
||||
@@ -151,14 +152,21 @@ class ReportBomStructure(models.AbstractModel):
|
||||
'operation': operation,
|
||||
'name': operation.name + ' - ' + operation.workcenter_id.name,
|
||||
'duration_expected': duration_expected,
|
||||
'total': float_round(total, precision_rounding=self.env.user.company_id.currency_id.rounding),
|
||||
'total': self.env.user.company_id.currency_id.round(total),
|
||||
})
|
||||
return operations
|
||||
|
||||
def _get_price(self, bom, factor):
|
||||
price = 0
|
||||
if bom.routing_id:
|
||||
operations = self._get_operation_line(bom.routing_id, factor, 0)
|
||||
# routing are defined on a BoM and don't have a concept of quantity.
|
||||
# It means that the operation time are defined for the quantity on
|
||||
# the BoM (the user produces a batch of products). E.g the user
|
||||
# product a batch of 10 units with a 5 minutes operation, the time
|
||||
# will be the 5 for a quantity between 1-10, then doubled for
|
||||
# 11-20,...
|
||||
operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP')
|
||||
operations = self._get_operation_line(bom.routing_id, operation_cycle, 0)
|
||||
price += sum([op['total'] for op in operations])
|
||||
|
||||
for line in bom.bom_line_ids:
|
||||
@@ -168,7 +176,8 @@ class ReportBomStructure(models.AbstractModel):
|
||||
price += sub_price
|
||||
else:
|
||||
prod_qty = line.product_qty * factor
|
||||
price += (line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty)
|
||||
not_rounded_price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty
|
||||
price += self.env.user.company_id.currency_id.round(not_rounded_price)
|
||||
return price
|
||||
|
||||
def _get_pdf_line(self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False):
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
from odoo.tools import float_compare, float_round
|
||||
|
||||
|
||||
class TestBoM(TestMrpCommon):
|
||||
@@ -158,3 +160,190 @@ class TestBoM(TestMrpCommon):
|
||||
})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
test_bom_3.explode(self.product_9, 1)
|
||||
|
||||
def test_bom_report(self):
|
||||
""" Simulate a crumble receipt with mrp and open the bom structure
|
||||
report and check that data insde are correct.
|
||||
"""
|
||||
uom_kg = self.env.ref('uom.product_uom_kgm')
|
||||
uom_litre = self.env.ref('uom.product_uom_litre')
|
||||
crumble = self.env['product.product'].create({
|
||||
'name': 'Crumble',
|
||||
'type': 'product',
|
||||
'uom_id': uom_kg.id,
|
||||
'uom_po_id': uom_kg.id,
|
||||
})
|
||||
butter = self.env['product.product'].create({
|
||||
'name': 'Butter',
|
||||
'type': 'product',
|
||||
'uom_id': uom_kg.id,
|
||||
'uom_po_id': uom_kg.id,
|
||||
'standard_price': 7.01
|
||||
})
|
||||
biscuit = self.env['product.product'].create({
|
||||
'name': 'Biscuit',
|
||||
'type': 'product',
|
||||
'uom_id': uom_kg.id,
|
||||
'uom_po_id': uom_kg.id,
|
||||
'standard_price': 1.5
|
||||
})
|
||||
bom_form_crumble = Form(self.env['mrp.bom'])
|
||||
bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id
|
||||
bom_form_crumble.product_qty = 11
|
||||
bom_form_crumble.product_uom_id = uom_kg
|
||||
bom_crumble = bom_form_crumble.save()
|
||||
|
||||
with Form(bom_crumble) as bom:
|
||||
with bom.bom_line_ids.new() as line:
|
||||
line.product_id = butter
|
||||
line.product_uom_id = uom_kg
|
||||
line.product_qty = 5
|
||||
with bom.bom_line_ids.new() as line:
|
||||
line.product_id = biscuit
|
||||
line.product_uom_id = uom_kg
|
||||
line.product_qty = 6
|
||||
|
||||
workcenter = self.env['mrp.workcenter'].create({
|
||||
'costs_hour': 10,
|
||||
'name': 'Deserts Table'
|
||||
})
|
||||
|
||||
routing_form = Form(self.env['mrp.routing'])
|
||||
routing_form.name = "Crumble process"
|
||||
routing_crumble = routing_form.save()
|
||||
|
||||
with Form(routing_crumble) as routing:
|
||||
with routing.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Prepare biscuits'
|
||||
operation.time_cycle_manual = 5
|
||||
with routing.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Prepare butter'
|
||||
operation.time_cycle_manual = 3
|
||||
with routing.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Mix manually'
|
||||
operation.time_cycle_manual = 5
|
||||
|
||||
bom_crumble.routing_id = routing_crumble.id
|
||||
|
||||
# TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
|
||||
# 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
|
||||
self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
|
||||
# Operation cost is the sum of operation line.
|
||||
operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
|
||||
self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
|
||||
|
||||
for component_line in report_values['lines']['components']:
|
||||
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||
if component_line['prod_id'] == butter.id:
|
||||
# 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$
|
||||
self.assertEqual(float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0)
|
||||
if component_line['prod_id'] == biscuit.id:
|
||||
# 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$
|
||||
self.assertEqual(float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0)
|
||||
# total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21
|
||||
self.assertEqual(float_compare(report_values['lines']['total'], 46.21, precision_digits=2), 0, 'Product Bom Price is not correct')
|
||||
self.assertEqual(float_compare(report_values['lines']['total'] / 11.0, 4.20, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||
|
||||
# TEST BOM STRUCTURE VALUE BY UNIT
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=1, searchVariant=False)
|
||||
# 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
|
||||
self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
|
||||
# Operation cost is the sum of operation line.
|
||||
operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
|
||||
self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
|
||||
|
||||
for component_line in report_values['lines']['components']:
|
||||
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||
if component_line['prod_id'] == butter.id:
|
||||
# 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19)
|
||||
self.assertEqual(float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0)
|
||||
if component_line['prod_id'] == biscuit.id:
|
||||
# 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82)
|
||||
self.assertEqual(float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0)
|
||||
# total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17
|
||||
self.assertEqual(float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||
|
||||
# TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY
|
||||
report_values_12 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=12, searchVariant=False)
|
||||
report_values_22 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=22, searchVariant=False)
|
||||
operation_cost = float_round(10 / 60 * 10, precision_digits=2) * 2 + float_round(6 / 60 * 10, precision_digits=2)
|
||||
# Both needs 2 operation cycle
|
||||
self.assertEqual(report_values_12['lines']['operations_cost'], report_values_22['lines']['operations_cost'])
|
||||
self.assertEqual(report_values_22['lines']['operations_cost'], operation_cost)
|
||||
report_values_23 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=23, searchVariant=False)
|
||||
operation_cost = float_round(15 / 60 * 10, precision_digits=2) * 2 + float_round(9 / 60 * 10, precision_digits=2)
|
||||
self.assertEqual(report_values_23['lines']['operations_cost'], operation_cost)
|
||||
|
||||
# Create a more complex BoM with a sub product
|
||||
cheese_cake = self.env['product.product'].create({
|
||||
'name': 'Cheese Cake 300g',
|
||||
'type': 'product',
|
||||
})
|
||||
cream = self.env['product.product'].create({
|
||||
'name': 'cream',
|
||||
'type': 'product',
|
||||
'uom_id': uom_litre.id,
|
||||
'uom_po_id': uom_litre.id,
|
||||
'standard_price': 5.17,
|
||||
})
|
||||
bom_form_cheese_cake = Form(self.env['mrp.bom'])
|
||||
bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id
|
||||
bom_form_cheese_cake.product_qty = 60
|
||||
bom_form_cheese_cake.product_uom_id = self.uom_unit
|
||||
bom_cheese_cake = bom_form_cheese_cake.save()
|
||||
|
||||
with Form(bom_cheese_cake) as bom:
|
||||
with bom.bom_line_ids.new() as line:
|
||||
line.product_id = cream
|
||||
line.product_uom_id = uom_litre
|
||||
line.product_qty = 3
|
||||
with bom.bom_line_ids.new() as line:
|
||||
line.product_id = crumble
|
||||
line.product_uom_id = uom_kg
|
||||
line.product_qty = 5.4
|
||||
|
||||
workcenter_2 = self.env['mrp.workcenter'].create({
|
||||
'name': 'cake mounting',
|
||||
'costs_hour': 20,
|
||||
'time_start': 10,
|
||||
'time_stop': 15
|
||||
})
|
||||
|
||||
routing_form = Form(self.env['mrp.routing'])
|
||||
routing_form.name = "Cheese cake process"
|
||||
routing_cheese = routing_form.save()
|
||||
|
||||
with Form(routing_cheese) as routing:
|
||||
with routing.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Mix cheese and crumble'
|
||||
operation.time_cycle_manual = 10
|
||||
with routing.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter_2
|
||||
operation.name = 'Cake mounting'
|
||||
operation.time_cycle_manual = 5
|
||||
|
||||
bom_cheese_cake.routing_id = routing_cheese.id
|
||||
|
||||
# TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
|
||||
self.assertEqual(report_values['lines']['operations_time'], 40.0, 'Operation time should be the same for 1 unit or for the batch')
|
||||
# Operation cost is the sum of operation line.
|
||||
operation_cost = float_round(10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20, precision_digits=2)
|
||||
self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0)
|
||||
|
||||
for component_line in report_values['lines']['components']:
|
||||
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||
if component_line['prod_id'] == cream.id:
|
||||
# 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$
|
||||
self.assertEqual(float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0)
|
||||
if component_line['prod_id'] == crumble.id:
|
||||
# 5.4 kg of crumble at the cost of a batch.
|
||||
crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total']
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user