Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14.0][IMP] stock_buffer_route: Add alternative item type in buffer #308

Open
wants to merge 2 commits into
base: 14.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions stock_buffer_route/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import mrp_production
from . import purchase_order
from . import stock_buffer
42 changes: 42 additions & 0 deletions stock_buffer_route/models/mrp_production.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).

from odoo import models


class MrpProduction(models.Model):
_inherit = "mrp.production"

def _get_domain_buffer_link_alternative(self, warehouse_level=False):
self.ensure_one()
if not warehouse_level:
locations = self.env["stock.location"].search(
[("id", "child_of", [self.location_dest_id.id])]
)
return [
("product_id", "=", self.product_id.id),
("company_id", "=", self.company_id.id),
("item_type_alternative", "=", "manufactured"),
("location_id", "in", locations.ids),
]
else:
return [
("product_id", "=", self.product_id.id),
("company_id", "=", self.company_id.id),
("item_type_alternative", "=", "manufactured"),
("warehouse_id", "=", self.picking_type_id.warehouse_id.id),
]

def _find_buffer_link(self):
res = super()._find_buffer_link()
buffer_model = self.env["stock.buffer"]
for rec in self.filtered(lambda r: not r.buffer_id):
domain = rec._get_domain_buffer_link_alternative()
buffer = buffer_model.search(domain, limit=1)
if not buffer:
domain = rec._get_domain_buffer_link_alternative(warehouse_level=True)
buffer = buffer_model.search(domain, limit=1)
rec.buffer_id = buffer
if buffer:
rec._calc_execution_priority()
return res
38 changes: 38 additions & 0 deletions stock_buffer_route/models/purchase_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).

from odoo import models


class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"

def _get_domain_buffer_link_alternative(self):
self.ensure_one()
if not self.product_id:
# Return impossible domain -> no buffer.
return [(0, "=", 1)]
return [
("product_id", "=", self.product_id.id),
("company_id", "=", self.order_id.company_id.id),
("item_type_alternative", "=", "purchased"),
("warehouse_id", "=", self.order_id.picking_type_id.warehouse_id.id),
]

def _find_buffer_link(self):
res = super()._find_buffer_link()
buffer_model = self.env["stock.buffer"]
move_model = self.env["stock.move"]
for rec in self.filtered(lambda r: not r.buffer_ids):
mto_move = move_model.search(
[("created_purchase_line_id", "=", rec.id)], limit=1
)
if mto_move:
# MTO lines are not accounted in MTS stock buffers.
continue
domain = rec._get_domain_buffer_link_alternative()
buffer = buffer_model.search(domain, limit=1)
if buffer:
rec.buffer_ids = buffer
rec._calc_execution_priority()
return res
157 changes: 154 additions & 3 deletions stock_buffer_route/models/stock_buffer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
# Copyright 2019-20 ForgeFlow S.L. (https://www.forgeflow.com)
# Copyright 2019-23 ForgeFlow S.L. (https://www.forgeflow.com)
# Copyright 2019-20 Camptocamp SA
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).

from datetime import datetime, timedelta

from odoo import api, fields, models
from odoo.tools import float_compare

_ITEM_TYPES = [
("manufactured", "Manufactured"),
("purchased", "Purchased"),
("distributed", "Distributed"),
]


class StockBuffer(models.Model):
Expand All @@ -12,15 +21,27 @@ class StockBuffer(models.Model):
"stock.location.route",
string="Allowed routes",
compute="_compute_route_ids",
store=True,
)
route_id = fields.Many2one(
"stock.location.route",
string="Route",
domain="[('id', 'in', route_ids)]",
ondelete="restrict",
)
item_type_alternative = fields.Selection(
string="Alternative Item Type",
selection=_ITEM_TYPES,
compute="_compute_item_type_alternative",
store=True,
)
dlt_alternative = fields.Float(
string="Alternative DLT (days)",
compute="_compute_dlt_alternative",
help="Alternative Decoupled Lead Time (days)",
)

@api.depends("product_id", "warehouse_id", "warehouse_id.route_ids", "location_id")
@api.depends("product_id", "location_id", "product_id.route_ids")
def _compute_route_ids(self):
route_obj = self.env["stock.location.route"]
for record in self:
Expand All @@ -35,6 +56,91 @@ def _compute_route_ids(self):
parents = record.get_parents()
record.route_ids = self._get_location_routes_of_parents(routes, parents)

@api.depends(
"product_id", "route_id", "route_id.rule_ids", "route_id.rule_ids.action"
)
def _compute_item_type_alternative(self):
for rec in self:
rec.item_type_alternative = ""
if rec.route_id:
if "buy" in rec.route_id.mapped("rule_ids.action"):
rec.item_type_alternative = "purchased"
elif "manufacture" in rec.route_id.mapped("rule_ids.action"):
rec.item_type_alternative = "manufactured"
elif "pull" in rec.route_id.mapped(
"rule_ids.action"
) or "pull_push" in rec.route_id.mapped("rule_ids.action"):
rec.item_type_alternative = "distributed"

def _compute_dlt_alternative(self):
for rec in self:
route_id = self.env.context.get("route_id", rec.route_id)
dlt = 0
if route_id:
item_type_alternative = self.env.context.get(
"item_type_alternative", rec.item_type_alternative
)
if item_type_alternative == "manufactured":
bom = rec._get_manufactured_bom()
dlt = bom.dlt
elif item_type_alternative == "distributed":
dlt = rec.lead_days
else:
sellers = rec._get_product_sellers()
dlt = sellers and fields.first(sellers).delay or rec.lead_days
rec.dlt_alternative = dlt

@api.depends(
"buffer_profile_id",
"item_type",
"product_id.seller_ids",
"product_id.seller_ids.company_id",
"product_id.seller_ids.name",
"product_id.seller_ids.product_id",
"product_id.seller_ids.sequence",
"product_id.seller_ids.min_qty",
"product_id.seller_ids.price",
"item_type_alternative",
)
def _compute_main_supplier(self):
res = super()._compute_main_supplier()
for rec in self:
if rec.item_type_alternative == "purchased":
suppliers = rec._get_product_sellers()
rec.main_supplier_id = suppliers[0].name if suppliers else False
return res

def _get_rfq_dlt_qty(self, outside_dlt=False):
res = super()._get_rfq_dlt_qty(outside_dlt)
if self.item_type_alternative == "purchased":
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.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")
)
res += sum(pols.mapped("product_qty"))
return res

def _get_date_planned(self, force_lt=None):
res = super()._get_date_planned(force_lt)
route_id = self.env.context.get("route_id", False)
if route_id:
dlt = int(self.dlt_alternative)
item_type_alternative = self.env.context.get(
"item_type_alternative", self.item_type_alternative
)
if self.warehouse_id.calendar_id and item_type_alternative != "purchased":
res = self.warehouse_id.wh_plan_days(datetime.now(), dlt)
else:
res = fields.datetime.today() + timedelta(days=dlt)
return res

def _get_location_routes_of_parents(self, routes, parents):
return routes.filtered(
lambda route: (
Expand All @@ -45,7 +151,6 @@ def _get_location_routes_of_parents(self, routes, parents):
)
& parents
)
or any(rule.action == "buy" for rule in route.rule_ids)
)

def get_parents(self):
Expand All @@ -62,8 +167,54 @@ def _values_source_location_from_route(self):
values["route_ids"] = self.route_id
return values

def _calc_distributed_source_location(self):
res = super()._calc_distributed_source_location()
for record in self:
if (
not record.distributed_source_location_id
and record.item_type_alternative != "distributed"
):
source_location = record._source_location_from_route()
record.distributed_source_location_id = source_location
return res

def _procure_qty_to_order(self):
res = super()._procure_qty_to_order()
qty_to_order = self.procure_recommended_qty
rounding = self.procure_uom_id.rounding or self.product_uom.rounding
if (
self.item_type_alternative == "distributed"
and self.buffer_profile_id.replenish_distributed_limit_to_free_qty
):
if (
float_compare(
self.distributed_source_location_qty,
self.procure_min_qty,
precision_rounding=rounding,
)
< 0
):
res = 0
else:
res = min(qty_to_order, self.distributed_source_location_qty)
return res

def write(self, vals):
res = super().write(vals)
if "route_id" in vals:
self._calc_distributed_source_location()
return res

def action_view_supply(self, outside_dlt=False):
res = super().action_view_supply(outside_dlt)
# If route is set it means that there is at least two alternatively ways to
# procure the buffer. Therefore, we will show Stock Pickings.
if self.route_id:
moves = self._search_stock_moves_incoming(outside_dlt)
picks = moves.mapped("picking_id")
res = self.env["ir.actions.actions"]._for_xml_id(
"stock.action_picking_tree_all"
)
res["context"] = {}
res["domain"] = [("id", "in", picks.ids)]
return res
68 changes: 67 additions & 1 deletion stock_buffer_route/views/stock_buffer_views.xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2019-20 ForgeFlow S.L. (https://www.forgeflow.com)
<!-- Copyright 2019-23 ForgeFlow S.L. (https://www.forgeflow.com)
License LGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="stock_buffer_view_tree" model="ir.ui.view">
<field name="name">stock.buffer.tree</field>
<field name="model">stock.buffer</field>
<field name="inherit_id" ref="ddmrp.stock_buffer_view_tree" />
<field name="arch" type="xml">
<field name="item_type" position="after">
<field name="item_type_alternative" optional="hide" />
</field>
</field>
</record>
<record id="stock_buffer_view_form" model="ir.ui.view">
<field name="name">stock.buffer.form</field>
<field name="model">stock.buffer</field>
<field name="inherit_id" ref="ddmrp.stock_buffer_view_form" />
<field name="arch" type="xml">
<button name="action_view_bom" position="attributes">
<attribute
name="attrs"
>{'invisible': [('item_type', '!=', 'manufactured'), ('item_type_alternative', '!=', 'manufactured')]}</attribute>
</button>
<button name="action_view_mrp_productions" position="attributes">
<attribute
name="attrs"
>{'invisible': [('item_type', '!=', 'manufactured'), ('item_type_alternative', '!=', 'manufactured')]}</attribute>
</button>
<button name="action_view_purchase" position="attributes">
<attribute
name="attrs"
>{'invisible': [('item_type', '!=', 'purchased'), ('item_type_alternative', '!=', 'purchased')]}</attribute>
</button>
<field name="item_type" position="after">
<field name="item_type_alternative" invisible="1" />
</field>
<field name="lead_days" position="attributes">
<attribute
name="attrs"
>{'invisible': [('item_type', '!=', 'distributed'), ('item_type_alternative', '!=', 'distributed')]}</attribute>
</field>
<field name="distributed_source_location_id" position="attributes">
<attribute
name="attrs"
>{'invisible': ['|', ('distributed_source_location_id', '=', False), ('item_type', '!=', 'distributed'), ('item_type_alternative', '!=', 'distributed')]}</attribute>
</field>
<field name="distributed_source_location_qty" position="attributes">
<attribute
name="attrs"
>{'invisible': ['|', ('distributed_source_location_id', '=', False), ('item_type', '!=', 'distributed'), ('item_type_alternative', '!=', 'distributed')]}</attribute>
</field>
<field name="main_supplier_id" position="attributes">
<attribute
name="attrs"
>{'invisible': [('item_type', '!=', 'purchased'), ('item_type_alternative', '!=', 'purchased')]}</attribute>
</field>
<field name="group_id" position="after">
<field
name="route_id"
Expand All @@ -22,6 +70,24 @@
<field name="model">stock.buffer</field>
<field name="inherit_id" ref="ddmrp.stock_buffer_search" />
<field name="arch" type="xml">
<xpath
expr="//filter[@name='item_type_manufactured']"
position="attributes"
>
<attribute
name="domain"
>['|', ('item_type', '=', 'manufactured'), ('item_type_alternative', '=', 'manufactured')]</attribute>
</xpath>
<xpath expr="//filter[@name='item_type_purchased']" position="attributes">
<attribute
name="domain"
>['|', ('item_type', '=', 'purchased'), ('item_type_alternative', '=', 'purchased')]</attribute>
</xpath>
<xpath expr="//filter[@name='item_type_distributed']" position="attributes">
<attribute
name="domain"
>['|', ('item_type', '=', 'distributed'), ('item_type_alternative', '=', 'distributed')]</attribute>
</xpath>
<xpath
expr="//filter[@name='main_supplier_group_filter']"
position="before"
Expand Down