# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime, timedelta from odoo.addons.mrp_account.tests.test_mrp_account import TestMrpAccount from odoo.tests.common import Form from odoo import Command from freezegun import freeze_time class TestReportsCommon(TestMrpAccount): def test_mrp_cost_structure(self): """ Check that values of mrp_cost_structure are correctly calculated even when: 1. byproducts with a cost share. 2. multi-company + multi-currency environment. """ # create MO with component cost + operations cost self.product_table_sheet.standard_price = 20.0 self.product_table_leg.standard_price = 5.0 self.product_bolt.standard_price = 1.0 self.product_screw.standard_price = 2.0 self.product_table_leg.tracking = 'none' self.product_table_sheet.tracking = 'none' self.mrp_workcenter.costs_hour = 50.0 bom = self.mrp_bom_desk.copy() production_table_form = Form(self.env['mrp.production']) production_table_form.product_id = self.dining_table production_table_form.bom_id = bom production_table_form.product_qty = 1 production_table = production_table_form.save() # add a byproduct w/ a non-zero cost share byproduct_cost_share = 10 byproduct = self.env['product.product'].create({ 'name': 'Plank', 'type': 'product', }) self.env['stock.move'].create({ 'name': "Byproduct", 'product_id': byproduct.id, 'product_uom': byproduct.uom_id.id, 'product_uom_qty': 1, 'production_id': production_table.id, 'location_id': self.ref('stock.stock_location_stock'), 'location_dest_id': self.ref('stock.stock_location_output'), 'cost_share': byproduct_cost_share }) production_table.action_confirm() mo_form = Form(production_table) mo_form.qty_producing = 1 production_table = mo_form.save() # add operation duration otherwise 0 operation cost self.env['mrp.workcenter.productivity'].create({ 'workcenter_id': self.mrp_workcenter.id, 'date_start': datetime.now() - timedelta(minutes=30), 'date_end': datetime.now(), 'loss_id': self.env.ref('mrp.block_reason7').id, 'description': self.env.ref('mrp.block_reason7').name, 'workorder_id': production_table.workorder_ids[0].id }) # avoid qty done not being updated when enterprise mrp_workorder is installed for move in production_table.move_raw_ids: move.quantity = move.product_uom_qty move.picked = True production_table._post_inventory() production_table.button_mark_done() total_component_cost = sum(move.product_id.standard_price * move.quantity for move in production_table.move_raw_ids) total_operation_cost = sum(wo.costs_hour * sum(wo.time_ids.mapped('duration')) / 60.0 for wo in production_table.workorder_ids) report = self.env['report.mrp_account_enterprise.mrp_cost_structure'] self.env.flush_all() # flush to avoid the wo duration not being available in the db in order to correctly build report report_values = report._get_report_values(docids=production_table.id)['lines'][0] self.assertEqual(report_values['component_cost_by_product'][self.dining_table], total_component_cost * (100 - byproduct_cost_share) / 100) self.assertEqual(report_values['operation_cost_by_product'][self.dining_table], total_operation_cost * (100 - byproduct_cost_share) / 100) self.assertEqual(report_values['component_cost_by_product'][byproduct], total_component_cost * byproduct_cost_share / 100) self.assertEqual(report_values['operation_cost_by_product'][byproduct], total_operation_cost * byproduct_cost_share / 100) # create another company w/ different currency + rate exchange_rate = 4 currency_p = self.env['res.currency'].create({ 'name': 'DBL', 'symbol': 'DD', 'rounding': 0.01, 'currency_unit_label': 'Doubloon' }) company_p = self.env['res.company'].create({'name': 'Pirates R Us', 'currency_id': currency_p.id}) self.env['res.currency.rate'].create({ 'name': '2010-01-01', 'rate': exchange_rate, 'currency_id': self.env.company.currency_id.id, 'company_id': company_p.id, }) user_p = self.env['res.users'].create({ 'name': 'pirate', 'login': 'pirate', 'groups_id': [(6, 0, [self.env.ref('base.group_user').id, self.env.ref('mrp.group_mrp_manager').id])], 'company_id': company_p.id, 'company_ids': [(6, 0, [company_p.id, self.env.company.id])] }) report_values = report.with_user(user_p)._get_report_values(docids=production_table.id)['lines'][0] self.assertEqual(report_values['component_cost_by_product'][self.dining_table], total_component_cost * (100 - byproduct_cost_share) / 100 / exchange_rate) self.assertEqual(report_values['operation_cost_by_product'][self.dining_table], total_operation_cost * (100 - byproduct_cost_share) / 100 / exchange_rate) self.assertEqual(report_values['component_cost_by_product'][byproduct], total_component_cost * byproduct_cost_share / 100 / exchange_rate) self.assertEqual(report_values['operation_cost_by_product'][byproduct], total_operation_cost * byproduct_cost_share / 100 / exchange_rate) @freeze_time('2022-05-28') def test_mrp_avg_cost_calculation(self): """ Check that the average cost is calculated based on the quantity produced in each MO - Final product Bom structure: - product_4: qty: 2, cost: $20 - product_3: qty: 3, cost: $50 - Work center > costs_hour = $80 1:/ MO1: - qty to produce: 10 units - work_order duration: 300 unit_component_cost = ((20 * 2) + (50 * 3)) = 190 unit_duration = 300 / 10 = 30 unit_operation_cost = (80 / 60) * 30'unit_duration' = 40 unit_cost = 190 + 40 = 250 2:/ MO2: - update product_3 cost to: $30 - qty to produce: 20 units - work order duration: 600 unit_component_cost = ((20 * 2) + (30 * 3)) = $130 unit_duration = 600 / 20 = 30 unit_operation_cost = (80 / 60) * 30'unit_duration' = 40 unit_cost = 130 + 40 = 170 total_qty_produced = 30 avg_unit_component_cost = ((190 * 10) + (130 * 20)) / 30 = $150 avg_unit_operation_cost = ((40*20) + (40*10)) / 30 = $40 avg_unit_duration = (600 + 300) / 30 = 30 avg_unit_cost = avg_unit_component_cost + avg_unit_operation_cost = $190 """ # Make some stock and reserve for product in self.bom_2.bom_line_ids.product_id: self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': product.id, 'inventory_quantity': 1000, 'location_id': self.stock_location_components.id, })._apply_inventory() # Change product_4 UOM to unit self.bom_2.bom_line_ids[0].product_uom_id = self.ref('uom.product_uom_unit') # Update the work center cost self.bom_2.operation_ids.workcenter_id.costs_hour = 80 # MO_1 self.product_4.standard_price = 20 self.product_3.standard_price = 50 production_form = Form(self.env['mrp.production']) production_form.bom_id = self.bom_2 production_form.product_qty = 10 mo_1 = production_form.save() mo_1.action_confirm() mo_1.button_plan() wo = mo_1.workorder_ids wo.button_start() wo.duration = 300 wo.qty_producing = 10 mo_1.button_mark_done() # MO_2 self.product_3.standard_price = 30 production_form = Form(self.env['mrp.production']) production_form.bom_id = self.bom_2 production_form.product_qty = 20 mo_2 = production_form.save() mo_2.action_confirm() mo_2.button_plan() wo = mo_2.workorder_ids wo.button_start() wo.duration = 600 wo.qty_producing = 20 mo_2.button_mark_done() # must flush else SQL request in report is not accurate self.env.flush_all() report = self.env['mrp.report']._read_group( [('product_id', '=', self.bom_2.product_id.id)], aggregates=['unit_cost:avg', 'unit_component_cost:avg', 'unit_operation_cost:avg', 'unit_duration:avg'], )[0] unit_cost, unit_component_cost, unit_operation_cost, unit_duration = report self.assertEqual(unit_cost, 190) self.assertEqual(unit_component_cost, 150) self.assertEqual(unit_operation_cost, 40) self.assertEqual(unit_duration, 30) def test_multiple_users_operation(self): """ Check what happens on the report when two users log on the same operation simultaneously. """ self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings') user_1 = self.env['res.users'].create({ 'name': 'Lonie', 'login': 'lonie', 'email': 'lonie@user.com', 'groups_id': [Command.set([self.env.ref('mrp.group_mrp_user').id])], }) user_2 = self.env['res.users'].create({ 'name': 'Doppleganger', 'login': 'dopple', 'email': 'dopple@user.com', 'groups_id': [Command.set([self.env.ref('mrp.group_mrp_user').id])], }) production_form = Form(self.env['mrp.production']) production_form.product_id = self.product_4 production = production_form.save() with Form(production) as mo_form: with mo_form.workorder_ids.new() as wo: wo.name = 'Do important stuff' wo.workcenter_id = self.workcenter_2 production.action_confirm() # Have both users working simultaneously on the same operation self.env['mrp.workcenter.productivity'].create({ 'workcenter_id': self.workcenter_2.id, 'date_start': datetime.now() - timedelta(minutes=30), 'date_end': datetime.now(), 'loss_id': self.env.ref('mrp.block_reason7').id, 'workorder_id': production.workorder_ids[0].id, 'user_id': user_1.id, }) self.env['mrp.workcenter.productivity'].create({ 'workcenter_id': self.workcenter_2.id, 'date_start': datetime.now() - timedelta(minutes=20), 'date_end': datetime.now() - timedelta(minutes=5), 'loss_id': self.env.ref('mrp.block_reason7').id, 'workorder_id': production.workorder_ids[0].id, 'user_id': user_2.id, }) production.button_mark_done() # Need to flush to have the duration correctly set on the workorders for the report. self.env.flush_all() cost_analysis = self.env['report.mrp_account_enterprise.mrp_cost_structure'].get_lines(production)[0] workcenter_times = list(filter(lambda op: op[0] == self.workcenter_2.name and op[2] == production.workorder_ids[0].name, cost_analysis['operations'])) self.assertEqual(len(workcenter_times), 1, "There should be only a single line for the workcenter cost") self.assertEqual(workcenter_times[0][3], production.workorder_ids[0].duration / 60, "Duration should be the total duration of this operation.") @freeze_time('2022-05-28') def test_cost_analysis_mismatch_in_produced_and_planned_quantity(self): ''' verify that the Cost Analysis report correctly reflects the actual quantity produced and cost per unit when it differs from the planned quantity. ''' # enable by-product self.env.user.groups_id += self.env.ref('mrp.group_mrp_byproducts') self.product_3.standard_price = 10 self.product_4.standard_price = 10 bom_1 = self.env['mrp.bom'].create({ 'product_id': self.product_5.id, 'product_tmpl_id': self.product_5.product_tmpl_id.id, 'product_uom_id': self.uom_unit.id, 'product_qty': 1.0, 'consumption': 'flexible', 'operation_ids': [ ], 'type': 'normal', 'bom_line_ids': [ (0, 0, {'product_id': self.product_3.id, 'product_qty': 1}), (0, 0, {'product_id': self.product_4.id, 'product_qty': 1}) ], 'byproduct_ids': [ Command.create({ 'product_id': self.product_2.id, 'product_uom_id': self.product_2.uom_id.id, 'product_qty': 1, }), ], }) # Make some stock and reserve for product in bom_1.bom_line_ids.product_id: self.env['stock.quant'].with_context(inventory_mode=True).create({ 'product_id': product.id, 'inventory_quantity': 1000, 'location_id': self.stock_location_components.id, })._apply_inventory() # first MO set qty as 1 produce 5 production_form = Form(self.env['mrp.production']) production_form.bom_id = bom_1 production_form.product_qty = 1 mo = production_form.save() mo.action_confirm() mo_form = Form(mo) mo_form.qty_producing = 5 # 4 dozen of byproduct with mo_form.move_byproduct_ids.edit(0) as line: line.product_uom = self.env.ref('uom.product_uom_dozen') line.quantity = 4 line.cost_share = 48 mo_done = mo_form.save() mo_done.button_mark_done() self.env.flush_all() cost_analysis = self.env['report.mrp_account_enterprise.mrp_cost_structure'].get_lines(mo_done) self.assertEqual(cost_analysis[0]['mo_qty'], 5) self.assertEqual(cost_analysis[0]['total_cost'], 100) # 4 * dozen = 48 units of by product self.assertEqual(cost_analysis[0]['qty_by_byproduct'][self.product_2], 48) self.assertEqual(cost_analysis[0]['total_cost_by_product'][self.product_2], 48) # first MO set qty as 5 produce 1 without a backorder production_form = Form(self.env['mrp.production']) production_form.bom_id = bom_1 production_form.product_qty = 5 no_backorder_mo = production_form.save() no_backorder_mo.action_confirm() no_backorder_mo_form = Form(no_backorder_mo) no_backorder_mo_form.qty_producing = 1 no_backorder_mo_done = no_backorder_mo_form.save() action = no_backorder_mo_done.button_mark_done() backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) backorder.save().action_close_mo() self.env.flush_all() cost_analysis = self.env['report.mrp_account_enterprise.mrp_cost_structure'].get_lines(no_backorder_mo_done) self.assertEqual(cost_analysis[0]['mo_qty'], 1) self.assertEqual(cost_analysis[0]['total_cost'], 20)