diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py index 0bb9a9bf327..6467e3450df 100644 --- a/addons/mrp/tests/__init__.py +++ b/addons/mrp/tests/__init__.py @@ -15,5 +15,5 @@ from . import test_multicompany from . import test_backorder from . import test_smp from . import test_performance -from . import test_consume_tracked_component +from . import test_consume_component from . import test_manual_consumption diff --git a/addons/mrp/tests/common_consume_tracked_component.py b/addons/mrp/tests/common_consume_tracked_component.py deleted file mode 100644 index e3c11fdb1af..00000000000 --- a/addons/mrp/tests/common_consume_tracked_component.py +++ /dev/null @@ -1,233 +0,0 @@ -import copy - -from odoo.exceptions import UserError -from odoo.tests import common - -class TestConsumeTrackedComponentCommon(common.TransactionCase): - - @classmethod - def setUpClass(cls): - """ - The following variables are used in each test to define the number of MO to generate. - They're also used as a verification in the executeConsumptionTriggers() to see if enough MO were passed to it - in order to test all the triggers. - - SERIAL : MO's product_tracking is 'serial' - DEFAULT : MO's product_tracking is 'none' or 'lot' - AVAILABLE : MO'S raw components are fully available - """ - super().setUpClass() - - cls.SERIAL_AVAILABLE_TRIGGERS_COUNT = 3 - cls.DEFAULT_AVAILABLE_TRIGGERS_COUNT = 2 - cls.SERIAL_TRIGGERS_COUNT = 2 - cls.DEFAULT_TRIGGERS_COUNT = 1 - - cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture') - cls.stock_id = cls.env.ref('stock.stock_location_stock').id - - cls.picking_type = cls.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0] - cls.picking_type.use_create_components_lots = True - cls.picking_type.use_auto_consume_components_lots = True - - #Create Products & Components - cls.produced_lot = cls.env['product.product'].create({ - 'name': 'Produced Lot', - 'type': 'product', - 'categ_id': cls.env.ref('product.product_category_all').id, - 'tracking' : 'lot', - 'route_ids': [(4, cls.manufacture_route.id, 0)], - }) - cls.produced_serial = cls.env['product.product'].create({ - 'name': 'Produced Serial', - 'type': 'product', - 'categ_id': cls.env.ref('product.product_category_all').id, - 'tracking' : 'serial', - 'route_ids': [(4, cls.manufacture_route.id, 0)], - }) - cls.produced_none = cls.env['product.product'].create({ - 'name': 'Produced None', - 'type': 'product', - 'categ_id': cls.env.ref('product.product_category_all').id, - 'tracking' : 'none', - 'route_ids': [(4, cls.manufacture_route.id, 0)], - }) - - cls.raw_lot = cls.env['product.product'].create({ - 'name': 'Raw Lot', - 'type': 'product', - 'categ_id': cls.env.ref('product.product_category_all').id, - 'tracking' : 'lot', - }) - cls.raw_serial = cls.env['product.product'].create({ - 'name': 'Raw Serial', - 'type': 'product', - 'categ_id': cls.env.ref('product.product_category_all').id, - 'tracking' : 'serial', - }) - cls.raw_none = cls.env['product.product'].create({ - 'name': 'Raw None', - 'type': 'product', - 'categ_id': cls.env.ref('product.product_category_all').id, - 'tracking' : 'none', - }) - - cls.raws = [cls.raw_none, cls.raw_lot, cls.raw_serial] - - #Workcenter - cls.workcenter = cls.env['mrp.workcenter'].create({ - 'name' : 'Assembly Line', - }) - - #BoMs - cls.bom_none = cls.env['mrp.bom'].create({ - 'product_tmpl_id' : cls.produced_none.product_tmpl_id.id, - 'product_uom_id' : cls.produced_none.uom_id.id, - 'consumption' : 'flexible', - 'sequence' : 1 - }) - - cls.bom_none_lines = cls.create_bom_lines(cls.bom_none, cls.raws, [3, 2, 1]) - - cls.bom_lot = cls.env['mrp.bom'].create({ - 'product_tmpl_id' : cls.produced_lot.product_tmpl_id.id, - 'product_uom_id' : cls.produced_lot.uom_id.id, - 'consumption' : 'flexible', - 'sequence' : 2 - }) - - cls.bom_lot_lines = cls.create_bom_lines(cls.bom_lot, cls.raws, [3, 2, 1]) - - cls.bom_serial = cls.env['mrp.bom'].create({ - 'product_tmpl_id' : cls.produced_serial.product_tmpl_id.id, - 'product_uom_id' : cls.produced_serial.uom_id.id, - 'consumption' : 'flexible', - 'sequence' : 1 - }) - - cls.bom_serial_lines = cls.create_bom_lines(cls.bom_serial, cls.raws, [3, 2, 1]) - - #Manufacturing Orders - cls.mo_none_tmpl = { - 'product_id' : cls.produced_none.id, - 'product_uom_id' : cls.produced_none.uom_id.id, - 'product_qty' : 1, - 'bom_id' : cls.bom_none.id - } - - cls.mo_lot_tmpl = { - 'product_id' : cls.produced_lot.id, - 'product_uom_id' : cls.produced_lot.uom_id.id, - 'product_qty' : 1, - 'bom_id' : cls.bom_lot.id - } - - cls.mo_serial_tmpl = { - 'product_id' : cls.produced_serial.id, - 'product_uom_id' : cls.produced_serial.uom_id.id, - 'product_qty' : 1, - 'bom_id' : cls.bom_serial.id - } - - @classmethod - def create_quant(cls, product, qty, offset=0, name="L"): - i = 1 - if product.tracking == 'serial': - i, qty = qty, 1 - if name == "L": - name = "S" - - vals = [] - for x in range(1, i+1): - qDict = { - 'location_id': cls.stock_id, - 'product_id': product.id, - 'inventory_quantity': qty, - } - - if product.tracking != 'none': - qDict['lot_id'] = cls.env['stock.lot'].create({ - 'name': name + str(offset + x), - 'product_id': product.id, - 'company_id': cls.env.company.id - }).id - vals.append(qDict) - - return cls.env['stock.quant'].create(vals) - - @classmethod - def create_bom_lines(cls, bom, products, quantities=None): - if quantities is None: - quantities = [1 for i in range(len(products))] - - vals = [] - for product, seq in zip(products, range(len(products))): - vals.append({ - 'product_id' : product.id, - 'product_qty' : quantities[seq], - 'product_uom_id' : product.uom_id.id, - 'sequence' : seq, - 'bom_id' : bom.id, - }) - - return cls.env['mrp.bom.line'].create(vals) - - @classmethod - def create_mo(cls, template, count): - vals = [] - for _ in range(count): - vals.append(copy.deepcopy(template)) - return cls.env['mrp.production'].create(vals) - - def executeConsumptionTriggers(self, mrp_productions): - """ - There's 3 different triggers to test : _onchange_producing(), action_generate_serial(), button_mark_done(). - - Depending on the tracking of the final product and the availability of the components, - only a part of these 3 triggers is available or intended to work. - - This function automatically call and process the appropriate triggers. - """ - tracking = mrp_productions[0].product_tracking - sameTracking = True - for mo in mrp_productions: - sameTracking = sameTracking and mo.product_tracking == tracking - self.assertTrue(sameTracking, "MOs passed to the executeConsumptionTriggers method shall have the same product_tracking") - - isSerial = tracking == 'serial' - isAvailable = all(move.state == 'assigned' for move in mrp_productions.move_raw_ids) - isComponentTracking = any(move.has_tracking != 'none' for move in mrp_productions.move_raw_ids) - - countOk = True - length = len(mrp_productions) - if isSerial: - if isAvailable: - countOk = length == self.SERIAL_AVAILABLE_TRIGGERS_COUNT - else: - countOk = length == self.SERIAL_TRIGGERS_COUNT - else: - if isAvailable: - countOk = length == self.DEFAULT_AVAILABLE_TRIGGERS_COUNT - else: - countOk = length == self.DEFAULT_TRIGGERS_COUNT - self.assertTrue(countOk, "The number of MOs passed to the executeConsumptionTriggers method does not match the associated TRIGGERS_COUNT") - - mrp_productions[0].qty_producing = mrp_productions[0].product_qty - mrp_productions[0]._onchange_producing() - - i = 1 - if isSerial: - mrp_productions[i].action_generate_serial() - i += 1 - - if isAvailable: - error = False - try: - mrp_productions[i].button_mark_done() - except UserError: - error = True - - if isComponentTracking and not mrp_productions[i].picking_type_id.use_auto_consume_components_lots: - self.assertTrue(error, "Immediate Production shall raise an error when tracked product are not provided.") - else: - self.assertFalse(error, "Immediate Production shall not raise an error.") diff --git a/addons/mrp/tests/test_consume_component.py b/addons/mrp/tests/test_consume_component.py new file mode 100644 index 00000000000..31c901f7e11 --- /dev/null +++ b/addons/mrp/tests/test_consume_component.py @@ -0,0 +1,385 @@ +import copy + +from odoo.exceptions import UserError +from odoo.tests import common, tagged + + +class TestConsumeComponentCommon(common.TransactionCase): + + @classmethod + def setUpClass(cls): + """ + The following variables are used in each test to define the number of MO to generate. + They're also used as a verification in the executeConsumptionTriggers() to see if enough MO were passed to it + in order to test all the triggers. + + SERIAL : MO's product_tracking is 'serial' + DEFAULT : MO's product_tracking is 'none' or 'lot' + AVAILABLE : MO'S raw components are fully available + """ + super().setUpClass() + + cls.SERIAL_AVAILABLE_TRIGGERS_COUNT = 3 + cls.DEFAULT_AVAILABLE_TRIGGERS_COUNT = 2 + cls.SERIAL_TRIGGERS_COUNT = 2 + cls.DEFAULT_TRIGGERS_COUNT = 1 + + cls.manufacture_route = cls.env.ref('mrp.route_warehouse0_manufacture') + cls.stock_id = cls.env.ref('stock.stock_location_stock').id + + cls.picking_type = cls.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])[0] + cls.picking_type.use_create_components_lots = True + cls.picking_type.use_auto_consume_components_lots = True + + # Create Products & Components + cls.produced_lot = cls.env['product.product'].create({ + 'name': 'Produced Lot', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'lot', + 'route_ids': [(4, cls.manufacture_route.id, 0)], + }) + cls.produced_serial = cls.env['product.product'].create({ + 'name': 'Produced Serial', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'serial', + 'route_ids': [(4, cls.manufacture_route.id, 0)], + }) + cls.produced_none = cls.env['product.product'].create({ + 'name': 'Produced None', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'none', + 'route_ids': [(4, cls.manufacture_route.id, 0)], + }) + + cls.raw_lot = cls.env['product.product'].create({ + 'name': 'Raw Lot', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'lot', + }) + cls.raw_serial = cls.env['product.product'].create({ + 'name': 'Raw Serial', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'serial', + }) + cls.raw_none = cls.env['product.product'].create({ + 'name': 'Raw None', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'none', + }) + + cls.raws = [cls.raw_none, cls.raw_lot, cls.raw_serial] + + # Workcenter + cls.workcenter = cls.env['mrp.workcenter'].create({ + 'name': 'Assembly Line', + }) + + # BoMs + cls.bom_none = cls.env['mrp.bom'].create({ + 'product_tmpl_id': cls.produced_none.product_tmpl_id.id, + 'product_uom_id': cls.produced_none.uom_id.id, + 'consumption': 'flexible', + 'sequence': 1 + }) + + cls.bom_none_lines = cls.create_bom_lines(cls.bom_none, cls.raws, [3, 2, 1]) + + cls.bom_lot = cls.env['mrp.bom'].create({ + 'product_tmpl_id': cls.produced_lot.product_tmpl_id.id, + 'product_uom_id': cls.produced_lot.uom_id.id, + 'consumption': 'flexible', + 'sequence': 2 + }) + + cls.bom_lot_lines = cls.create_bom_lines(cls.bom_lot, cls.raws, [3, 2, 1]) + + cls.bom_serial = cls.env['mrp.bom'].create({ + 'product_tmpl_id': cls.produced_serial.product_tmpl_id.id, + 'product_uom_id': cls.produced_serial.uom_id.id, + 'consumption': 'flexible', + 'sequence': 1 + }) + + cls.bom_serial_lines = cls.create_bom_lines(cls.bom_serial, cls.raws, [3, 2, 1]) + + # Manufacturing Orders + cls.mo_none_tmpl = { + 'product_id': cls.produced_none.id, + 'product_uom_id': cls.produced_none.uom_id.id, + 'product_qty': 1, + 'bom_id': cls.bom_none.id + } + + cls.mo_lot_tmpl = { + 'product_id': cls.produced_lot.id, + 'product_uom_id': cls.produced_lot.uom_id.id, + 'product_qty': 1, + 'bom_id': cls.bom_lot.id + } + + cls.mo_serial_tmpl = { + 'product_id': cls.produced_serial.id, + 'product_uom_id': cls.produced_serial.uom_id.id, + 'product_qty': 1, + 'bom_id': cls.bom_serial.id + } + + @classmethod + def create_quant(cls, product, qty, offset=0, name="L"): + i = 1 + if product.tracking == 'serial': + i, qty = qty, 1 + if name == "L": + name = "S" + + vals = [] + for x in range(1, i + 1): + qDict = { + 'location_id': cls.stock_id, + 'product_id': product.id, + 'inventory_quantity': qty, + } + + if product.tracking != 'none': + qDict['lot_id'] = cls.env['stock.lot'].create({ + 'name': name + str(offset + x), + 'product_id': product.id, + 'company_id': cls.env.company.id + }).id + vals.append(qDict) + + return cls.env['stock.quant'].create(vals) + + @classmethod + def create_bom_lines(cls, bom, products, quantities=None): + if quantities is None: + quantities = [1 for i in range(len(products))] + + vals = [] + for product, seq in zip(products, range(len(products))): + vals.append({ + 'product_id': product.id, + 'product_qty': quantities[seq], + 'product_uom_id': product.uom_id.id, + 'sequence': seq, + 'bom_id': bom.id, + }) + + return cls.env['mrp.bom.line'].create(vals) + + @classmethod + def create_mo(cls, template, count): + vals = [] + for _ in range(count): + vals.append(copy.deepcopy(template)) + mos = cls.env['mrp.production'].create(vals) + mos.move_raw_ids.mapped('manual_consumption') + return mos + + def executeConsumptionTriggers(self, mrp_productions): + """There's 3 different triggers to test : _onchange_producing(), action_generate_serial(), button_mark_done(). + + Depending on the tracking of the final product and the availability of the components, + only a part of these 3 triggers is available or intended to work. + + This function automatically call and process the appropriate triggers. + """ + tracking = mrp_productions[0].product_tracking + sameTracking = True + for mo in mrp_productions: + sameTracking = sameTracking and mo.product_tracking == tracking + self.assertTrue(sameTracking, "MOs passed to the executeConsumptionTriggers method shall have the same product_tracking") + + isSerial = tracking == 'serial' + isAvailable = all(move.state == 'assigned' for move in mrp_productions.move_raw_ids) + isComponentTracking = any(move.has_tracking != 'none' for move in mrp_productions.move_raw_ids) + + countOk = True + length = len(mrp_productions) + if isSerial: + if isAvailable: + countOk = length == self.SERIAL_AVAILABLE_TRIGGERS_COUNT + else: + countOk = length == self.SERIAL_TRIGGERS_COUNT + else: + if isAvailable: + countOk = length == self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + else: + countOk = length == self.DEFAULT_TRIGGERS_COUNT + self.assertTrue(countOk, "The number of MOs passed to the executeConsumptionTriggers method does not match the associated TRIGGERS_COUNT") + + mrp_productions[0].qty_producing = mrp_productions[0].product_qty + mrp_productions[0]._onchange_producing() + + i = 1 + if isSerial: + mrp_productions[i].action_generate_serial() + i += 1 + + if isAvailable: + error = False + try: + mrp_productions[i].button_mark_done() + except UserError: + error = True + + if isComponentTracking and not mrp_productions[i].picking_type_id.use_auto_consume_components_lots: + self.assertTrue(error, "Immediate Production shall raise an error when tracked product are not provided.") + else: + self.assertFalse(error, "Immediate Production shall not raise an error.") + + +@tagged('post_install', '-at_install') +class TestConsumeComponent(TestConsumeComponentCommon): + def test_option_disabled_and_qty_available(self): + """Option disabled, qty available + -> Not Tracked components are fully consumed + -> Tracked components are only consumed on button_mark_done trigger + """ + self.picking_type.use_auto_consume_components_lots = False + + mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) + mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT) + mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) + + mo_all = mo_none + mo_serial + mo_lot + mo_all.action_confirm() + + all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT + + quant = self.create_quant(self.raw_none, 3 * all_qty) + quant |= self.create_quant(self.raw_lot, 2 * all_qty) + quant |= self.create_quant(self.raw_serial, 1 * all_qty) + quant.action_apply_inventory() + + # Quantities are fully reserved (stock.move state is available) + mo_all.action_assign() + for mov in mo_all.move_raw_ids: + self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.") + + # Test for Serial Product + self.executeConsumptionTriggers(mo_serial) + self.executeConsumptionTriggers(mo_none) + self.executeConsumptionTriggers(mo_lot) + for mov in mo_all.move_raw_ids: + if mov.has_tracking == 'none' or mov.raw_material_production_id.state == 'done': + self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") + else: + self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to 0.") + + def test_option_enabled_and_qty_available(self): + """Option enabled, qty available + -> Not Tracked components are fully consumed + -> Tracked components are fully consumed + """ + + mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) + mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT) + mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) + + mo_all = mo_none + mo_serial + mo_lot + mo_all.action_confirm() + + all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT + + quant = self.create_quant(self.raw_none, 3*all_qty) + quant |= self.create_quant(self.raw_lot, 2*all_qty) + quant |= self.create_quant(self.raw_serial, 1*all_qty) + quant.action_apply_inventory() + + # Quantities are fully reserved (stock.move state is available) + mo_all.action_assign() + for mov in mo_all.move_raw_ids: + self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.") + + self.executeConsumptionTriggers(mo_serial) + self.executeConsumptionTriggers(mo_none) + self.executeConsumptionTriggers(mo_lot) + for mov in mo_all.move_raw_ids: + self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") + + def test_option_enabled_and_qty_not_available(self): + """Option enabled, qty not available + -> Not Tracked components are fully consumed + -> Tracked components are not consumed + """ + + mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_TRIGGERS_COUNT) + mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_TRIGGERS_COUNT) + mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT) + + mo_all = mo_none + mo_serial + mo_lot + mo_all.action_confirm() + + # Quantities are not reserved at all (stock.move state is confirmed) + mo_all.action_assign() + for mov in mo_all.move_raw_ids: + self.assertEqual(0, mov.reserved_availability, "Reserved quantity shall be equal to 0.") + + self.executeConsumptionTriggers(mo_serial) + self.executeConsumptionTriggers(mo_none) + self.executeConsumptionTriggers(mo_lot) + + for mov in mo_all.move_raw_ids: + if mov.has_tracking == 'none': + self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") + else: + self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") + + def test_option_enabled_and_qty_partially_available(self): + """Option enabled, qty partially available + -> Not Tracked components are fully consumed + -> Tracked components are partially consumed + """ + + # Update BoM serial component qty + self.bom_none_lines[2].product_qty = 2 + self.bom_serial_lines[2].product_qty = 2 + self.bom_lot_lines[2].product_qty = 2 + + raw_none_qty = 2 + raw_tracked_qty = 1 + + quant = self.create_quant(self.raw_none, raw_none_qty) + quant |= self.create_quant(self.raw_lot, raw_tracked_qty) + quant |= self.create_quant(self.raw_serial, raw_tracked_qty) + quant.action_apply_inventory() + + # We must create & process each MO at once as we must assign quants for each individually + def testUnit(mo_tmpl, serialTrigger=None): + mo = self.create_mo(mo_tmpl, 1) + mo.action_confirm() + + # are partially reserved (stock.move state is partially_available) + mo.action_assign() + for mov in mo.move_raw_ids: + if mov.has_tracking == "none": + self.assertEqual(raw_none_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_none_qty)+ ".") + else: + self.assertEqual(raw_tracked_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_tracked_qty)+ ".") + + if serialTrigger is None: + self.executeConsumptionTriggers(mo) + elif serialTrigger == 1: + mo.qty_producing = mo.product_qty + mo._onchange_producing() + elif serialTrigger == 2: + mo.action_generate_serial() + + for mov in mo.move_raw_ids: + if mov.has_tracking == "none": + self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") + else: + self.assertEqual(raw_tracked_qty, mov.quantity_done, "Done quantity shall be equal to " + str(raw_tracked_qty)+ ".") + mo.action_cancel() + + testUnit(self.mo_none_tmpl) + testUnit(self.mo_lot_tmpl) + testUnit(self.mo_serial_tmpl, 1) + testUnit(self.mo_serial_tmpl, 2) diff --git a/addons/mrp/tests/test_consume_tracked_component.py b/addons/mrp/tests/test_consume_tracked_component.py deleted file mode 100644 index cded3090949..00000000000 --- a/addons/mrp/tests/test_consume_tracked_component.py +++ /dev/null @@ -1,157 +0,0 @@ -from odoo.tests import tagged -from odoo.addons.mrp.tests.common_consume_tracked_component import TestConsumeTrackedComponentCommon - -@tagged('post_install', '-at_install') -class TestConsumeTrackedComponent(TestConsumeTrackedComponentCommon): - - def test_option_disabled_and_qty_available(self): - """ - Option disabled, qty available - -> Not Tracked components are fully consumed - -> Tracked components are only consumed on button_mark_done trigger - """ - - self.picking_type.use_auto_consume_components_lots = False - - mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) - mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT) - mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) - - mo_all = mo_none + mo_serial + mo_lot - mo_all.action_confirm() - - all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT - - quant = self.create_quant(self.raw_none, 3*all_qty) - quant |= self.create_quant(self.raw_lot, 2*all_qty) - quant |= self.create_quant(self.raw_serial, 1*all_qty) - quant.action_apply_inventory() - - #Quantities are fully reserved (stock.move state is available) - mo_all.action_assign() - for mov in mo_all.move_raw_ids: - self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.") - - #Test for Serial Product - self.executeConsumptionTriggers(mo_serial) - self.executeConsumptionTriggers(mo_none) - self.executeConsumptionTriggers(mo_lot) - for mov in mo_all.move_raw_ids: - if mov.has_tracking == 'none' or mov.raw_material_production_id.state == 'done': - self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") - else: - self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to 0.") - - def test_option_enabled_and_qty_available(self): - """ - Option enabled, qty available - -> Not Tracked components are fully consumed - -> Tracked components are fully consumed - """ - - mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) - mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_AVAILABLE_TRIGGERS_COUNT) - mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_AVAILABLE_TRIGGERS_COUNT) - - mo_all = mo_none + mo_serial + mo_lot - mo_all.action_confirm() - - all_qty = 2 * self.DEFAULT_AVAILABLE_TRIGGERS_COUNT + self.SERIAL_AVAILABLE_TRIGGERS_COUNT - - quant = self.create_quant(self.raw_none, 3*all_qty) - quant |= self.create_quant(self.raw_lot, 2*all_qty) - quant |= self.create_quant(self.raw_serial, 1*all_qty) - quant.action_apply_inventory() - - #Quantities are fully reserved (stock.move state is available) - mo_all.action_assign() - for mov in mo_all.move_raw_ids: - self.assertEqual(mov.product_qty, mov.reserved_availability, "Reserved quantity shall be equal to To Consume quantity.") - - self.executeConsumptionTriggers(mo_serial) - self.executeConsumptionTriggers(mo_none) - self.executeConsumptionTriggers(mo_lot) - for mov in mo_all.move_raw_ids: - self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") - - def test_option_enabled_and_qty_not_available(self): - """ - Option enabled, qty not available - -> Not Tracked components are fully consumed - -> Tracked components are not consumed - """ - - mo_none = self.create_mo(self.mo_none_tmpl, self.DEFAULT_TRIGGERS_COUNT) - mo_serial = self.create_mo(self.mo_serial_tmpl, self.SERIAL_TRIGGERS_COUNT) - mo_lot = self.create_mo(self.mo_lot_tmpl, self.DEFAULT_TRIGGERS_COUNT) - - mo_all = mo_none + mo_serial + mo_lot - mo_all.action_confirm() - - #Quantities are not reserved at all (stock.move state is confirmed) - mo_all.action_assign() - for mov in mo_all.move_raw_ids: - self.assertEqual(0, mov.reserved_availability, "Reserved quantity shall be equal to 0.") - - self.executeConsumptionTriggers(mo_serial) - self.executeConsumptionTriggers(mo_none) - self.executeConsumptionTriggers(mo_lot) - - for mov in mo_all.move_raw_ids: - if mov.has_tracking == 'none': - self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") - else: - self.assertEqual(0, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") - - def test_option_enabled_and_qty_partially_available(self): - """ - Option enabled, qty partially available - -> Not Tracked components are fully consumed - -> Tracked components are partially consumed - """ - - #update BoM serial component qty - self.bom_none_lines[2].product_qty = 2 - self.bom_serial_lines[2].product_qty = 2 - self.bom_lot_lines[2].product_qty = 2 - - raw_none_qty = 2 - raw_tracked_qty = 1 - - quant = self.create_quant(self.raw_none, raw_none_qty) - quant |= self.create_quant(self.raw_lot, raw_tracked_qty) - quant |= self.create_quant(self.raw_serial, raw_tracked_qty) - quant.action_apply_inventory() - - #We must create & process each MO at once as we must assign quants for each individually - def testUnit(mo_tmpl, serialTrigger=None): - mo = self.create_mo(mo_tmpl, 1) - mo.action_confirm() - - #Quantities are partially reserved (stock.move state is partially_available) - mo.action_assign() - for mov in mo.move_raw_ids: - if mov.has_tracking == "none": - self.assertEqual(raw_none_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_none_qty)+ ".") - else: - self.assertEqual(raw_tracked_qty, mov.reserved_availability, "Reserved quantity shall be equal to " + str(raw_tracked_qty)+ ".") - - if serialTrigger is None: - self.executeConsumptionTriggers(mo) - elif serialTrigger == 1: - mo.qty_producing = mo.product_qty - mo._onchange_producing() - elif serialTrigger == 2: - mo.action_generate_serial() - - for mov in mo.move_raw_ids: - if mov.has_tracking == "none": - self.assertEqual(mov.product_qty, mov.quantity_done, "Done quantity shall be equal to To Consume quantity.") - else: - self.assertEqual(raw_tracked_qty, mov.quantity_done, "Done quantity shall be equal to " + str(raw_tracked_qty)+ ".") - mo.action_cancel() - - testUnit(self.mo_none_tmpl) - testUnit(self.mo_lot_tmpl) - testUnit(self.mo_serial_tmpl, 1) - testUnit(self.mo_serial_tmpl, 2)