Skip to content

Commit

Permalink
[IMP] stock_buffer_route: Add alternative item type in buffer
Browse files Browse the repository at this point in the history
Considering that the 'Route' field will be used to define a different way to procure, a field called 'Alternative Item Type' is added in the Stock Buffer.
This will help improve the search with the filters and enhance the calculations by taking the alternative route into account.
  • Loading branch information
BernatPForgeFlow committed Aug 2, 2023
1 parent 390db0c commit 0288733
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 13 deletions.
150 changes: 149 additions & 1 deletion 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 @@ -19,6 +28,17 @@ class StockBuffer(models.Model):
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")
def _compute_route_ids(self):
Expand All @@ -35,6 +55,86 @@ 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 _calc_incoming_dlt_qty(self):
res = super()._calc_incoming_dlt_qty()
for rec in self:
if rec.item_type_alternative == "purchased":
cut_date = rec._get_incoming_supply_date_limit()
pols = rec.purchase_line_ids.filtered(
lambda l: l.date_planned > fields.Datetime.to_datetime(cut_date)
and l.state in ("draft", "sent")
)
rec.rfq_outside_dlt_qty = 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 Down Expand Up @@ -62,8 +162,56 @@ 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 record.item_type_alternative != "distributed":
record.distributed_source_location_id = self.env[
"stock.location"
].browse()
continue

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, view_rfq=False):
res = super().action_view_supply(outside_dlt, view_rfq)
# 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 and not view_rfq:
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
51 changes: 40 additions & 11 deletions stock_buffer_route/wizards/make_procurement_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@

from odoo import api, fields, models

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


class MakeProcurementBuffer(models.TransientModel):
_inherit = "make.procurement.buffer"

partner_id = fields.Many2one(
comodel_name="res.partner",
string="Vendor",
help="If set, will be used as preferred vendor for purchase routes.",
)
item_ids = fields.One2many(
comodel_name="make.procurement.buffer.item",
inverse_name="wiz_id",
string="Items",
)

@api.model
def _prepare_item(self, buffer, qty_override=None):
res = super(MakeProcurementBuffer, self)._prepare_item(buffer, qty_override)
res["route_ids"] = buffer.route_ids.ids
res["route_id"] = buffer.route_id.id
context = {
"route_id": buffer.route_id,
"item_type_alternative": buffer.item_type_alternative,
}
res["date_planned"] = buffer.with_context(context)._get_date_planned()
return res


Expand All @@ -37,6 +37,35 @@ class MakeProcurementBufferItem(models.TransientModel):
"stock.location.route",
domain="[('id', 'in', route_ids)]",
)
item_type_alternative = fields.Selection(
string="Alternative Item Type",
selection=_ITEM_TYPES,
compute="_compute_item_type_alternative",
store=True,
)

@api.onchange("route_id")
def _onchange_route_id(self):
for rec in self:
context = {
"route_id": rec.route_id,
"item_type_alternative": rec.item_type_alternative,
}
rec.date_planned = rec.buffer_id.with_context(context)._get_date_planned()

@api.depends("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 _prepare_values_make_procurement(self):
values = super()._prepare_values_make_procurement()
Expand Down

0 comments on commit 0288733

Please sign in to comment.