From 176b5fdd0284b3f6433571c45c92570bdabc077d Mon Sep 17 00:00:00 2001 From: BernatPForgeFlow Date: Tue, 21 Nov 2023 08:30:53 +0100 Subject: [PATCH] [IMP] ddmrp: Add customized Stock Moves view When checking incoming quantities or qualified demand moves, we will use a customized view that allows to redirect to the source document of that move. A stock move can be chained with many other moves and can have two sources, like a dropship. Then, to calculate the source of that stock move, a list of fields ordered by priority is followed, in which if any record is found in any of them, it is returned as a source. For example, if we have a stock move that belongs to a dropship with related sale and purchase, it will always return the sale as source because its the first field in the list. The sources field method is extensible to add any others that are needed. --- ddmrp/models/stock_buffer.py | 97 ++++++++++++++----------------- ddmrp/models/stock_move.py | 68 +++++++++++++++++++++- ddmrp/views/stock_buffer_view.xml | 55 ++++++++++++------ ddmrp/views/stock_move_views.xml | 11 ++-- 4 files changed, 153 insertions(+), 78 deletions(-) diff --git a/ddmrp/models/stock_buffer.py b/ddmrp/models/stock_buffer.py index 11f7c0639..b19427937 100644 --- a/ddmrp/models/stock_buffer.py +++ b/ddmrp/models/stock_buffer.py @@ -1370,17 +1370,6 @@ def _stock_move_tree_view(self, lines): "domain": str([("id", "in", lines.ids)]), } - def open_moves(self): - self.ensure_one() - # Utility method used to add an "Open Moves" button in the buffer - # planning view - domain = self._search_open_stock_moves_domain() - moves = self.env["stock.move"].search(domain) - moves = moves.filtered( - lambda move: move.location_dest_id.is_sublocation_of(self.location_id) - ) - return self._stock_move_tree_view(moves) - def _get_horizon_adu_past_demand(self): return self.adu_calculation_method.horizon_past or 0 @@ -1828,72 +1817,72 @@ def do_auto_procure(self): wizard.make_procurement() return True - def _search_purchase_order_lines_incoming(self, outside_dlt=False): + def action_view_supply_moves(self): + result = self.env["ir.actions.actions"]._for_xml_id("stock.stock_move_action") + result["context"] = {} + moves = self._search_stock_moves_incoming() + self._search_stock_moves_incoming( + outside_dlt=True + ) + result["domain"] = [("id", "in", moves.ids)] + return result + + def _get_rfq_dlt(self, outside_dlt=False): + self.ensure_one() cut_date = self._get_incoming_supply_date_limit() if not outside_dlt: pols = self.purchase_line_ids.filtered( lambda l: l.date_planned <= fields.Datetime.to_datetime(cut_date) - and l.order_id.state in ("draft", "sent") + and l.state in ("draft", "sent") ) else: pols = self.purchase_line_ids.filtered( lambda l: l.date_planned > fields.Datetime.to_datetime(cut_date) - and l.order_id.state in ("draft", "sent") + and l.state in ("draft", "sent") ) return pols - def action_view_supply(self, outside_dlt=False): - if self.item_type == "purchased": - pols = self._search_purchase_order_lines_incoming(outside_dlt) - moves = self._search_stock_moves_incoming(outside_dlt) - while moves.mapped("move_orig_ids"): - moves = moves.mapped("move_orig_ids") - pos = pols.mapped("order_id") + moves.mapped("purchase_line_id.order_id") - result = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq") - # Remove the context since the action display RFQ and not PO. - result["context"] = {} - result["domain"] = [("id", "in", pos.ids)] - elif self.item_type == "manufactured": - moves = self._search_stock_moves_incoming(outside_dlt) - mos = moves.mapped("production_id") - result = self.env["ir.actions.actions"]._for_xml_id( - "mrp.mrp_production_action" - ) - result["context"] = {} - result["domain"] = [("id", "in", mos.ids)] - else: - moves = self._search_stock_moves_incoming(outside_dlt) - picks = moves.mapped("picking_id") - result = self.env["ir.actions.actions"]._for_xml_id( - "stock.action_picking_tree_all" - ) - result["context"] = {} - result["domain"] = [("id", "in", picks.ids)] + def action_view_supply_moves_inside_dlt_window(self): + result = self.env["ir.actions.actions"]._for_xml_id("stock.stock_move_action") + moves = self._search_stock_moves_incoming() + result["context"] = {} + result["domain"] = [("id", "in", moves.ids)] return result - def action_view_supply_inside_dlt_window(self): - return self.action_view_supply() + def action_view_supply_moves_outside_dlt_window(self): + result = self.env["ir.actions.actions"]._for_xml_id("stock.stock_move_action") + moves = self._search_stock_moves_incoming(outside_dlt=True) + result["context"] = {} + result["domain"] = [("id", "in", moves.ids)] + return result - def action_view_supply_outside_dlt_window(self): - return self.action_view_supply(outside_dlt=True) + def action_view_supply_rfq_inside_dlt_window(self): + result = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq") + pols = self._get_rfq_dlt() + pos = pols.mapped("order_id") + result["context"] = {} + result["domain"] = [("id", "in", pos.ids)] + return result - def action_view_qualified_demand_pickings(self): - moves = self.qualified_demand_stock_move_ids - picks = moves.mapped("picking_id") - result = self.env["ir.actions.actions"]._for_xml_id( - "stock.action_picking_tree_all" - ) + def action_view_supply_rfq_outside_dlt_window(self): + result = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq") + pols = self._get_rfq_dlt(outside_dlt=True) + pos = pols.mapped("order_id") result["context"] = {} - result["domain"] = [("id", "in", picks.ids)] + result["domain"] = [("id", "in", pos.ids)] + return result + + def action_view_qualified_demand_moves(self): + result = self.env["ir.actions.actions"]._for_xml_id("stock.stock_move_action") + result["context"] = {} + result["domain"] = [("id", "in", self.qualified_demand_stock_move_ids.ids)] return result def action_view_qualified_demand_mrp(self): - mrp_moves = self.qualified_demand_mrp_move_ids result = self.env["ir.actions.actions"]._for_xml_id( "mrp_multi_level.mrp_move_action" ) result["context"] = {} - result["domain"] = [("id", "in", mrp_moves.ids)] + result["domain"] = [("id", "in", self.qualified_demand_mrp_move_ids.ids)] return result def action_view_past_adu_direct_demand(self): diff --git a/ddmrp/models/stock_move.py b/ddmrp/models/stock_move.py index 29106999a..5ef08b600 100644 --- a/ddmrp/models/stock_move.py +++ b/ddmrp/models/stock_move.py @@ -1,7 +1,7 @@ # 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 +from odoo import _, api, fields, models class StockMove(models.Model): @@ -84,3 +84,69 @@ def _update_ddmrp_nfp(self): 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 diff --git a/ddmrp/views/stock_buffer_view.xml b/ddmrp/views/stock_buffer_view.xml index 5b8966758..98d3252bd 100644 --- a/ddmrp/views/stock_buffer_view.xml +++ b/ddmrp/views/stock_buffer_view.xml @@ -38,24 +38,19 @@ type="object" /> -