In case of a MRP subcontracting, it is not possible to process the
delivery with several backorders.
To reproduce the issue:
1. Create two products P_compo, P_finished
- Both storable
- P_compo must have the route "Resupply Subcontractor on Order"
2. Update P_compo's quantity: 5
3. Create a BoM:
- Product: P_finished
- BoM type: Subcontracting
- Subcontractors: a partner P
- Components: 1 x P_compo
4. In Inventory, create a planned transfer T:
- Operation Type: Receipt
- Receive From: P
- Operations: 5 x P_finished
5. Mark as Todo
6. Inventory > Resupply Subcontractor, find the delivery of P_compo for
P and process it
7. Back to T, set the done quantity to 1.25
8. Validate T (with backorder)
9. On the backorder BO1, set the done quantity to 1.0
10. Validate BO1 (with backorder)
11. Open the second backorder BO2 and validate it
Error: a Validation Error is displayed "You can not enter negative
quantities.", which doesn't make sense
The issue comes from the inner method `_get_available_move_lines` in
`_action_assign`. When validating a picking, at some point, we are here,
at the beginning of `/mrp_subcontracting._action_done`:
https://github.com/odoo/odoo/blob/6d6b2c41adfa842c82252373b3dbd1ed0a5ebc92/addons/mrp_subcontracting/models/stock_picking.py#L36-L37
To understand what happens next, we need to note that, in this method,
the associated MO will be marked as done later (on line 76). Back to the
current line (L37), we are calling `super` which leads to the
problematic `_get_available_move_lines`. Suppose we are on step 10 (we
are validating the first backorder), we have
https://github.com/odoo/odoo/blob/58a9f57c0827a51e46ef0f52a3d48ba343667fe0/addons/stock/models/stock_move.py#L1390-L1391
`move_orig_ids` contains two stock moves, each one associated to one MO.
However, as said above, the second MO is not yet marked as done, this
will be done later on in `/mrp_subcontracting._action_done`. Therefore,
`move_lines_in` is only defined with the first SM, from the first MO,
with a quantity of `1.25`
Further in `_get_available_move_lines`, we have:
https://github.com/odoo/odoo/blob/58a9f57c0827a51e46ef0f52a3d48ba343667fe0/addons/stock/models/stock_move.py#L1403-L1405
`move.move_orig_ids.mapped('move_dest_ids') - move` gives two SM, and
here is the issue: the second one is already processed, so both are kept
in `move_lines_out_done` and the sum of their quantity is equal to
`1.25 + 1 = 2.25`
Therefore, at the end of the method, when computing the difference, it
gives a negative difference. This negative value will be used to create
a SML and later on, it will trigger the validation error.
Once the above issue is fixed, there is another "invisible" issue that
need to be fixed: when generating several backorders on the deliveries,
only the first one will trigger the creation of a new MO. Suppose we are
back in `/mrp_subcontracting._action_done` (still on step 10 in the
above case), in the first for-loop:
https://github.com/odoo/odoo/blob/6d6b2c41adfa842c82252373b3dbd1ed0a5ebc92/addons/mrp_subcontracting/models/stock_picking.py#L39-L43
`move._get_subcontract_production()` returns the two MO (from the
initial picking and from the first backorder). However, since the first
one has already been processed, the if-condition is validated.
Therefore, this for-loop is done and the second MO is not processed (the
quantities are not recorded and its field
`subcontracting_has_been_recorded` is not defined to `True`). As a
result, still in `/mrp_subcontracting._action_done`, we are now in the
second for-loop:
https://github.com/odoo/odoo/blob/6d6b2c41adfa842c82252373b3dbd1ed0a5ebc92/addons/mrp_subcontracting/models/stock_picking.py#L71-L72
`_subcontracting_filter_to_done` removes the two MOs because the first
one is done and the second one has not its field
`subcontracting_has_been_recorded` set to `True`. The condition in the
first for-loop is not accurate enough.
OPW-2731658
closes odoo/odoo#83658
X-original-commit: 1b740ae148acfc832b124122763817b08ea110ab
Signed-off-by: Arnold Moyaux <arm@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
150 lines
8.2 KiB
Python
150 lines
8.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import timedelta
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.float_utils import float_compare
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
|
|
class StockPicking(models.Model):
|
|
_inherit = 'stock.picking'
|
|
|
|
display_action_record_components = fields.Selection(
|
|
[('hide', 'Hide'), ('facultative', 'Facultative'), ('mandatory', 'Mandatory')],
|
|
compute='_compute_display_action_record_components')
|
|
|
|
@api.depends('state', 'move_ids')
|
|
def _compute_display_action_record_components(self):
|
|
self.display_action_record_components = 'hide'
|
|
for picking in self:
|
|
# Hide if not encoding state or it is not a subcontracting picking
|
|
if picking.state in ('draft', 'cancel', 'done') or not picking._is_subcontract():
|
|
continue
|
|
subcontracted_moves = picking.move_ids.filtered(lambda m: m.is_subcontract)
|
|
if subcontracted_moves._subcontrating_should_be_record():
|
|
picking.display_action_record_components = 'mandatory'
|
|
continue
|
|
if subcontracted_moves._subcontrating_can_be_record():
|
|
picking.display_action_record_components = 'facultative'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Action methods
|
|
# -------------------------------------------------------------------------
|
|
def _action_done(self):
|
|
res = super(StockPicking, self)._action_done()
|
|
for move in self.move_ids.filtered(lambda move: move.is_subcontract):
|
|
# Auto set qty_producing/lot_producing_id of MO wasn't recorded
|
|
# manually (if the flexible + record_component or has tracked component)
|
|
productions = move._get_subcontract_production()
|
|
recorded_productions = productions.filtered(lambda p: p._has_been_recorded())
|
|
recorded_qty = sum(recorded_productions.mapped('qty_producing'))
|
|
sm_done_qty = sum(productions._get_subcontract_move().mapped('quantity_done'))
|
|
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
if float_compare(recorded_qty, sm_done_qty, precision_digits=rounding) >= 0:
|
|
continue
|
|
production = productions - recorded_productions
|
|
if not production:
|
|
continue
|
|
if len(production) > 1:
|
|
raise UserError("It shouldn't happen to have multiple production to record for the same subcontracted move")
|
|
# 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()
|
|
production.subcontracting_has_been_recorded = True
|
|
if move_line != move.move_line_ids[-1]:
|
|
backorder = production._split_productions()[1:]
|
|
# 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:
|
|
productions_to_done = picking._get_subcontract_production()._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(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):
|
|
self.ensure_one()
|
|
move_subcontracted = self.move_ids.filtered(lambda m: m.is_subcontract)
|
|
for move in move_subcontracted:
|
|
production = move._subcontrating_should_be_record()
|
|
if production:
|
|
return move._action_record_components()
|
|
for move in move_subcontracted:
|
|
production = move._subcontrating_can_be_record()
|
|
if production:
|
|
return move._action_record_components()
|
|
raise UserError(_("Nothing to record"))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Subcontract helpers
|
|
# -------------------------------------------------------------------------
|
|
def _is_subcontract(self):
|
|
self.ensure_one()
|
|
return self.picking_type_id.code == 'incoming' and any(m.is_subcontract for m in self.move_ids)
|
|
|
|
def _get_subcontract_production(self):
|
|
return self.move_ids._get_subcontract_production()
|
|
|
|
def _get_warehouse(self, subcontract_move):
|
|
return subcontract_move.warehouse_id or self.picking_type_id.warehouse_id or subcontract_move.move_dest_ids.picking_type_id.warehouse_id
|
|
|
|
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
|
|
subcontract_move.ensure_one()
|
|
group = self.env['procurement.group'].create({
|
|
'name': self.name,
|
|
'partner_id': self.partner_id.id,
|
|
})
|
|
product = subcontract_move.product_id
|
|
warehouse = self._get_warehouse(subcontract_move)
|
|
vals = {
|
|
'company_id': subcontract_move.company_id.id,
|
|
'procurement_group_id': group.id,
|
|
'product_id': product.id,
|
|
'product_uom_id': subcontract_move.product_uom.id,
|
|
'bom_id': bom.id,
|
|
'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,
|
|
'date_planned_start': subcontract_move.date - relativedelta(days=product.produce_delay)
|
|
}
|
|
return vals
|
|
|
|
def _subcontracted_produce(self, subcontract_details):
|
|
self.ensure_one()
|
|
for move, bom in subcontract_details:
|
|
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.
|
|
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
|
|
finished_move.write({'move_dest_ids': [(4, move.id, False)]})
|
|
mo.action_assign()
|