diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 21ce6234d3e..49bcbe0799b 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -351,7 +351,7 @@ class StockMove(models.Model): moves_ids_to_unlink = OrderedSet() phantom_moves_vals_list = [] for move in self: - if not move.picking_type_id or (move.production_id and move.production_id.product_id == move.product_id): + if (not move.picking_type_id and not self.env.context.get('is_scrap')) or (move.production_id and move.production_id.product_id == move.product_id): moves_ids_to_return.add(move.id) continue bom = self.env['mrp.bom'].sudo()._bom_find(move.product_id, company_id=move.company_id.id, bom_type='phantom')[move.product_id] @@ -364,7 +364,7 @@ class StockMove(models.Model): factor = move.product_uom._compute_quantity(move.product_uom_qty, bom.product_uom_id) / bom.product_qty boms, lines = bom.sudo().explode(move.product_id, factor, picking_type=bom.picking_type_id) for bom_line, line_data in lines: - if move.picking_id.immediate_transfer or float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): + if move.picking_id.immediate_transfer or float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding) or self.env.context.get('is_scrap'): phantom_moves_vals_list += move._generate_move_phantom(bom_line, 0, line_data['qty']) else: phantom_moves_vals_list += move._generate_move_phantom(bom_line, line_data['qty'], 0) diff --git a/addons/mrp/models/stock_scrap.py b/addons/mrp/models/stock_scrap.py index 20ea8329c6a..b8f4184c8de 100644 --- a/addons/mrp/models/stock_scrap.py +++ b/addons/mrp/models/stock_scrap.py @@ -3,7 +3,6 @@ from odoo import _, api, fields, models - class StockScrap(models.Model): _inherit = 'stock.scrap' @@ -14,6 +13,12 @@ class StockScrap(models.Model): 'mrp.workorder', 'Work Order', states={'done': [('readonly', True)]}, check_company=True) # Not to restrict or prefer quants, but informative + product_is_kit = fields.Boolean(related='product_id.is_kits') + product_template = fields.Many2one(related='product_id.product_tmpl_id') + bom_id = fields.Many2one( + 'mrp.bom', 'Kit', + domain="[('type', '=', 'phantom'), '|', ('product_id', '=', product_id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_template)]", + states={'done': [('readonly', True)]}, check_company=True) @api.onchange('workorder_id') def _onchange_workorder_id(self): @@ -50,3 +55,19 @@ class StockScrap(models.Model): return {'warning': {'title': _('Warning'), 'message': message}} else: return super()._onchange_serial_number() + + @api.depends('move_ids', 'move_ids.move_line_ids.qty_done', 'product_id') + def _compute_scrap_qty(self): + self.scrap_qty = 1 + for scrap in self: + if not scrap.bom_id: + return super(StockScrap, scrap)._compute_scrap_qty() + if scrap.move_ids: + filters = { + 'incoming_moves': lambda m: True, + 'outgoing_moves': lambda m: False + } + scrap.scrap_qty = scrap.move_ids._compute_kit_quantities(scrap.product_id, scrap.scrap_qty, scrap.bom_id, filters) + + def _should_check_available_qty(self): + return super()._should_check_available_qty() or self.product_is_kit diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py index f15ae8e219f..27f6a4c0c32 100644 --- a/addons/mrp/tests/test_stock.py +++ b/addons/mrp/tests/test_stock.py @@ -146,7 +146,7 @@ class TestWarehouseMrp(common.TestMrpCommon): # Scrap Product Wood with lot. scrap_id = self.env['stock.scrap'].with_context(active_model='mrp.production', active_id=production_3.id).create({'product_id': self.product_2.id, 'scrap_qty': 1.0, 'product_uom_id': self.product_2.uom_id.id, 'location_id': location_id, 'lot_id': lot_product_2.id, 'production_id': production_3.id}) scrap_id.do_scrap() - scrap_move = scrap_id.move_id + scrap_move = scrap_id.move_ids[0] self.assertTrue(scrap_move.raw_material_production_id) self.assertTrue(scrap_move.scrapped) diff --git a/addons/mrp/views/stock_scrap_views.xml b/addons/mrp/views/stock_scrap_views.xml index 11fcd7bd077..e1b51b20ab8 100644 --- a/addons/mrp/views/stock_scrap_views.xml +++ b/addons/mrp/views/stock_scrap_views.xml @@ -8,6 +8,9 @@ + + + @@ -19,6 +22,9 @@ + + + diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 703f6bd965e..f99ef69c8cc 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -127,7 +127,7 @@ class StockMove(models.Model): "this second option should be chosen.") scrapped = fields.Boolean( 'Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True) - scrap_ids = fields.One2many('stock.scrap', 'move_id') + scrap_id = fields.Many2one('stock.scrap', 'Scrap operation', readonly=True, check_company=True) group_id = fields.Many2one('procurement.group', 'Procurement Group', default=_default_group_id, index=True) rule_id = fields.Many2one( 'stock.rule', 'Stock Rule', ondelete='restrict', help='The stock rule that created this stock move', @@ -1881,7 +1881,7 @@ Please change the quantity done or the rounding precision of your unit of measur # We don't want to create back order for scrap moves # Replace by a kwarg in master if self.env.context.get('is_scrap'): - return moves_todo + return moves if picking and not cancel_backorder: backorder = picking._create_backorder() diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 8c33239c9f6..967ab43e396 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -947,7 +947,7 @@ class Picking(models.Model): move_ids_without_package |= move else: move_ids_without_package |= move - return move_ids_without_package.filtered(lambda move: not move.scrap_ids) + return move_ids_without_package.filtered(lambda move: not move.scrap_id) def _check_move_lines_map_quant_package(self, package): return package._check_move_lines_map_quant(self.move_line_ids.filtered(lambda ml: ml.package_id == package), 'reserved_qty') diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py index c2719d63f14..1034cfe6db2 100644 --- a/addons/stock/models/stock_scrap.py +++ b/addons/stock/models/stock_scrap.py @@ -34,7 +34,7 @@ class StockScrap(models.Model): 'stock.quant.package', 'Package', states={'done': [('readonly', True)]}, check_company=True) owner_id = fields.Many2one('res.partner', 'Owner', states={'done': [('readonly', True)]}, check_company=True) - move_id = fields.Many2one('stock.move', 'Scrap Move', readonly=True, check_company=True, copy=False) + move_ids = fields.One2many('stock.move', 'scrap_id') picking_id = fields.Many2one('stock.picking', 'Picking', states={'done': [('readonly', True)]}, check_company=True) location_id = fields.Many2one( 'stock.location', 'Source Location', @@ -46,7 +46,7 @@ class StockScrap(models.Model): domain="[('scrap_location', '=', True), ('company_id', 'in', [company_id, False])]", check_company=True, readonly=False) scrap_qty = fields.Float( 'Quantity', required=True, states={'done': [('readonly', True)]}, digits='Product Unit of Measure', - compute='_compute_scrap_qty', precompute=True, readonly=False, store=True) + compute='_compute_scrap_qty', default=0.0, readonly=False, store=True) state = fields.Selection([ ('draft', 'Draft'), ('done', 'Done')], @@ -83,12 +83,12 @@ class StockScrap(models.Model): for scrap in self: scrap.scrap_location_id = locations_per_company[scrap.company_id.id] - @api.depends('move_id', 'move_id.move_line_ids.qty_done', 'product_id') + @api.depends('move_ids', 'move_ids.move_line_ids.qty_done', 'product_id') def _compute_scrap_qty(self): self.scrap_qty = 1 for scrap in self: - if scrap.move_id: - scrap.scrap_qty = scrap.move_id.quantity_done + if scrap.move_ids: + scrap.scrap_qty = scrap.move_ids[0].quantity_done @api.onchange('lot_id') def _onchange_serial_number(self): @@ -120,6 +120,7 @@ class StockScrap(models.Model): 'product_uom_qty': self.scrap_qty, 'location_id': self.location_id.id, 'scrapped': True, + 'scrap_id': self.id, 'location_dest_id': self.scrap_location_id.id, 'move_line_ids': [(0, 0, {'product_id': self.product_id.id, 'product_uom_id': self.product_uom_id.id, @@ -140,7 +141,7 @@ class StockScrap(models.Model): move = self.env['stock.move'].create(scrap._prepare_move_values()) # master: replace context by cancel_backorder move.with_context(is_scrap=True)._action_done() - scrap.write({'move_id': move.id, 'state': 'done'}) + scrap.write({'state': 'done'}) scrap.date_done = fields.Datetime.now() return True @@ -151,25 +152,32 @@ class StockScrap(models.Model): def action_get_stock_move_lines(self): action = self.env['ir.actions.act_window']._for_xml_id('stock.stock_move_line_action') - action['domain'] = [('move_id', '=', self.move_id.id)] + action['domain'] = [('move_id', 'in', self.move_ids.ids)] return action + def _should_check_available_qty(self): + return self.product_id.type == 'product' + + def check_available_qty(self): + if not self._should_check_available_qty(): + return True + + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + available_qty = self.with_context( + location=self.location_id.id, + lot_id=self.lot_id.id, + package_id=self.package_id.id, + owner_id=self.owner_id.id + ).product_id.qty_available + scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id) + return float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0 + def action_validate(self): self.ensure_one() if float_is_zero(self.scrap_qty, precision_rounding=self.product_uom_id.rounding): raise UserError(_('You can only enter positive quantities.')) - if self.product_id.type != 'product': - return self.do_scrap() - precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - available_qty = sum(self.env['stock.quant']._gather(self.product_id, - self.location_id, - self.lot_id, - self.package_id, - self.owner_id, - strict=True).mapped('quantity')) - scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id) - if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0: + if self.check_available_qty(): return self.do_scrap() else: ctx = dict(self.env.context) @@ -177,7 +185,7 @@ class StockScrap(models.Model): 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_scrap_id': self.id, - 'default_quantity': scrap_qty, + 'default_quantity': self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id), 'default_product_uom_name': self.product_id.uom_name }) return { diff --git a/addons/stock/report/stock_traceability.py b/addons/stock/report/stock_traceability.py index eb1aba84c8b..15d99f77076 100644 --- a/addons/stock/report/stock_traceability.py +++ b/addons/stock/report/stock_traceability.py @@ -94,10 +94,10 @@ class MrpStockReport(models.TransientModel): res_model = 'stock.move' res_id = move_line.move_id.id ref = 'Inventory Adjustment' - elif move_line.move_id.scrapped and move_line.move_id.scrap_ids: + elif move_line.move_id.scrapped and move_line.move_id.scrap_id: res_model = 'stock.scrap' - res_id = move_line.move_id.scrap_ids[0].id - ref = move_line.move_id.scrap_ids[0].name + res_id = move_line.move_id.scrap_id.id + ref = move_line.move_id.scrap_id.name return res_model, res_id, ref @api.model diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 1184dfb7e17..72983743f1d 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -4727,7 +4727,7 @@ class StockMove(TransactionCase): scrap = scrap_form.save() scrap.do_scrap() self.assertEqual(scrap.state, 'done') - move = scrap.move_id + move = scrap.move_ids[0] self.assertEqual(move.state, 'done') self.assertEqual(move.quantity_done, 1) self.assertEqual(move.scrapped, True) @@ -4746,7 +4746,7 @@ class StockMove(TransactionCase): scrap.do_scrap() self.assertTrue(scrap.name.startswith('SP/'), 'Sequence should be Changed after do_scrap') self.assertEqual(scrap.state, 'done') - move = scrap.move_id + move = scrap.move_ids[0] self.assertEqual(move.state, 'done') self.assertEqual(move.quantity_done, 1) self.assertEqual(move.scrapped, True) @@ -4818,7 +4818,7 @@ class StockMove(TransactionCase): self.assertEqual(len(picking.move_ids), 2) scrapped_move = picking.move_ids.filtered(lambda m: m.state == 'done') self.assertTrue(scrapped_move, 'No scrapped move created.') - self.assertEqual(scrapped_move.scrap_ids.ids, [scrap.id], 'Wrong scrap linked to the move.') + self.assertEqual(scrapped_move.scrap_id.id, scrap.id, 'Wrong scrap linked to the move.') self.assertEqual(scrap.scrap_qty, 5, 'Scrap quantity has been modified and is not correct anymore.') scrapped_move.quantity_done = 8 diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 56f9a5793cc..c466c4725f6 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -2381,7 +2381,7 @@ class TestStockFlow(TestStockCommon): self.assertEqual(picking.state, 'cancel') self.assertEqual(move.state, 'cancel') - self.assertEqual(scrap.move_id.state, 'done') + self.assertEqual(scrap.move_ids[0].state, 'done') def test_receive_tracked_product(self): self.productA.tracking = 'serial' diff --git a/addons/stock/views/stock_scrap_views.xml b/addons/stock/views/stock_scrap_views.xml index 97f862422c5..e90fde8b570 100644 --- a/addons/stock/views/stock_scrap_views.xml +++ b/addons/stock/views/stock_scrap_views.xml @@ -38,12 +38,12 @@ - +