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 closes odoo/odoo#62109 X-original-commit: 1a34e46eaca14aec981dd0f162e8014c7cb0abea Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com> Signed-off-by: Rémy Voet <ryv-odoo@users.noreply.github.com>
208 lines
9.4 KiB
Python
208 lines
9.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
|
|
from odoo import fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.float_utils import float_compare, float_is_zero
|
|
|
|
|
|
class StockMove(models.Model):
|
|
_inherit = 'stock.move'
|
|
|
|
is_subcontract = fields.Boolean('The move is a subcontract receipt')
|
|
show_subcontracting_details_visible = fields.Boolean(
|
|
compute='_compute_show_subcontracting_details_visible'
|
|
)
|
|
|
|
def _compute_show_subcontracting_details_visible(self):
|
|
""" Compute if the action button in order to see moves raw is visible """
|
|
for move in self:
|
|
if move.is_subcontract and move._has_tracked_subcontract_components() and\
|
|
not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding):
|
|
move.show_subcontracting_details_visible = True
|
|
else:
|
|
move.show_subcontracting_details_visible = False
|
|
|
|
def _compute_show_details_visible(self):
|
|
""" If the move is subcontract and the components are tracked. Then the
|
|
show details button is visible.
|
|
"""
|
|
res = super(StockMove, self)._compute_show_details_visible()
|
|
for move in self:
|
|
if not move.is_subcontract:
|
|
continue
|
|
if not move._has_tracked_subcontract_components():
|
|
continue
|
|
move.show_details_visible = True
|
|
return res
|
|
|
|
def copy(self, default=None):
|
|
self.ensure_one()
|
|
if not self.is_subcontract or 'location_id' in default:
|
|
return super(StockMove, self).copy(default=default)
|
|
if not default:
|
|
default = {}
|
|
default['location_id'] = self.picking_id.location_id.id
|
|
return super(StockMove, self).copy(default=default)
|
|
|
|
def write(self, values):
|
|
""" If the initial demand is updated then also update the linked
|
|
subcontract order to the new quantity.
|
|
"""
|
|
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'])
|
|
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
|
|
subcontracted product. Otherwise use standard behavior.
|
|
"""
|
|
self.ensure_one()
|
|
if self.is_subcontract:
|
|
rounding = self.product_uom.rounding
|
|
production = self.move_orig_ids.production_id[-1:]
|
|
if self._has_tracked_subcontract_components() and\
|
|
float_compare(production.qty_produced, production.product_uom_qty, precision_rounding=rounding) < 0 and\
|
|
float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) < 0:
|
|
return self._action_record_components()
|
|
action = super(StockMove, self).action_show_details()
|
|
if self.is_subcontract and self._has_tracked_subcontract_components():
|
|
action['views'] = [(self.env.ref('stock.view_stock_move_operations').id, 'form')]
|
|
action['context'].update({
|
|
'show_lots_m2o': self.has_tracking != 'none',
|
|
'show_lots_text': False,
|
|
})
|
|
return action
|
|
|
|
def action_show_subcontract_details(self):
|
|
""" Display moves raw for subcontracted product self. """
|
|
moves = self.move_orig_ids.production_id.move_raw_ids
|
|
tree_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_tree_view')
|
|
form_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view')
|
|
ctx = dict(self._context, search_default_by_product=True, subcontract_move_id=self.id)
|
|
return {
|
|
'name': _('Raw Materials for %s') % (self.product_id.display_name),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'stock.move',
|
|
'views': [(tree_view.id, 'list'), (form_view.id, 'form')],
|
|
'target': 'current',
|
|
'domain': [('id', 'in', moves.ids)],
|
|
'context': ctx
|
|
}
|
|
|
|
def _action_cancel(self):
|
|
for move in self:
|
|
if move.is_subcontract:
|
|
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 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):
|
|
subcontract_details_per_picking = defaultdict(list)
|
|
for move in self:
|
|
if move.location_id.usage != 'supplier' or move.location_dest_id.usage == 'supplier':
|
|
continue
|
|
if move.move_orig_ids.production_id:
|
|
continue
|
|
bom = move._get_subcontract_bom()
|
|
if not bom:
|
|
continue
|
|
if float_is_zero(move.product_qty, precision_rounding=move.product_uom.rounding) and\
|
|
move.picking_id.immediate_transfer is True:
|
|
raise UserError(_("To subcontract, use a planned transfer."))
|
|
subcontract_details_per_picking[move.picking_id].append((move, bom))
|
|
move.write({
|
|
'is_subcontract': True,
|
|
'location_id': move.picking_id.partner_id.with_company(move.company_id).property_stock_subcontractor.id
|
|
})
|
|
for picking, subcontract_details in subcontract_details_per_picking.items():
|
|
picking._subcontracted_produce(subcontract_details)
|
|
|
|
res = super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into)
|
|
if subcontract_details_per_picking:
|
|
self.env['stock.picking'].concat(*list(subcontract_details_per_picking.keys())).action_assign()
|
|
return res
|
|
|
|
def _action_record_components(self):
|
|
self.ensure_one()
|
|
production = self.move_orig_ids.production_id[-1:]
|
|
view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_form_view')
|
|
return {
|
|
'name': _('Subcontract'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_model': 'mrp.production',
|
|
'views': [(view.id, 'form')],
|
|
'view_id': view.id,
|
|
'target': 'new',
|
|
'res_id': production.id,
|
|
'context': dict(self.env.context, subcontract_move_id=self.id),
|
|
}
|
|
|
|
def _get_subcontract_bom(self):
|
|
self.ensure_one()
|
|
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
|
|
product=self.product_id,
|
|
picking_type=self.picking_type_id,
|
|
company_id=self.company_id.id,
|
|
bom_type='subcontract',
|
|
subcontractor=self.picking_id.partner_id,
|
|
)
|
|
return bom
|
|
|
|
def _has_tracked_subcontract_components(self):
|
|
self.ensure_one()
|
|
return any(m.has_tracking != 'none' for m in self.move_orig_ids.production_id.move_raw_ids)
|
|
|
|
def _prepare_extra_move_vals(self, qty):
|
|
vals = super(StockMove, self)._prepare_extra_move_vals(qty)
|
|
vals['location_id'] = self.location_id.id
|
|
return vals
|
|
|
|
def _prepare_move_split_vals(self, qty):
|
|
vals = super(StockMove, self)._prepare_move_split_vals(qty)
|
|
vals['location_id'] = self.location_id.id
|
|
return vals
|
|
|
|
def _should_bypass_set_qty_producing(self):
|
|
if self.env.context.get('subcontract_move_id'):
|
|
return False
|
|
return super()._should_bypass_set_qty_producing()
|
|
|
|
def _should_bypass_reservation(self):
|
|
""" If the move is subcontracted then ignore the reservation. """
|
|
should_bypass_reservation = super(StockMove, self)._should_bypass_reservation()
|
|
if not should_bypass_reservation and self.is_subcontract:
|
|
return True
|
|
return should_bypass_reservation
|
|
|
|
def _update_subcontract_order_qty(self, new_quantity):
|
|
for move in self:
|
|
quantity_to_remove = move.product_uom_qty - new_quantity
|
|
productions = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))[::-1]
|
|
# Cancel productions until reach new_quantity
|
|
for production in productions:
|
|
if quantity_to_remove <= 0.0:
|
|
break
|
|
if quantity_to_remove >= production.product_qty:
|
|
quantity_to_remove -= production.product_qty
|
|
production.with_context(skip_activity=True).action_cancel()
|
|
else:
|
|
self.env['change.production.qty'].with_context(skip_activity=True).create({
|
|
'mo_id': production.id,
|
|
'product_qty': production.product_uom_qty - quantity_to_remove
|
|
}).change_prod_qty()
|