# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import Command, fields from odoo.tests.common import Form, tagged from odoo.tools.float_utils import float_round, float_compare from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon from odoo.addons.mrp_account.tests.test_bom_price import TestBomPriceCommon @tagged('post_install', '-at_install') class TestAccountSubcontractingFlows(TestMrpSubcontractingCommon): def test_subcontracting_account_flow_1(self): # pylint: disable=bad-whitespace self.stock_location = self.env.ref('stock.stock_location_stock') self.customer_location = self.env.ref('stock.stock_location_customers') self.supplier_location = self.env.ref('stock.stock_location_suppliers') self.uom_unit = self.env.ref('uom.product_uom_unit') product_category_all = self.env.ref('product.product_category_all') product_category_all.property_cost_method = 'fifo' product_category_all.property_valuation = 'real_time' in_account = self.env['account.account'].create({ 'name': 'IN Account', 'code': '000001', 'account_type': 'asset_current', }) out_account = self.env['account.account'].create({ 'name': 'OUT Account', 'code': '000002', 'account_type': 'asset_current', }) valu_account = self.env['account.account'].create({ 'name': 'VALU Account', 'code': '000003', 'account_type': 'asset_current', }) production_cost_account = self.env['account.account'].create({ 'name': 'PROD COST Account', 'code': '000004', 'account_type': 'asset_current', }) product_category_all.property_stock_account_input_categ_id = in_account product_category_all.property_stock_account_output_categ_id = out_account product_category_all.property_stock_account_production_cost_id = production_cost_account product_category_all.property_stock_valuation_account_id = valu_account stock_in_acc_id = product_category_all.property_stock_account_input_categ_id.id stock_out_acc_id = product_category_all.property_stock_account_output_categ_id.id stock_valu_acc_id = product_category_all.property_stock_valuation_account_id.id stock_cop_acc_id = product_category_all.property_stock_account_production_cost_id.id # IN 10@10 comp1 10@20 comp2 move1 = self.env['stock.move'].create({ 'name': 'IN 10 units @ 10.00 per unit', 'location_id': self.supplier_location.id, 'location_dest_id': self.env.company.subcontracting_location_id.id, 'product_id': self.comp1.id, 'product_uom': self.uom_unit.id, 'product_uom_qty': 10.0, 'price_unit': 10.0, }) move1._action_confirm() move1._action_assign() move1.move_line_ids.quantity = 10.0 move1.picked = True move1._action_done() move2 = self.env['stock.move'].create({ 'name': 'IN 10 units @ 20.00 per unit', 'location_id': self.supplier_location.id, 'location_dest_id': self.env.company.subcontracting_location_id.id, 'product_id': self.comp2.id, 'product_uom': self.uom_unit.id, 'product_uom_qty': 10.0, 'price_unit': 20.0, }) move2._action_confirm() move2._action_assign() move2.move_line_ids.quantity = 10.0 move2.picked = True move2._action_done() all_amls_ids = self.env['account.move.line'].search([]).ids picking_form = Form(self.env['stock.picking']) picking_form.picking_type_id = self.env.ref('stock.picking_type_in') picking_form.partner_id = self.subcontractor_partner1 with picking_form.move_ids_without_package.new() as move: move.product_id = self.finished move.product_uom_qty = 1 picking_receipt = picking_form.save() picking_receipt.move_ids.price_unit = 15.0 picking_receipt.action_confirm() # Suppose the additional cost changes: picking_receipt.move_ids.price_unit = 30.0 picking_receipt.move_ids.quantity = 1.0 picking_receipt.move_ids.picked = True picking_receipt._action_done() mo = picking_receipt._get_subcontract_production() # Finished is made of 1 comp1 and 1 comp2. # Cost of comp1 = 10 # Cost of comp2 = 20 # --> Cost of finished = 10 + 20 = 30 # Additionnal cost = 30 (from the purchase order line or directly set on the stock move here) # Total cost of subcontracting 1 unit of finished = 30 + 30 = 60 self.assertEqual(mo.move_finished_ids.stock_valuation_layer_ids.value, 60) self.assertEqual(picking_receipt.move_ids.stock_valuation_layer_ids.value, 0) self.assertEqual(picking_receipt.move_ids.product_id.value_svl, 60) amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)]) all_amls_ids += amls.ids self.assertRecordValues(amls, [ # Receipt from subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.finished.id, 'debit': 60.0, 'credit': 0.0}, {'account_id': stock_in_acc_id, 'product_id': self.finished.id, 'debit': 0.0, 'credit': 30.0}, {'account_id': stock_cop_acc_id, 'product_id': self.finished.id, 'debit': 0.0, 'credit': 30.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp2.id, 'debit': 0.0, 'credit': 20.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp2.id, 'debit': 20.0, 'credit': 0.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp1.id, 'debit': 0.0, 'credit': 10.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp1.id, 'debit': 10.0, 'credit': 0.0}, ]) # Do the same without any additionnal cost picking_form = Form(self.env['stock.picking']) picking_form.picking_type_id = self.env.ref('stock.picking_type_in') picking_form.partner_id = self.subcontractor_partner1 with picking_form.move_ids_without_package.new() as move: move.product_id = self.finished move.product_uom_qty = 1 picking_receipt = picking_form.save() picking_receipt.move_ids.price_unit = 0 picking_receipt.action_confirm() picking_receipt.move_ids.quantity = 1.0 picking_receipt.move_ids.picked = True picking_receipt._action_done() mo = picking_receipt._get_subcontract_production() # In this case, since there isn't any additionnal cost, the total cost of the subcontracting # is the sum of the components' costs: 10 + 20 = 30 self.assertEqual(mo.move_finished_ids.stock_valuation_layer_ids.value, 30) self.assertEqual(picking_receipt.move_ids.product_id.value_svl, 90) amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)]) self.assertRecordValues(amls, [ # Receipt from subcontractor {'account_id': stock_cop_acc_id, 'product_id': self.finished.id, 'debit': 0.0, 'credit': 30.0}, {'account_id': stock_valu_acc_id, 'product_id': self.finished.id, 'debit': 30.0, 'credit': 0.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp2.id, 'debit': 0.0, 'credit': 20.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp2.id, 'debit': 20.0, 'credit': 0.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp1.id, 'debit': 0.0, 'credit': 10.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp1.id, 'debit': 10.0, 'credit': 0.0}, ]) def test_subcontracting_account_flow_2(self): """Test when set Cost of Production account on production location, subcontracting won't use it. """ # pylint: disable=bad-whitespace self.stock_location = self.env.ref('stock.stock_location_stock') self.customer_location = self.env.ref('stock.stock_location_customers') self.supplier_location = self.env.ref('stock.stock_location_suppliers') self.uom_unit = self.env.ref('uom.product_uom_unit') product_category_all = self.env.ref('product.product_category_all') product_category_all.property_cost_method = 'fifo' product_category_all.property_valuation = 'real_time' in_account = self.env['account.account'].create({ 'name': 'IN Account', 'code': '000001', 'account_type': 'asset_current', }) out_account = self.env['account.account'].create({ 'name': 'OUT Account', 'code': '000002', 'account_type': 'asset_current', }) valu_account = self.env['account.account'].create({ 'name': 'VALU Account', 'code': '000003', 'account_type': 'asset_current', }) production_cost_account = self.env['account.account'].create({ 'name': 'PROD COST Account', 'code': '000004', 'account_type': 'asset_current', }) product_category_all.property_stock_account_input_categ_id = in_account product_category_all.property_stock_account_output_categ_id = out_account product_category_all.property_stock_account_production_cost_id = production_cost_account product_category_all.property_stock_valuation_account_id = valu_account stock_in_acc_id = product_category_all.property_stock_account_input_categ_id.id stock_valu_acc_id = product_category_all.property_stock_valuation_account_id.id stock_cop_acc_id = product_category_all.property_stock_account_production_cost_id.id # set Cost of Production account on production location cop_account = self.env['account.account'].create({ 'name': 'Cost of Production', 'code': 'CoP', "account_type": 'expense', 'reconcile': False, }) self.comp1.property_stock_production.write({ 'valuation_out_account_id': cop_account.id, 'valuation_in_account_id': cop_account.id, }) # IN 10@10 comp1 10@20 comp2 move1 = self.env['stock.move'].create({ 'name': 'IN 10 units @ 10.00 per unit', 'location_id': self.supplier_location.id, 'location_dest_id': self.env.company.subcontracting_location_id.id, 'product_id': self.comp1.id, 'product_uom': self.uom_unit.id, 'product_uom_qty': 10.0, 'price_unit': 10.0, }) move1._action_confirm() move1._action_assign() move1.move_line_ids.quantity = 10.0 move1.picked = True move1._action_done() move2 = self.env['stock.move'].create({ 'name': 'IN 10 units @ 20.00 per unit', 'location_id': self.supplier_location.id, 'location_dest_id': self.env.company.subcontracting_location_id.id, 'product_id': self.comp2.id, 'product_uom': self.uom_unit.id, 'product_uom_qty': 10.0, 'price_unit': 20.0, }) move2._action_confirm() move2._action_assign() move2.move_line_ids.quantity = 10.0 move2.picked = True move2._action_done() all_amls_ids = self.env['account.move.line'].search([]).ids picking_form = Form(self.env['stock.picking']) picking_form.picking_type_id = self.env.ref('stock.picking_type_in') picking_form.partner_id = self.subcontractor_partner1 with picking_form.move_ids_without_package.new() as move: move.product_id = self.finished move.product_uom_qty = 1 picking_receipt = picking_form.save() picking_receipt.move_ids.price_unit = 30.0 picking_receipt.action_confirm() picking_receipt.move_ids.quantity = 1.0 picking_receipt.move_ids.picked = True picking_receipt._action_done() amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)]) all_amls_ids += amls.ids self.assertRecordValues(amls, [ # Receipt from subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.finished.id, 'debit': 60.0, 'credit': 0.0}, {'account_id': stock_in_acc_id, 'product_id': self.finished.id, 'debit': 0.0, 'credit': 30.0}, {'account_id': stock_cop_acc_id, 'product_id': self.finished.id, 'debit': 0.0, 'credit': 30.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp2.id, 'debit': 0.0, 'credit': 20.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp2.id, 'debit': 20.0, 'credit': 0.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp1.id, 'debit': 0.0, 'credit': 10.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp1.id, 'debit': 10.0, 'credit': 0.0}, ]) def test_subcontracting_account_backorder(self): """ This test uses tracked (serial and lot) component and tracked (serial) finished product The original subcontracting production order will be split into 4 backorders. This test ensure the extra cost asked from the subcontractor is added correctly on all the finished product valuation layer. Not only the first one. """ todo_nb = 4 self.comp2.tracking = 'lot' self.comp1.tracking = 'serial' self.comp2.standard_price = 100 self.finished.tracking = 'serial' self.env.ref('product.product_category_all').property_cost_method = 'fifo' # Create a receipt picking from the subcontractor picking_form = Form(self.env['stock.picking']) picking_form.picking_type_id = self.env.ref('stock.picking_type_in') picking_form.partner_id = self.subcontractor_partner1 with picking_form.move_ids_without_package.new() as move: move.product_id = self.finished move.quantity = todo_nb picking_receipt = picking_form.save() # Mimic the extra cost on the po line picking_receipt.move_ids.price_unit = 50 picking_receipt.action_confirm() picking_receipt.do_unreserve() # We should be able to call the 'record_components' button self.assertTrue(picking_receipt.display_action_record_components) lot_comp2 = self.env['stock.lot'].create({ 'name': 'lot_comp2', 'product_id': self.comp2.id, 'company_id': self.env.company.id, }) serials_finished = [] serials_comp1 = [] for i in range(todo_nb): serials_finished.append(self.env['stock.lot'].create({ 'name': 'serial_fin_%s' % i, 'product_id': self.finished.id, 'company_id': self.env.company.id, })) serials_comp1.append(self.env['stock.lot'].create({ 'name': 'serials_comp1_%s' % i, 'product_id': self.comp1.id, 'company_id': self.env.company.id, })) for i in range(todo_nb): action = picking_receipt.action_record_components() mo = self.env['mrp.production'].browse(action['res_id']) mo_form = Form(mo.with_context(**action['context']), view=action['view_id']) mo_form.lot_producing_id = serials_finished[i] with mo_form.move_line_raw_ids.edit(0) as ml: self.assertEqual(ml.product_id, self.comp1) ml.lot_id = serials_comp1[i] with mo_form.move_line_raw_ids.edit(1) as ml: self.assertEqual(ml.product_id, self.comp2) ml.lot_id = lot_comp2 mo = mo_form.save() mo.subcontracting_record_component() # We should not be able to call the 'record_components' button picking_receipt.move_ids.picked = True picking_receipt.button_validate() f_layers = self.finished.stock_valuation_layer_ids self.assertEqual(len(f_layers), 4) for layer in f_layers: self.assertEqual(layer.value, 100 + 50) def test_tracked_compo_and_backorder(self): """ Suppose a subcontracted product P with two tracked components, P is FIFO Create a receipt for 10 x P, receive 5, then 3 and then 2 """ self.env.ref('product.product_category_all').property_cost_method = 'fifo' self.comp1.tracking = 'lot' self.comp1.standard_price = 10 self.comp2.tracking = 'lot' self.comp2.standard_price = 20 lot01, lot02 = self.env['stock.lot'].create([{ 'name': "Lot of %s" % product.name, 'product_id': product.id, 'company_id': self.env.company.id, } for product in (self.comp1, self.comp2)]) receipt_form = Form(self.env['stock.picking']) receipt_form.picking_type_id = self.env.ref('stock.picking_type_in') receipt_form.partner_id = self.subcontractor_partner1 with receipt_form.move_ids_without_package.new() as move: move.product_id = self.finished move.product_uom_qty = 10 receipt = receipt_form.save() # add an extra cost receipt.move_ids.price_unit = 50 receipt.action_confirm() for qty_producing in (5, 3, 2): action = receipt.action_record_components() mo = self.env['mrp.production'].browse(action['res_id']) mo_form = Form(mo.with_context(**action['context']), view=action['view_id']) mo_form.qty_producing = qty_producing with mo_form.move_line_raw_ids.edit(0) as ml: ml.lot_id = lot01 with mo_form.move_line_raw_ids.edit(1) as ml: ml.lot_id = lot02 mo = mo_form.save() mo.subcontracting_record_component() action = receipt.button_validate() if isinstance(action, dict): wizard = Form(self.env[action['res_model']].with_context(action['context'])).save() wizard.process() receipt = receipt.backorder_ids self.assertRecordValues(self.finished.stock_valuation_layer_ids, [ {'quantity': 5, 'value': 5 * (10 + 20 + 50)}, {'quantity': 3, 'value': 3 * (10 + 20 + 50)}, {'quantity': 2, 'value': 2 * (10 + 20 + 50)}, ]) def test_subcontract_cost_different_when_standard_price(self): """Test when subcontracting with standard price when Final product cost != Components cost + Subcontracting cost When posting the account entries for receiving final product, the subcontracting cost will be adjusted based on the difference of the cost. """ # pylint: disable=bad-whitespace self.stock_location = self.env.ref('stock.stock_location_stock') self.customer_location = self.env.ref('stock.stock_location_customers') self.supplier_location = self.env.ref('stock.stock_location_suppliers') self.uom_unit = self.env.ref('uom.product_uom_unit') product_category_all = self.env.ref('product.product_category_all') product_category_all.property_cost_method = 'standard' product_category_all.property_valuation = 'real_time' in_account = self.env['account.account'].create({ 'name': 'IN Account', 'code': '000001', 'account_type': 'asset_current', }) out_account = self.env['account.account'].create({ 'name': 'OUT Account', 'code': '000002', 'account_type': 'asset_current', }) valu_account = self.env['account.account'].create({ 'name': 'VALU Account', 'code': '000003', 'account_type': 'asset_current', }) production_cost_account = self.env['account.account'].create({ 'name': 'PROD COST Account', 'code': '000004', 'account_type': 'asset_current', }) product_category_all.property_stock_account_input_categ_id = in_account product_category_all.property_stock_account_output_categ_id = out_account product_category_all.property_stock_account_production_cost_id = production_cost_account product_category_all.property_stock_valuation_account_id = valu_account stock_in_acc_id = product_category_all.property_stock_account_input_categ_id.id stock_valu_acc_id = product_category_all.property_stock_valuation_account_id.id stock_cop_acc_id = product_category_all.property_stock_account_production_cost_id.id self.comp1.standard_price = 10 self.comp2.standard_price = 20 self.finished.standard_price = 40 all_amls_ids = self.env['account.move.line'].search([]).ids picking_form = Form(self.env['stock.picking']) picking_form.picking_type_id = self.env.ref('stock.picking_type_in') picking_form.partner_id = self.subcontractor_partner1 with picking_form.move_ids_without_package.new() as move: move.product_id = self.finished move.product_uom_qty = 1 picking_receipt = picking_form.save() # subcontracting cost is 15 picking_receipt.move_ids.price_unit = 15.0 picking_receipt.action_confirm() picking_receipt.move_ids.quantity = 1.0 picking_receipt.move_ids.picked = True picking_receipt._action_done() amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)]) self.assertRecordValues(amls, [ # Receipt from subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.finished.id, 'debit': 40.0, 'credit': 0.0}, {'account_id': stock_in_acc_id, 'product_id': self.finished.id, 'debit': 0.0, 'credit': 10.0}, # adjust according to the difference {'account_id': stock_cop_acc_id, 'product_id': self.finished.id, 'debit': 0.0, 'credit': 30.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp2.id, 'debit': 0.0, 'credit': 20.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp2.id, 'debit': 20.0, 'credit': 0.0}, # Delivery com2 to subcontractor {'account_id': stock_valu_acc_id, 'product_id': self.comp1.id, 'debit': 0.0, 'credit': 10.0}, {'account_id': stock_cop_acc_id, 'product_id': self.comp1.id, 'debit': 10.0, 'credit': 0.0}, ]) class TestBomPriceSubcontracting(TestBomPriceCommon): def test_01_compute_price_subcontracting_cost(self): """Test calculation of bom cost with subcontracting.""" self.table_head.uom_po_id = self.dozen partner = self.env['res.partner'].create({ 'name': 'A name can be a Many2one...' }) (self.bom_1 | self.bom_2).write({ 'type': 'subcontract', 'subcontractor_ids': [Command.link(partner.id)] }) suppliers = self.env['product.supplierinfo'].create([ { 'partner_id': partner.id, 'product_tmpl_id': self.dining_table.product_tmpl_id.id, 'price': 150.0, }, { 'partner_id': partner.id, 'product_tmpl_id': self.table_head.product_tmpl_id.id, 'price': 120.0, # 10 by Unit because uom_po_id is in dozen } ]) self.assertEqual(suppliers.mapped('is_subcontractor'), [True, True]) # ----------------------------------------------------------------- # Cost of BoM (Dining Table 1 Unit) # ----------------------------------------------------------------- # Component Cost = Table Head 1 Unit * 300 = 300 (478.75 from it's components) # Screw 5 Unit * 10 = 50 # Leg 4 Unit * 25 = 100 # Glass 1 Unit * 100 = 100 # Subcontracting 1 Unit * 150 = 150 # Total = 700 [878.75 if components of Table Head considered] (for 1 Unit) # ----------------------------------------------------------------- self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000") self.dining_table.button_bom_cost() self.assertEqual(float_round(self.dining_table.standard_price, precision_digits=2), 700.0, "After computing price from BoM price should be 700") # Cost of BoM (Table Head 1 Dozen) # ----------------------------------------------------------------- # Component Cost = Plywood Sheet 12 Unit * 200 = 2400 # Bolt 60 Unit * 10 = 600 # Colour 12 Unit * 100 = 1200 # Corner Slide 57 Unit * 25 = 1425 # Subcontracting 1 Dozen * 120 = 120 # Total = 5745 # 1 Unit price (5745/12) = 478.75 # ----------------------------------------------------------------- self.assertEqual(self.table_head.standard_price, 300, "Initial price of the Product should be 300") self.Product.browse([self.dining_table.id, self.table_head.id]).action_bom_cost() self.assertEqual(float_compare(self.table_head.standard_price, 478.75, precision_digits=2), 0, "After computing price from BoM price should be 878.75") self.assertEqual(float_compare(self.dining_table.standard_price, 878.75, precision_digits=2), 0, "After computing price from BoM price should be 878.75") def test_02_compute_price_subcontracting_cost(self): """Test calculation of bom cost with subcontracting and supplier in different currency.""" currency_a = self.env['res.currency'].create({ 'name': 'ZEN', 'symbol': 'Z', 'rounding': 0.01, 'currency_unit_label': 'Zenny', 'rate_ids': [(0, 0, { 'name': fields.Date.today(), 'company_rate': 0.5, })], }) partner = self.env['res.partner'].create({ 'name': 'supplier', }) product = self.env['product.product'].create({ 'name': 'product', 'type': 'product', 'standard_price': 100, 'company_id': self.env.company.id, }) supplier = self.env['product.supplierinfo'].create([{ 'partner_id': partner.id, 'product_tmpl_id': product.product_tmpl_id.id, 'price': 120.0, 'currency_id': currency_a.id, }]) self.env['mrp.bom'].create({ 'product_tmpl_id': product.product_tmpl_id.id, 'product_qty': 1, 'type': 'subcontract', 'subcontractor_ids': [Command.link(partner.id)], 'bom_line_ids': [ (0, 0, {'product_id': self.table_head.id, 'product_qty': 1}), ], }) self.table_head.standard_price = 100 self.assertEqual(supplier.is_subcontractor, True) self.assertEqual(product.standard_price, 100, "Initial price of the Product should be 100") product.button_bom_cost() # 120 Zen = 240 USD (120 * 2) # price = 240 + 100 (1 unit of component "table_head") = 340 self.assertEqual(product.standard_price, 340, "After computing price from BoM price should be 340")