[FIX] repair,mrp,stock: count returned SN products

Suppose a tracked-by-usn and consumed product returns in the stock
thanks to a repair order. Using again this component in a new
manufacturing order will raise an error

To reproduce the issue:
1. In Settings, enable "Storage Locations"
2. Create two products P_finished, P_compo
    - Storable
    - P_comp tracked by USN
3. Update the quantity of P_compo:
    - WH/Stock: 1 x Lot01
4. Create a manufacturing order MO:
    - Product: P_finished
    - Components:
        - 1 x P_compo
5. Confirm, Check availability and Mark MO as Done
    - (Lot01 should be consumed)
6. Create a repair order RO:
    - Product: P_finished
    - Parts:
        - Type: Remove
        - Product: P_compo
        - Lot: Lot01
        - Destination Location: WH/Stock
7. Confirm RO, Start RO, End RO
    - (There should be one Lot01 available in stock)
8. Repeat 4-5

Error: When checking the availability on the MO, Lot01 is correctly
reserved. However, when marking the second MO as done, a User Error is
displayed: "The serial number Lot01 used for component P_compo has
already been consumed" although this lot should be available

When checking the uniqueness of the lot, nothing includes the products
back in stock thanks to the repair orders.

OPW-2701668

closes odoo/odoo#82544

X-original-commit: 3d9355f90fa1dd9436f1745c515c89947ed04de0
Signed-off-by: Tiffany Chang <tic@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
Adrien Widart
2022-01-11 15:24:11 +00:00
parent 2e6e2f59e5
commit 683c02b099
6 changed files with 99 additions and 15 deletions
+7 -15
View File
@@ -1949,36 +1949,28 @@ class MrpProduction(models.Model):
for move_line in move.move_line_ids:
if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding):
continue
domain = [
('lot_id', '=', move_line.lot_id.id),
('qty_done', '=', 1),
('state', '=', 'done')
]
message = _('The serial number %(number)s used for component %(component)s has already been consumed',
number=move_line.lot_id.name,
component=move_line.product_id.name)
co_prod_move_lines = self.move_raw_ids.move_line_ids
domain_unbuild = domain + [
('production_id', '=', False),
('location_id.usage', '=', 'production')
]
# Check presence of same sn in previous productions
duplicates = self.env['stock.move.line'].search_count(domain + [
('location_dest_id.usage', '=', 'production')
duplicates = self.env['stock.move.line'].search_count([
('lot_id', '=', move_line.lot_id.id),
('qty_done', '=', 1),
('state', '=', 'done'),
('location_dest_id.usage', '=', 'production'),
])
if duplicates:
# Maybe some move lines have been compensated by unbuild
duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [
('move_id.unbuild_id', '!=', False)
])
duplicates_returned = move.product_id._count_returned_sn_products(move_line.lot_id)
removed = self.env['stock.move.line'].search_count([
('lot_id', '=', move_line.lot_id.id),
('state', '=', 'done'),
('location_dest_id.scrap_location', '=', True)
])
# Either removed or unbuild
if not ((duplicates_unbuild or removed) and duplicates - duplicates_unbuild - removed == 0):
if not ((duplicates_returned or removed) and duplicates - duplicates_returned - removed == 0):
raise UserError(message)
# Check presence of same sn in current production
duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line
+11
View File
@@ -261,3 +261,14 @@ class ProductProduct(models.Model):
if not any(ptav in self.product_template_attribute_value_ids for ptav in iter_ptav):
return False
return True
def _count_returned_sn_products(self, sn_lot):
res = self.env['stock.move.line'].search_count([
('lot_id', '=', sn_lot.id),
('qty_done', '=', 1),
('state', '=', 'done'),
('production_id', '=', False),
('location_id.usage', '=', 'production'),
('move_id.unbuild_id', '!=', False),
])
return super()._count_returned_sn_products(sn_lot) + res
@@ -72,3 +72,61 @@ class TestRepairTraceability(TestMrpCommon):
})
# We are not forbidden to use that serial number, so nothing raised here
mo2.button_mark_done()
def test_mo_with_used_sn_component(self):
"""
Suppose a tracked-by-usn component has been used to produce a product. Then, using a repair order,
this component is removed from the product and returned as available stock. The user should be able to
use the component in a new MO
"""
def produce_one(product, component):
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = product
with mo_form.move_raw_ids.new() as raw_line:
raw_line.product_id = component
raw_line.product_uom_qty = 1
mo = mo_form.save()
mo.action_confirm()
mo.action_assign()
action = mo.button_mark_done()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
return mo
stock_location = self.env.ref('stock.stock_location_stock')
finished, component = self.env['product.product'].create([{
'name': 'Finished Product',
'type': 'product',
}, {
'name': 'SN Componentt',
'type': 'product',
'tracking': 'serial',
}])
sn_lot = self.env['stock.lot'].create({
'product_id': component.id,
'name': 'USN01',
'company_id': self.env.company.id,
})
self.env['stock.quant']._update_available_quantity(component, stock_location, 1, lot_id=sn_lot)
mo = produce_one(finished, component)
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.move_raw_ids.lot_ids, sn_lot)
ro_form = Form(self.env['repair.order'])
ro_form.product_id = finished
with ro_form.operations.new() as ro_line:
ro_line.type = 'remove'
ro_line.product_id = component
ro_line.lot_id = sn_lot
ro_line.location_dest_id = stock_location
ro = ro_form.save()
ro.action_validate()
ro.action_repair_start()
ro.action_repair_end()
mo = produce_one(finished, component)
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.move_raw_ids.lot_ids, sn_lot)
+1
View File
@@ -5,3 +5,4 @@ from . import repair
from . import stock_traceability
from . import stock_lot
from . import account_move
from . import product
+18
View File
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class Product(models.Model):
_inherit = "product.product"
def _count_returned_sn_products(self, sn_lot):
res = self.env['repair.line'].search_count([
('type', '=', 'remove'),
('product_uom_qty', '=', 1),
('lot_id', '=', sn_lot.id),
('state', '=', 'done'),
('location_dest_id.usage', '=', 'internal'),
])
return super()._count_returned_sn_products(sn_lot) + res
+4
View File
@@ -596,6 +596,10 @@ class Product(models.Model):
linked_product_ids = [group['product_id'][0] for group in lines]
return super(Product, self - self.browse(linked_product_ids))._filter_to_unlink()
@api.model
def _count_returned_sn_products(self, sn_lot):
return 0
class ProductTemplate(models.Model):
_inherit = 'product.template'