oca-ocb-mrp/odoo-bringout-oca-ocb-mrp_subcontracting/mrp_subcontracting/models/stock_move.py
Ernad Husremovic 6e65e8c877 19.0 vanilla
2026-03-09 09:31:47 +01:00

300 lines
15 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import fields, models, api, _
from odoo.exceptions import AccessError
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
from odoo.tools.misc import OrderedSet
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 """
self.show_subcontracting_details_visible = False
for move in self:
if not move.is_subcontract:
continue
if not move.move_line_ids or move.product_uom.is_zero(move.quantity):
continue
productions = move._get_subcontract_production().filtered(lambda m: m.state != 'cancel')
if not productions:
continue
move.show_subcontracting_details_visible = True
@api.depends('is_subcontract')
def _compute_show_info(self):
super()._compute_show_info()
subcontract_moves = self.filtered(lambda m: m.is_subcontract and m.show_lots_text)
subcontract_moves.show_lots_text = False
subcontract_moves.show_lots_m2o = True
@api.depends('is_subcontract', 'has_tracking')
def _compute_is_quantity_done_editable(self):
done_moves = self.env['stock.move']
for move in self:
if move.is_subcontract:
move.is_quantity_done_editable = move.has_tracking == 'none'
done_moves |= move
return super(StockMove, self - done_moves)._compute_is_quantity_done_editable()
def copy_data(self, default=None):
default = dict(default or {})
vals_list = super().copy_data(default=default)
for move, vals in zip(self, vals_list):
if 'location_id' in default or not move.is_subcontract:
continue
vals['location_id'] = move.picking_id.location_id.id
return vals_list
def write(self, vals):
""" If the initial demand is updated then also update the linked
subcontract order to the new quantity.
"""
self._check_access_if_subcontractor(vals)
res = super().write(vals)
if 'date' in vals:
for move in self:
if move.state in ('done', 'cancel') or not move.is_subcontract:
continue
move.move_orig_ids.production_id.with_context(from_subcontract=True).filtered(lambda p: p.state not in ('done', 'cancel')).write({
'date_start': move.date,
'date_finished': move.date,
})
return res
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._check_access_if_subcontractor(vals)
return super().create(vals_list)
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:
action = super(StockMove, self.with_context(force_lot_m2o=True)).action_show_details()
if self.env.user._is_portal():
action['views'] = [(self.env.ref('mrp_subcontracting.mrp_subcontracting_view_stock_move_operations').id, 'form')]
return action
return super().action_show_details()
def action_show_subcontract_details(self, lot_id=None):
""" Display moves raw for subcontracted product self. """
productions = self._get_subcontract_production().filtered(lambda m: m.state != 'cancel')
if lot_id is not None:
if lot_id:
productions = productions.filtered(lambda p: p.lot_producing_ids and p.lot_producing_ids[0] == self.env['stock.lot'].browse(lot_id))
else:
productions = productions.filtered(lambda p: not p.lot_producing_ids)
ctx = {"mrp_subcontracting": True}
if self.env.user._is_portal():
form_view_id = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_portal_form_view')
ctx.update(no_breadcrumbs=False)
else:
form_view_id = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_form_view')
action = {
'type': 'ir.actions.act_window',
'res_model': 'mrp.production',
'target': 'current',
'context': ctx
}
if len(productions) > 1:
action.update({
'name': _('Subcontracting MOs'),
'views': [
(self.env.ref('mrp_subcontracting.mrp_production_subcontracting_tree_view').id, 'list'),
(form_view_id.id, 'form'),
],
'domain': [('id', 'in', productions.ids)],
})
else:
action.update({
'views': [(form_view_id.id, 'form')],
'res_id': productions.id,
})
return action
def _action_cancel(self):
productions_to_cancel_ids = OrderedSet()
for move in self:
if move.is_subcontract:
active_productions = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
moves_todo = self.env.context.get('moves_todo')
not_todo_productions = active_productions.filtered(lambda p: p not in moves_todo.move_orig_ids.production_id) if moves_todo else active_productions
if not_todo_productions:
productions_to_cancel_ids.update(not_todo_productions.ids)
if productions_to_cancel_ids:
productions_to_cancel = self.env['mrp.production'].browse(productions_to_cancel_ids)
productions_to_cancel.with_context(skip_activity=True).action_cancel()
return super()._action_cancel()
def _action_confirm(self, merge=True, merge_into=False, create_proc=True):
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
company = move.company_id
subcontracting_location = \
move.picking_id.partner_id.with_company(company).property_stock_subcontractor \
or company.subcontracting_location_id
move.write({
'is_subcontract': True,
'location_id': subcontracting_location.id
})
move._action_assign() # Re-reserve as the write on location_id will break the link
res = super()._action_confirm(merge=merge, merge_into=merge_into, create_proc=create_proc)
for move in res:
if move.is_subcontract:
subcontract_details_per_picking[move.picking_id].append((move, move._get_subcontract_bom()))
for picking, subcontract_details in subcontract_details_per_picking.items():
picking._subcontracted_produce(subcontract_details)
if subcontract_details_per_picking:
self.env['stock.picking'].concat(*list(subcontract_details_per_picking.keys())).action_assign()
return res
def _get_subcontract_bom(self):
self.ensure_one()
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
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 _get_subcontract_production(self):
return self.filtered(lambda m: m.is_subcontract).move_orig_ids.production_id
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 _prepare_procurement_values(self):
res = super()._prepare_procurement_values()
if self.raw_material_production_id.subcontractor_id:
res['warehouse_id'] = self.picking_type_id.warehouse_id
return res
def _should_bypass_reservation(self, forced_location=False):
""" If the move is subcontracted then ignore the reservation. """
should_bypass_reservation = super()._should_bypass_reservation(forced_location=forced_location)
if not should_bypass_reservation and self.is_subcontract:
return True
return should_bypass_reservation
def _get_available_move_lines(self, assigned_moves_ids, partially_available_moves_ids):
return super(StockMove, self.filtered(lambda m: not m.is_subcontract))._get_available_move_lines(assigned_moves_ids, partially_available_moves_ids)
def _check_access_if_subcontractor(self, vals):
if self.env.user._is_portal() and not self.env.su:
if vals.get('state') == 'done':
raise AccessError(_("Portal users cannot create a stock move with a state 'Done' or change the current state to 'Done'."))
def _is_subcontract_return(self):
self.ensure_one()
subcontracting_location = self.picking_id.partner_id.with_company(self.company_id).property_stock_subcontractor
return (
not self.is_subcontract
and self.origin_returned_move_id.is_subcontract
and self.location_dest_id.id == subcontracting_location.id
)
def _can_create_lot(self):
return super()._can_create_lot() or self.env.context.get('force_lot_m2o')
def _sync_subcontracting_productions(self):
"""
Enforce the relationship between subcontracting receipt moves and their respective subcontracting productions.
* For untracked moves:
* There will always be only 1 production.
* Updating the move quantity will update the production quantity.
* For tracked moves:
* There will be 1 production for every lot on this move.
* This method will enforce the synchronisation between the total quantity per lot on the move and the linked productions.
* The split mechanism for productions will be used to create new subcontracting MOs.
* We take care to always keep at least 1 subcontracting production linked to the subcontracting receipt.
This ensures there will always be a production available for splitting.
"""
for move in self:
productions = move._get_subcontract_production()
if not productions:
continue
if move.has_tracking == 'none':
if productions.product_uom_id.compare(productions.product_qty, move.quantity) != 0:
self.sudo().env['change.production.qty'].with_context(skip_activity=True).create([{
'mo_id': productions.id,
'product_qty': move.quantity or move.product_uom_qty,
}]).change_prod_qty()
productions.action_assign()
else:
qty_by_lot = dict(move.move_line_ids._read_group([('move_id', '=', move.id)], ['lot_id'], ['quantity_product_uom:sum']))
mos_to_assign = self.env['mrp.production']
# 1. Ensure quantities of linked MOs still match the quantities on the move
mos_to_create = {} # lot -> qty
for lot_id, ml_qty in qty_by_lot.items():
lot_mo = productions.filtered(lambda p: (p.lot_producing_ids and p.lot_producing_ids[0] == lot_id) or (not lot_id and not p.lot_producing_ids))
if not lot_mo:
mos_to_create[lot_id] = ml_qty
elif lot_mo.product_uom_id.compare(lot_mo.product_qty, ml_qty) != 0:
self.sudo().env['change.production.qty'].with_context(skip_activity=True).create([{
'mo_id': lot_mo.id,
'product_qty': ml_qty
}]).change_prod_qty()
mos_to_assign |= lot_mo
# 2. Create new MOs where needed, by splitting them from an existing subcontracting MO
if mos_to_create:
production_to_split = move._get_subcontract_production()[0]
new_mos = production_to_split.sudo().with_context(allow_more=True, mrp_subcontracting=False)._split_productions({
production_to_split: [production_to_split.product_qty] + list(mos_to_create.values())
}, cancel_remaining_qty=True)[1:]
mos_to_assign |= new_mos
for mo, lot_id in zip(new_mos, mos_to_create.keys()):
mo.lot_producing_ids = lot_id
# 3. Delete 'orphan' MOs with lot not linked to any move line
productions = move._get_subcontract_production()
orphan_productions = productions.filtered(lambda p: (p.lot_producing_ids and p.lot_producing_ids[0] not in qty_by_lot) or (not p.lot_producing_ids and self.env['stock.lot'] not in qty_by_lot))
if len(productions) == len(orphan_productions):
# Make sure not to delete all MOs, leave 1 subcontracting MO as 'open' MO for splitting later
production_to_keep = orphan_productions[-1]
production_to_keep.lot_producing_ids = False
orphan_productions = orphan_productions[:-1]
if orphan_productions:
orphan_productions.sudo().with_context(skip_activity=True).unlink()
productions -= orphan_productions
mos_to_assign.sudo().action_assign()
def _generate_serial_numbers(self, next_serial, next_serial_count=False, location_id=False):
if self.is_subcontract:
return super(StockMove, self.with_context(force_lot_m2o=True))._generate_serial_numbers(next_serial, next_serial_count, location_id)
return super()._generate_serial_numbers(next_serial, next_serial_count, location_id)
def _get_partner_id(self):
if self.raw_material_production_id.subcontractor_id:
route = self.env.ref('mrp_subcontracting.route_resupply_subcontractor_mto', raise_if_not_found=False)
if route and self.rule_id.route_id == route:
return self.raw_material_production_id.subcontractor_id.id
return super()._get_partner_id()