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

[IMP] stock: Warning on Replenishing too much #97802

Closed
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
1 change: 1 addition & 0 deletions addons/sale_purchase_stock/tests/__init__.py
Expand Up @@ -3,3 +3,4 @@

from . import test_sale_purchase_stock_flow
from . import test_access_rights
from . import test_unwanted_replenish_flow
135 changes: 135 additions & 0 deletions addons/sale_purchase_stock/tests/test_unwanted_replenish_flow.py
@@ -0,0 +1,135 @@
from datetime import datetime, timedelta
from odoo import Command
from odoo.tests import common, Form, tagged

@tagged('post_install', '-at_install')
class TestWarnUnwantedReplenish(common.TransactionCase):

@classmethod
def setUpClass(cls):
super().setUpClass()

cls.buy_route = cls.env.ref('purchase_stock.route_warehouse0_buy')

# Create a vendor (& suppliers) and a customer
cls.vendor = cls.env['res.partner'].create(dict(name='Vendor'))
cls.customer = cls.env['res.partner'].create(dict(name='Customer'))

cls.supplier_A = cls.env['product.supplierinfo'].create({
'partner_id' : cls.vendor.id,
'min_qty' : 0.0,
'price' : 10.0,
'delay' : 0
})

cls.supplier_B = cls.env['product.supplierinfo'].create({
'partner_id' : cls.vendor.id,
'min_qty' : 0.0,
'price' : 12.0,
'delay' : 0
})

# Create a "A" and a "B" Product :
# No Stock
# Partner/Customer Lead Time = 0
# Manual reordering 0 0

mama-odoo marked this conversation as resolved.
Show resolved Hide resolved
cls.product_A = cls.env['product.product'].create({
'name': 'Product A',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'purchase_method': 'purchase',
'invoice_policy': 'delivery',
'standard_price': 5.0,
'list_price': 10.0,
'seller_ids': [Command.link(cls.supplier_A.id)],
'route_ids': [Command.link(cls.buy_route.id)],
'sale_delay' : 0,
})

cls.product_B = cls.env['product.product'].create({
'name': 'Product B',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'purchase_method': 'purchase',
'invoice_policy': 'delivery',
'standard_price': 6.0,
'list_price': 12.0,
'seller_ids': [Command.link(cls.supplier_B.id)],
'route_ids': [Command.link(cls.buy_route.id)],
'sale_delay': 0,
})


orderpoint_form = Form(cls.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = cls.product_A
orderpoint_form.product_min_qty = 0.0
orderpoint_form.product_max_qty = 1.0
cls.orderpoint_A = orderpoint_form.save()
cls.orderpoint_A.trigger = 'manual'

orderpoint_form = Form(cls.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = cls.product_B
orderpoint_form.product_min_qty = 0.0
orderpoint_form.product_max_qty = 1.0
cls.orderpoint_B = orderpoint_form.save()
cls.orderpoint_B.trigger = 'manual'

# Create Sales
# For A and for B
# Delivered today
# Confirm SO

cls.sale_order = cls.env['sale.order'].create({
'partner_id': cls.customer.id,
'order_line': [
Command.create({
'product_id': cls.product_A.id,
'product_uom_qty': 10,
}),
Command.create({
'product_id': cls.product_B.id,
'product_uom_qty': 10,
}),
],
})

cls.sale_order.action_confirm()

# Create PO for Product A
# Confirm PO with date planned : TODAY
# Incoming Picking : reschedule in one week

cls.po_A = cls.env['purchase.order'].create({
'partner_id': cls.vendor.id,
'order_line': [
Command.create({
'name': cls.product_A.name,
'product_id': cls.product_A.id,
'product_qty': 10.0,
'price_unit': 10.0,
'date_planned': datetime.today(),
})],
})

cls.po_A.button_confirm()

cls.picking_A = cls.po_A.picking_ids[0]
cls.picking_A.scheduled_date = (datetime.today() + timedelta(days=10))

def test_01_pre_updateA_post(self):
"""
TEST 1
Replenishment ->
Product A
unwanted_replenish SHALL be TRUE
Product B
unwanted_replenish SHALL be FALSE
Product A
Modify Visible Days past 1 Week -> unwanted_replenish SHALL be FALSE
"""
self.assertTrue(self.orderpoint_A.unwanted_replenish, 'Orderpoint A not set to unwanted_replenish')
self.assertFalse(self.orderpoint_B.unwanted_replenish, 'Orderpoint B is set to unwanted_replenish')
mama-odoo marked this conversation as resolved.
Show resolved Hide resolved
#Update Orderpoint A
self.orderpoint_A.visibility_days = 10
self.assertFalse(self.orderpoint_A.unwanted_replenish, 'Orderpoint A shall not be set to unwanted_replenish')
13 changes: 12 additions & 1 deletion addons/stock/models/stock_orderpoint.py
Expand Up @@ -11,7 +11,7 @@
from odoo.addons.stock.models.stock_rule import ProcurementException
from odoo.exceptions import RedirectWarning, UserError, ValidationError
from odoo.osv import expression
from odoo.tools import float_compare, frozendict, split_every
from odoo.tools import float_compare, float_is_zero, frozendict, split_every

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -94,6 +94,8 @@ def _domain_product_id(self):
help="Consider product forecast these many days in the future upon product replenishment, set to 0 for just-in-time.\n"
"The value depends on the type of the route (Buy or Manufacture)")

unwanted_replenish = fields.Boolean('Unwanted Replenish', compute="_compute_unwanted_replenish")

_sql_constraints = [
('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'),
('product_location_check', 'unique (product_id, location_id, company_id)', 'A replenishment rule already exists for this product on this location.'),
Expand Down Expand Up @@ -169,6 +171,15 @@ def _compute_location_id(self):
], limit=1)
orderpoint.location_id = warehouse.lot_stock_id.id

@api.depends('product_id', 'qty_to_order', 'product_max_qty')
def _compute_unwanted_replenish(self):
for orderpoint in self:
if not orderpoint.product_id or float_is_zero(orderpoint.qty_to_order, precision_rounding=orderpoint.product_uom.rounding) or float_is_zero(orderpoint.product_max_qty, precision_rounding=orderpoint.product_uom.rounding):
orderpoint.unwanted_replenish = False
else:
after_replenish_qty = orderpoint.product_id.with_context(company_id=orderpoint.company_id.id, location=orderpoint.location_id.id).virtual_available + orderpoint.qty_to_order
orderpoint.unwanted_replenish = float_compare(after_replenish_qty, orderpoint.product_max_qty, precision_rounding=orderpoint.product_uom.rounding) > 0

@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
Expand Down
4 changes: 3 additions & 1 deletion addons/stock/views/stock_orderpoint_views.xml
Expand Up @@ -43,12 +43,14 @@
<field name="company_id" invisible="1"/>
<field name="product_category_id" invisible="1"/>
<field name="product_tmpl_id" invisible="1"/>
<field name="unwanted_replenish" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('product_id', '!=', False)]}" force_save="1"/>
<field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="warehouse_id" options="{'no_create': True}" groups="stock.group_stock_multi_warehouses" optional="hide"/>
<field name="qty_on_hand" force_save="1"/>
<field name="qty_forecast" force_save="1"/>
<button name="action_product_forecast_report" type="object" icon="fa-area-chart" title="Forecast Report" attrs="{'invisible': [('id', '=', False)]}"/>
<button name="action_product_forecast_report" type="object" icon="fa-area-chart" title="Forecast Report" attrs="{'invisible': ['|', ('id', '=', False), ('unwanted_replenish', '=', True)]}"/>
<button name="action_product_forecast_report" type="object" icon="fa-warning text-warning" title="Due to receipts scheduled in the future, you might end up with excessive stock . Check the Forecasted Report  before reordering" attrs="{'invisible': ['|', ('id', '=', False), ('unwanted_replenish', '=', False)]}"/>
<field name="visibility_days" optional="hidden"/>
<field name="route_id" options="{'no_create': True, 'no_open': True}" optional="hidden"/>
<button name="action_stock_replenishment_info" type="object" icon="fa-info-circle" title="Replenishment Information" attrs="{'invisible': [('id', '=', False)]}"/>
Expand Down