From 0ef8c4f01d22abee3b46c71d640a17bf55922d02 Mon Sep 17 00:00:00 2001 From: "Mathias Mathy (MAMA)" Date: Wed, 3 Aug 2022 15:12:53 +0000 Subject: [PATCH] [IMP] stock: Warning on Replenishing too much **Steps to reproduce :** - Create a Product A, with no stock and a 0 min 1 max manual reordering rule (no sales lead time, no purchase lead time) - Sell 10 of A with a delivery scheduled today - In Replenishment, create a PO for A, confirm it with reception scheduled today - Reschedule reception of PO in 7 days **Expected Behavior :** The forecast icon is replaced by an orange warning icon. When hovering the icon, the user is warned that replenishing can lead to an overstock. **Current behavior :** In Replenishment, Product A has a forecast of -10 and Qty to Order is set to 11. When the user replenish A again, the PO is created and may lead to an overstock. task : 2918947 closes odoo/odoo#97802 Signed-off-by: Arnold Moyaux (arm) --- addons/sale_purchase_stock/tests/__init__.py | 1 + .../tests/test_unwanted_replenish_flow.py | 135 ++++++++++++++++++ addons/stock/models/stock_orderpoint.py | 13 +- addons/stock/views/stock_orderpoint_views.xml | 4 +- 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 addons/sale_purchase_stock/tests/test_unwanted_replenish_flow.py diff --git a/addons/sale_purchase_stock/tests/__init__.py b/addons/sale_purchase_stock/tests/__init__.py index 54b0c0958fd..32d6b905302 100644 --- a/addons/sale_purchase_stock/tests/__init__.py +++ b/addons/sale_purchase_stock/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_sale_purchase_stock_flow from . import test_access_rights +from . import test_unwanted_replenish_flow diff --git a/addons/sale_purchase_stock/tests/test_unwanted_replenish_flow.py b/addons/sale_purchase_stock/tests/test_unwanted_replenish_flow.py new file mode 100644 index 00000000000..95c8d668f74 --- /dev/null +++ b/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 + + 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') + #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') diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py index d852d160f8b..6f2f5632c0e 100644 --- a/addons/stock/models/stock_orderpoint.py +++ b/addons/stock/models/stock_orderpoint.py @@ -11,7 +11,7 @@ from odoo import SUPERUSER_ID, _, api, fields, models, registry 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__) @@ -94,6 +94,8 @@ class StockWarehouseOrderpoint(models.Model): 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.'), @@ -169,6 +171,15 @@ class StockWarehouseOrderpoint(models.Model): ], 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: diff --git a/addons/stock/views/stock_orderpoint_views.xml b/addons/stock/views/stock_orderpoint_views.xml index 008d6b3c60d..af72498f734 100644 --- a/addons/stock/views/stock_orderpoint_views.xml +++ b/addons/stock/views/stock_orderpoint_views.xml @@ -43,12 +43,14 @@ + -