summaryrefslogtreecommitdiff
path: root/addons/mrp_subcontracting
diff options
context:
space:
mode:
authorRémy Voet (ryv) <ryv@odoo.com>2020-11-10 16:30:28 +0000
committerRémy Voet (ryv) <ryv@odoo.com>2020-11-18 09:16:08 +0000
commit1a34e46eaca14aec981dd0f162e8014c7cb0abea (patch)
treef8ba8ec3169f43f1fb164914ba29452d02498951 /addons/mrp_subcontracting
parent9e244ae3053f46fc22b8e9a75015d57cd70053b8 (diff)
[FIX] mrp_subcontracting: fix flow with only tracked finished product
Step to reproduce: - Create a subcontracting BoM by partner A, for a tracked product B (by serial or lots) - Create a incoming transfer coming from A and mark as todo. - Fill the move with move lines with lot + quantity. - It is impossible to validate the transfer, "You need to supply lot..." Also at the creation of the picking a warning popover indicate that the previous operation (the hidden MO) is set after the transfer. These issues comes from the refactor of mrp for v14 and the part of subcontracting wasn't complete obviously. Fixes done: - Rewrite the code of the `_action_done` for subcontracting. It is for the case of tracked finished product without any tracked component, there wasn't any code to manage that. Now manage it by backorder MO feature. Fix the main bug - Set finished_date_planned before the transfer to avoid the alert popover. - Avoid to write activity in channel about the hidden MO in case of cancelling. - Fix `action_record_components` to manage multiple subcontracting products with tracked component. - Remove the `priority` field from the wizard MO (in case of tracked product. Also the wizard is not really a wizard, just a weird hybrid MO form) PR: odoo/odoo#61606 task-2357115 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
Diffstat (limited to 'addons/mrp_subcontracting')
-rw-r--r--addons/mrp_subcontracting/models/mrp_production.py5
-rw-r--r--addons/mrp_subcontracting/models/stock_move.py23
-rw-r--r--addons/mrp_subcontracting/models/stock_picking.py86
-rw-r--r--addons/mrp_subcontracting/tests/test_subcontracting.py58
-rw-r--r--addons/mrp_subcontracting/views/mrp_production_views.xml3
5 files changed, 126 insertions, 49 deletions
diff --git a/addons/mrp_subcontracting/models/mrp_production.py b/addons/mrp_subcontracting/models/mrp_production.py
index fcc77b32e63..e546bf1b034 100644
--- a/addons/mrp_subcontracting/models/mrp_production.py
+++ b/addons/mrp_subcontracting/models/mrp_production.py
@@ -125,3 +125,8 @@ class MrpProduction(models.Model):
for ml in subcontract_move_id.move_line_ids:
ml.product_uom_qty = ml.qty_done
subcontract_move_id._recompute_state()
+
+ def _subcontracting_filter_to_done(self):
+ """ Filter subcontracting production where composant is already recorded and should be consider to be validate """
+ mos = self.filtered(lambda mo: mo.state not in ('done', 'cancel'))
+ return mos.filtered(lambda pro: all(line.lot_id for line in pro.move_raw_ids.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids))
diff --git a/addons/mrp_subcontracting/models/stock_move.py b/addons/mrp_subcontracting/models/stock_move.py
index 32817a4ca8b..45f5bcce5eb 100644
--- a/addons/mrp_subcontracting/models/stock_move.py
+++ b/addons/mrp_subcontracting/models/stock_move.py
@@ -51,11 +51,18 @@ class StockMove(models.Model):
""" If the initial demand is updated then also update the linked
subcontract order to the new quantity.
"""
- if 'product_uom_qty' in values:
- if self.env.context.get('cancel_backorder') is False:
- return super(StockMove, self).write(values)
+ if 'product_uom_qty' in values and self.env.context.get('cancel_backorder') is not False:
self.filtered(lambda m: m.is_subcontract and m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty(values['product_uom_qty'])
- return super(StockMove, self).write(values)
+ res = super().write(values)
+ if 'date' in values:
+ for move in self:
+ if move.state in ('done', 'cancel') or not move.is_subcontract:
+ continue
+ move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel')).write({
+ 'date_planned_finished': move.date,
+ 'date_planned_start': move.date,
+ })
+ return res
def action_show_details(self):
""" Open the produce wizard in order to register tracked components for
@@ -97,10 +104,10 @@ class StockMove(models.Model):
def _action_cancel(self):
for move in self:
if move.is_subcontract:
- production = move.move_orig_ids.production_id
+ active_production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
moves = self.env.context.get('moves_todo')
- if not moves or production not in moves.move_orig_ids.production_id:
- production._action_cancel()
+ if not moves or active_production not in moves.move_orig_ids.production_id:
+ active_production.with_context(skip_activity=True).action_cancel()
return super()._action_cancel()
def _action_confirm(self, merge=True, merge_into=False):
@@ -192,7 +199,7 @@ class StockMove(models.Model):
break
if quantity_to_remove >= production.product_qty:
quantity_to_remove -= production.product_qty
- production.action_cancel()
+ production.with_context(skip_activity=True).action_cancel()
else:
self.env['change.production.qty'].with_context(skip_activity=True).create({
'mo_id': production.id,
diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py
index 6faebd8a809..23949e6cac9 100644
--- a/addons/mrp_subcontracting/models/stock_picking.py
+++ b/addons/mrp_subcontracting/models/stock_picking.py
@@ -4,7 +4,7 @@
from datetime import timedelta
from odoo import api, fields, models
-from odoo.tools.float_utils import float_is_zero
+from odoo.tools.float_utils import float_compare, float_is_zero
class StockPicking(models.Model):
@@ -41,39 +41,48 @@ class StockPicking(models.Model):
# -------------------------------------------------------------------------
def _action_done(self):
res = super(StockPicking, self)._action_done()
+
+ for move in self.move_lines.filtered(lambda move: move.is_subcontract):
+ # Auto set qty_producing/lot_producing_id of MO if there isn't tracked component
+ # If there is tracked component, the flow use subcontracting_record_component instead
+ if move._has_tracked_subcontract_components():
+ continue
+ production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))[-1:]
+ if not production:
+ continue
+ # Manage additional quantities
+ quantity_done_move = move.product_uom._compute_quantity(move.quantity_done, production.product_uom_id)
+ if float_compare(production.product_qty, quantity_done_move, precision_rounding=production.product_uom_id.rounding) == -1:
+ change_qty = self.env['change.production.qty'].create({
+ 'mo_id': production.id,
+ 'product_qty': quantity_done_move
+ })
+ change_qty.with_context(skip_activity=True).change_prod_qty()
+ # Create backorder MO for each move lines
+ for move_line in move.move_line_ids:
+ if move_line.lot_id:
+ production.lot_producing_id = move_line.lot_id
+ production.qty_producing = move_line.product_uom_id._compute_quantity(move_line.qty_done, production.product_uom_id)
+ production._set_qty_producing()
+ if move_line != move.move_line_ids[-1]:
+ backorder = production._generate_backorder_productions(close_mo=False)
+ # The move_dest_ids won't be set because the _split filter out done move
+ backorder.move_finished_ids.filtered(lambda mo: mo.product_id == move.product_id).move_dest_ids = production.move_finished_ids.filtered(lambda mo: mo.product_id == move.product_id).move_dest_ids
+ production.product_qty = production.qty_producing
+ production = backorder
+
for picking in self:
- # Auto set qty_producing if not tracked component
- for move in picking.move_lines.filtered(lambda move: move.is_subcontract):
- if not move._has_tracked_subcontract_components():
- production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
- if production:
- qty_done_production_uom = move.product_uom._compute_quantity(move.quantity_done, production.product_uom_id)
- production.qty_producing += qty_done_production_uom
- for move in (production.move_raw_ids | production.move_finished_ids.filtered(lambda m: m.product_id != production.product_id)):
- if move.state in ('done', 'cancel'):
- continue
- if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
- continue
- new_qty = production.product_uom_id._compute_quantity(
- (production.qty_producing - production.qty_produced) * move.unit_factor, production.product_uom_id,
- rounding_method='HALF-UP')
- move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
- move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
-
- productions_to_done = picking.move_lines.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
- # TODO : to change in master with field on mrp
- productions_to_done = productions_to_done.filtered(lambda pro: all(line.lot_id for line in pro.move_raw_ids.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids))
- for subcontracted_production in productions_to_done:
- if subcontracted_production.state == 'progress':
- subcontracted_production._post_inventory()
- else:
- subcontracted_production.with_context(subcontract_move_id=True).button_mark_done()
- # For concistency, set the date on production move before the date
- # on picking. (Tracability report + Product Moves menu item)
- minimum_date = min(picking.move_line_ids.mapped('date'))
- production_moves = subcontracted_production.move_raw_ids | subcontracted_production.move_finished_ids
- production_moves.write({'date': minimum_date - timedelta(seconds=1)})
- production_moves.move_line_ids.write({'date': minimum_date - timedelta(seconds=1)})
+ productions_to_done = picking._get_subcontracted_productions()._subcontracting_filter_to_done()
+ production_ids_backorder = []
+ if not self.env.context.get('cancel_backorder'):
+ production_ids_backorder = productions_to_done.filtered(lambda mo: mo.state == "progress").ids
+ productions_to_done.with_context(subcontract_move_id=True, mo_ids_to_backorder=production_ids_backorder).button_mark_done()
+ # For concistency, set the date on production move before the date
+ # on picking. (Traceability report + Product Moves menu item)
+ minimum_date = min(picking.move_line_ids.mapped('date'))
+ production_moves = productions_to_done.move_raw_ids | productions_to_done.move_finished_ids
+ production_moves.write({'date': minimum_date - timedelta(seconds=1)})
+ production_moves.move_line_ids.write({'date': minimum_date - timedelta(seconds=1)})
return res
def action_record_components(self):
@@ -81,7 +90,9 @@ class StockPicking(models.Model):
for move in self.move_lines:
if not move._has_tracked_subcontract_components():
continue
- production = move.move_orig_ids.production_id[-1:]
+ productions = move.move_orig_ids.production_id
+ productions_to_done = productions._subcontracting_filter_to_done()
+ production = (productions - productions_to_done)[-1:]
if not production:
continue
return move._action_record_components()
@@ -94,8 +105,7 @@ class StockPicking(models.Model):
return self.picking_type_id.code == 'incoming' and any(m.is_subcontract for m in self.move_lines)
def _get_subcontracted_productions(self):
- self.ensure_one()
- return self.move_lines.mapped('move_orig_ids.production_id')
+ return self.move_lines.move_orig_ids.production_id
def _get_warehouse(self, subcontract_move):
return subcontract_move.warehouse_id or self.picking_type_id.warehouse_id
@@ -117,7 +127,8 @@ class StockPicking(models.Model):
'location_src_id': subcontract_move.picking_id.partner_id.with_company(subcontract_move.company_id).property_stock_subcontractor.id,
'location_dest_id': subcontract_move.picking_id.partner_id.with_company(subcontract_move.company_id).property_stock_subcontractor.id,
'product_qty': subcontract_move.product_uom_qty,
- 'picking_type_id': warehouse.subcontracting_type_id.id
+ 'picking_type_id': warehouse.subcontracting_type_id.id,
+ 'date_planned_start': subcontract_move.date
}
return vals
@@ -127,6 +138,7 @@ class StockPicking(models.Model):
mo = self.env['mrp.production'].with_company(move.company_id).create(self._prepare_subcontract_mo_vals(move, bom))
self.env['stock.move'].create(mo._get_moves_raw_values())
self.env['stock.move'].create(mo._get_moves_finished_values())
+ mo.date_planned_finished = move.date # Avoid to have the picking late depending of the MO
mo.action_confirm()
# Link the finished to the receipt move.
diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py
index 88e21407ff2..82ce42f86ab 100644
--- a/addons/mrp_subcontracting/tests/test_subcontracting.py
+++ b/addons/mrp_subcontracting/tests/test_subcontracting.py
@@ -416,10 +416,14 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_receipt.id)])
self.assertTrue(backorder)
self.assertEqual(backorder.move_lines.product_uom_qty, 2)
- subcontract_order = backorder.move_lines.move_orig_ids.production_id.filtered(lambda p: p.state != 'done')
- self.assertTrue(subcontract_order)
- self.assertEqual(subcontract_order.product_uom_qty, 5)
- self.assertEqual(subcontract_order.qty_produced, 3)
+ mo_done = backorder.move_lines.move_orig_ids.production_id.filtered(lambda p: p.state == 'done')
+ backorder_mo = backorder.move_lines.move_orig_ids.production_id.filtered(lambda p: p.state != 'done')
+ self.assertTrue(mo_done)
+ self.assertEqual(mo_done.qty_produced, 3)
+ self.assertEqual(mo_done.product_uom_qty, 3)
+ self.assertTrue(backorder_mo)
+ self.assertEqual(backorder_mo.product_uom_qty, 2)
+ self.assertEqual(backorder_mo.qty_produced, 0)
backorder.move_lines.quantity_done = 2
backorder._action_done()
self.assertTrue(picking_receipt.move_lines.move_orig_ids.production_id.state == 'done')
@@ -601,6 +605,52 @@ class TestSubcontractingTracking(TransactionCase):
self.assertEqual(avail_qty_comp2, -1)
self.assertEqual(avail_qty_finished, 1)
+ def test_flow_tracked_only_finished(self):
+ """ Test when only the finished product is tracked """
+ self.finished_product.tracking = "serial"
+ self.comp1_sn.tracking = "none"
+ nb_finished_product = 3
+ # 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_product
+ move.product_uom_qty = nb_finished_product
+ picking_receipt = picking_form.save()
+ picking_receipt.action_confirm()
+
+ # We shouldn't be able to call the 'record_components' button
+ self.assertFalse(picking_receipt.display_action_record_components)
+
+ wh = picking_receipt.picking_type_id.warehouse_id
+ lot_names_finished = [f"subtracked_{i}" for i in range(nb_finished_product)]
+
+ move_details = Form(picking_receipt.move_lines, view='stock.view_stock_move_nosuggest_operations')
+ for lot_name in lot_names_finished:
+ with move_details.move_line_nosuggest_ids.new() as ml:
+ ml.qty_done = 1
+ ml.lot_name = lot_name
+ move_details.save()
+
+ picking_receipt.button_validate()
+ # Check the created manufacturing order
+ # Should have one mo by serial number
+ mos = picking_receipt.move_lines.move_orig_ids.production_id
+ self.assertEqual(len(mos), nb_finished_product)
+ self.assertEqual(mos.mapped("state"), ["done"] * nb_finished_product)
+ self.assertEqual(mos.picking_type_id, wh.subcontracting_type_id)
+ self.assertFalse(mos.picking_type_id.active)
+ self.assertEqual(set(mos.lot_producing_id.mapped("name")), set(lot_names_finished))
+
+ # Available quantities should be negative at the subcontracting location for each components
+ avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
+ avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_subcontractor, allow_negative=True)
+ avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_product, wh.lot_stock_id)
+ self.assertEqual(avail_qty_comp1, -nb_finished_product)
+ self.assertEqual(avail_qty_comp2, -nb_finished_product)
+ self.assertEqual(avail_qty_finished, nb_finished_product)
+
def test_flow_tracked_backorder(self):
""" This test uses tracked (serial and lot) component and tracked (serial) finished product """
todo_nb = 4
diff --git a/addons/mrp_subcontracting/views/mrp_production_views.xml b/addons/mrp_subcontracting/views/mrp_production_views.xml
index 657bcaaf7e1..6284ae858ec 100644
--- a/addons/mrp_subcontracting/views/mrp_production_views.xml
+++ b/addons/mrp_subcontracting/views/mrp_production_views.xml
@@ -26,6 +26,9 @@
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
+ <xpath expr="//field[@name='priority']" position="attributes">
+ <attribute name="invisible">1</attribute>
+ </xpath>
<xpath expr="//field[@name='bom_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>