oca-technical/odoo-bringout-oca-ddmrp-ddmrp/ddmrp/models/stock_move.py
2025-08-29 15:43:03 +02:00

152 lines
5.8 KiB
Python

# Copyright 2019-20 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, fields, models
class StockMove(models.Model):
_inherit = "stock.move"
buffer_ids = fields.Many2many(
comodel_name="stock.buffer",
string="Linked Stock Buffers",
)
# Add an index as '_find_buffer_link' method is using it as search criteria
created_purchase_line_id = fields.Many2one(index=True)
def _prepare_procurement_values(self):
res = super(StockMove, self)._prepare_procurement_values()
if self.buffer_ids:
res["buffer_ids"] = self.buffer_ids
return res
def _merge_moves_fields(self):
res = super(StockMove, self)._merge_moves_fields()
res["buffer_ids"] = [(4, m.id) for m in self.mapped("buffer_ids")]
return res
def write(self, vals):
res = super(StockMove, self).write(vals)
if self and self.env.company.ddmrp_auto_update_nfp:
# Stock moves changes can be triggered by users without
# access to write stock buffers, thus we do it with sudo.
if "state" in vals:
self.sudo()._update_ddmrp_nfp()
elif "location_id" in vals or "location_dest_id" in vals:
self.sudo().filtered(
lambda m: m.state
in ("confirmed", "partially_available", "assigned")
)._update_ddmrp_nfp()
return res
@api.model_create_multi
def create(self, vals_list):
moves = super(StockMove, self).create(vals_list)
# TODO should we use @api.model_create_single instead?
moves_to_update_ids = []
for vals, move in zip(vals_list, moves):
if (
"state" in vals
and move.state not in ("draft", "cancel")
and self.env.company.ddmrp_auto_update_nfp
):
moves_to_update_ids.append(move.id)
# Stock moves state changes can be triggered by users without
# access to write stock buffers, thus we do it with sudo.
if moves_to_update_ids:
self.browse(moves_to_update_ids).sudo()._update_ddmrp_nfp()
return moves
def _find_buffers_to_update_nfp(self):
# Find buffers that can be affected. `out_buffers` will see the move as
# outgoing and `in_buffers` as incoming.
out_buffers = in_buffers = self.env["stock.buffer"]
for move in self:
out_buffers |= move.mapped("product_id.buffer_ids").filtered(
lambda buffer: (
move.location_id.is_sublocation_of(buffer.location_id)
and not move.location_dest_id.is_sublocation_of(buffer.location_id)
)
)
in_buffers |= move.mapped("product_id.buffer_ids").filtered(
lambda buffer: (
not move.location_id.is_sublocation_of(buffer.location_id)
and move.location_dest_id.is_sublocation_of(buffer.location_id)
)
)
return out_buffers, in_buffers
def _update_ddmrp_nfp(self):
if self.env.context.get("no_ddmrp_auto_update_nfp"):
return True
out_buffers, in_buffers = self._find_buffers_to_update_nfp()
for buffer in out_buffers.with_context(no_ddmrp_history=True):
buffer.cron_actions(only_nfp="out")
for buffer in in_buffers.with_context(no_ddmrp_history=True):
buffer.cron_actions(only_nfp="in")
def _get_all_linked_moves(self):
"""Retrieve all linked moves both origin and destination recursively."""
def get_moves(move_set, attr):
new_moves = move_set.mapped(attr)
while new_moves:
move_set |= new_moves
new_moves = new_moves.mapped(attr)
return move_set
all_moves = (
self | get_moves(self, "move_orig_ids") | get_moves(self, "move_dest_ids")
)
return all_moves
def _get_source_field_candidates(self):
"""Extend for more source field candidates."""
return [
"sale_line_id.order_id",
"purchase_line_id.order_id",
"production_id",
"raw_material_production_id",
"unbuild_id",
"repair_id",
"rma_line_id",
"picking_id",
]
def _has_nested_field(self, field):
"""Check if an object has a nested chain of fields."""
current_object = self
try:
for field in field.split("."):
current_object = getattr(current_object, field)
return True
except AttributeError:
return False
def _get_source_record(self):
"""Find the first source record in the field candidates linked with the moves,
prioritizing the order of field candidates."""
moves = self._get_all_linked_moves()
field_candidates = self._get_source_field_candidates()
# Iterate over the prioritized list of candidate fields
for field in field_candidates:
if self._has_nested_field(field):
for move in moves:
record = move.mapped(field)
if record:
return record
return False
def action_open_stock_move_source(self):
"""Open the source record of the stock move, if it exists."""
self.ensure_one()
record = self._get_source_record()
if record:
return {
"name": getattr(record, "name", _("Stock Move Source")),
"view_mode": "form",
"res_model": record._name,
"type": "ir.actions.act_window",
"res_id": record.id,
}
return False