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

[16.0][IMP] stock_buffer_route: Add alternative item type in buffer #355

Open
wants to merge 3 commits into
base: 16.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()

Check warning on line 11 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L11

Added line #L11 was not covered by tests
if not warehouse_level:
locations = self.env["stock.location"].search(

Check warning on line 13 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L13

Added line #L13 was not covered by tests
[("id", "child_of", [self.location_dest_id.id])]
)
return [

Check warning on line 16 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L16

Added line #L16 was not covered by tests
("product_id", "=", self.product_id.id),
("company_id", "=", self.company_id.id),
("item_type_alternative", "=", "manufactured"),
("location_id", "in", locations.ids),
]
else:
return [

Check warning on line 23 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L23

Added line #L23 was not covered by tests
("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"]

Check warning on line 32 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L31-L32

Added lines #L31 - L32 were not covered by tests
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)

Check warning on line 35 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L34-L35

Added lines #L34 - L35 were not covered by tests
if not buffer:
domain = rec._get_domain_buffer_link_alternative(warehouse_level=True)
buffer = buffer_model.search(domain, limit=1)
rec.buffer_id = buffer

Check warning on line 39 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L37-L39

Added lines #L37 - L39 were not covered by tests
if buffer:
rec._calc_execution_priority()
return res

Check warning on line 42 in stock_buffer_route/models/mrp_production.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/mrp_production.py#L41-L42

Added lines #L41 - L42 were not covered by tests
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()

Check warning on line 11 in stock_buffer_route/models/purchase_order.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/purchase_order.py#L11

Added line #L11 was not covered by tests
if not self.product_id:
# Return impossible domain -> no buffer.
return [(0, "=", 1)]
return [

Check warning on line 15 in stock_buffer_route/models/purchase_order.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/purchase_order.py#L14-L15

Added lines #L14 - L15 were not covered by tests
("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"]

Check warning on line 25 in stock_buffer_route/models/purchase_order.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/purchase_order.py#L23-L25

Added lines #L23 - L25 were not covered by tests
for rec in self.filtered(lambda r: not r.buffer_ids):
mto_move = move_model.search(

Check warning on line 27 in stock_buffer_route/models/purchase_order.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/purchase_order.py#L27

Added line #L27 was not covered by tests
[("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)

Check warning on line 34 in stock_buffer_route/models/purchase_order.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/purchase_order.py#L32-L34

Added lines #L32 - L34 were not covered by tests
if buffer:
rec.buffer_ids = buffer
rec._calc_execution_priority()
return res

Check warning on line 38 in stock_buffer_route/models/purchase_order.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/purchase_order.py#L36-L38

Added lines #L36 - L38 were not covered by tests
158 changes: 155 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 @@
"stock.route",
string="Allowed routes",
compute="_compute_route_ids",
store=True,
)
route_id = fields.Many2one(
"stock.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.route"]
for record in self:
Expand All @@ -35,6 +56,92 @@
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"

Check warning on line 67 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L67

Added line #L67 was not covered by tests
elif "manufacture" in rec.route_id.mapped("rule_ids.action"):
rec.item_type_alternative = "manufactured"

Check warning on line 69 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L69

Added line #L69 was not covered by tests
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

Check warning on line 85 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L84-L85

Added lines #L84 - L85 were not covered by tests
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

Check warning on line 90 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L89-L90

Added lines #L89 - L90 were not covered by tests
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.partner_id",
"product_id.seller_ids.product_id",
"product_id.seller_ids.sequence",
"product_id.seller_ids.min_qty",
"product_id.seller_ids.price",
)
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 = (

Check warning on line 109 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L108-L109

Added lines #L108 - L109 were not covered by tests
suppliers[0].partner_id.id if suppliers else False
)
return res

def _get_rfq_dlt_qty(self, outside_dlt=False):
res = super()._get_rfq_dlt_qty(outside_dlt)

Check warning on line 115 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L115

Added line #L115 was not covered by tests
if self.item_type_alternative == "purchased":
cut_date = self._get_incoming_supply_date_limit()

Check warning on line 117 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L117

Added line #L117 was not covered by tests
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

Check warning on line 129 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L128-L129

Added lines #L128 - L129 were not covered by tests

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)

Check warning on line 140 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L140

Added line #L140 was not covered by tests
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 +152,6 @@
)
& parents
)
or any(rule.action == "buy" for rule in route.rule_ids)
)

def get_parents(self):
Expand All @@ -62,8 +168,54 @@
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

Check warning on line 198 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L198

Added line #L198 was not covered by tests
else:
res = min(qty_to_order, self.distributed_source_location_qty)

Check warning on line 200 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L200

Added line #L200 was not covered by tests
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)

Check warning on line 210 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L210

Added line #L210 was not covered by tests
# 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(

Check warning on line 216 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L214-L216

Added lines #L214 - L216 were not covered by tests
"stock.action_picking_tree_all"
)
res["context"] = {}
res["domain"] = [("id", "in", picks.ids)]
return res

Check warning on line 221 in stock_buffer_route/models/stock_buffer.py

View check run for this annotation

Codecov / codecov/patch

stock_buffer_route/models/stock_buffer.py#L219-L221

Added lines #L219 - L221 were not covered by tests
1 change: 1 addition & 0 deletions stock_buffer_route/static/description/index.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
Expand Down
70 changes: 68 additions & 2 deletions stock_buffer_route/views/stock_buffer_views.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,61 @@
<?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">
<field name="location_id" position="after">
<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"
options="{'no_create': True}"
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