From e5e90d6c7efeb52ddd1fec9fdcc3b9e37550df75 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Thu, 28 Aug 2025 10:16:46 -0400 Subject: [PATCH] [ADD] New module replenish_by_first_vendor. Added a new module for setting the vendor on manual replenishments that are created by the system. This permits buyers using the Replenishment interface (stock.orderpoint list view) to more easily order items as they can group by vendor when working to set up purchase orders. --- replenish_by_first_vendor/__init__.py | 1 + replenish_by_first_vendor/__manifest__.py | 25 +++ replenish_by_first_vendor/models/__init__.py | 1 + .../models/stock_orderpoint.py | 17 ++ replenish_by_first_vendor/tests/__init__.py | 1 + .../tests/test_replenish_by_first_vendor.py | 200 ++++++++++++++++++ 6 files changed, 245 insertions(+) create mode 100644 replenish_by_first_vendor/__init__.py create mode 100644 replenish_by_first_vendor/__manifest__.py create mode 100644 replenish_by_first_vendor/models/__init__.py create mode 100644 replenish_by_first_vendor/models/stock_orderpoint.py create mode 100644 replenish_by_first_vendor/tests/__init__.py create mode 100644 replenish_by_first_vendor/tests/test_replenish_by_first_vendor.py diff --git a/replenish_by_first_vendor/__init__.py b/replenish_by_first_vendor/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/replenish_by_first_vendor/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/replenish_by_first_vendor/__manifest__.py b/replenish_by_first_vendor/__manifest__.py new file mode 100644 index 0000000..dd4d48a --- /dev/null +++ b/replenish_by_first_vendor/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +{ + "name": "Replenish by First Vendor", + "version": "18.0.0.0.1", + "category": "Inventory/Inventory", + "license": "GPL-3", + "summary": "Auto-select first vendor when creating manual orderpoints", + "description": """ + Automatically selects the first vendor from a product's vendor list (vendor pricelists) + when an orderpoint with manual trigger is created, if no vendor is already set. + + This helps streamline the orderpoint creation process by automatically setting + the most relevant vendor based on the product's supplier information. + """, + "author": "Bemade Inc.", + "website": "https://www.bemade.org", + "depends": [ + "stock", + "purchase", + "purchase_stock", + ], + "data": [], + "auto_install": False, + "installable": True, +} diff --git a/replenish_by_first_vendor/models/__init__.py b/replenish_by_first_vendor/models/__init__.py new file mode 100644 index 0000000..84144d1 --- /dev/null +++ b/replenish_by_first_vendor/models/__init__.py @@ -0,0 +1 @@ +from . import stock_orderpoint diff --git a/replenish_by_first_vendor/models/stock_orderpoint.py b/replenish_by_first_vendor/models/stock_orderpoint.py new file mode 100644 index 0000000..35e3b1c --- /dev/null +++ b/replenish_by_first_vendor/models/stock_orderpoint.py @@ -0,0 +1,17 @@ +from odoo import models, fields, api + + +class StockOrderpoint(models.Model): + _inherit = "stock.warehouse.orderpoint" + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for orderpoint in res: + if ( + orderpoint.trigger == "manual" + and not orderpoint.supplier_id + and orderpoint.product_id.seller_ids + ): + orderpoint.supplier_id = orderpoint.product_id.seller_ids[0] + return res diff --git a/replenish_by_first_vendor/tests/__init__.py b/replenish_by_first_vendor/tests/__init__.py new file mode 100644 index 0000000..725e4bc --- /dev/null +++ b/replenish_by_first_vendor/tests/__init__.py @@ -0,0 +1 @@ +from . import test_replenish_by_first_vendor diff --git a/replenish_by_first_vendor/tests/test_replenish_by_first_vendor.py b/replenish_by_first_vendor/tests/test_replenish_by_first_vendor.py new file mode 100644 index 0000000..fac0e9b --- /dev/null +++ b/replenish_by_first_vendor/tests/test_replenish_by_first_vendor.py @@ -0,0 +1,200 @@ +from odoo.tests import TransactionCase, tagged + + +@tagged("-at_install", "post_install") +class TestReplenishByFirstVendor(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create test vendors + cls.vendor1 = cls.env["res.partner"].create( + { + "name": "First Vendor", + "supplier_rank": 1, + } + ) + + cls.vendor2 = cls.env["res.partner"].create( + { + "name": "Second Vendor", + "supplier_rank": 1, + } + ) + + # Create test product with Make to Stock route + cls.product_mts = cls.env["product.product"].create( + { + "name": "Test Product MTS", + "type": "consu", + "tracking": "none", + "list_price": 100.0, + "standard_price": 80.0, + } + ) + + # Create supplier info for the product (vendor1 first, vendor2 second) + cls.supplierinfo1 = cls.env["product.supplierinfo"].create( + { + "partner_id": cls.vendor1.id, + "product_tmpl_id": cls.product_mts.product_tmpl_id.id, + "min_qty": 1, + "price": 75.0, + "sequence": 1, # First vendor + } + ) + + cls.supplierinfo2 = cls.env["product.supplierinfo"].create( + { + "partner_id": cls.vendor2.id, + "product_tmpl_id": cls.product_mts.product_tmpl_id.id, + "min_qty": 1, + "price": 70.0, + "sequence": 2, # Second vendor + } + ) + + # Create a product without vendors + cls.product_no_vendor = cls.env["product.product"].create( + { + "name": "Product Without Vendor", + "type": "consu", + "tracking": "none", + "list_price": 50.0, + "standard_price": 40.0, + } + ) + + # Get warehouse and location + cls.warehouse = cls.env["stock.warehouse"].search([], limit=1) + cls.location = cls.warehouse.lot_stock_id + + def test_manual_orderpoint_sets_first_vendor(self): + """Test that manual orderpoints automatically set the first vendor.""" + orderpoint = self.env["stock.warehouse.orderpoint"].create( + { + "product_id": self.product_mts.id, + "location_id": self.location.id, + "warehouse_id": self.warehouse.id, + "product_min_qty": 5.0, + "product_max_qty": 20.0, + "trigger": "manual", + } + ) + + # Check that supplier_id is set to the first vendor's supplierinfo + self.assertEqual( + orderpoint.supplier_id, + self.supplierinfo1, + "Manual orderpoint should automatically set the first vendor as supplier_id", + ) + + def test_auto_orderpoint_no_vendor_setting(self): + """Test that automatic orderpoints don't get auto-vendor setting.""" + orderpoint = self.env["stock.warehouse.orderpoint"].create( + { + "product_id": self.product_mts.id, + "location_id": self.location.id, + "warehouse_id": self.warehouse.id, + "product_min_qty": 5.0, + "product_max_qty": 20.0, + "trigger": "auto", + } + ) + + # Check that supplier_id is not set for auto trigger + self.assertFalse( + orderpoint.supplier_id, + "Auto orderpoint should not automatically set supplier_id", + ) + + def test_manual_orderpoint_with_existing_vendor(self): + """Test that manual orderpoints with existing vendor don't get overridden.""" + orderpoint = self.env["stock.warehouse.orderpoint"].create( + { + "product_id": self.product_mts.id, + "location_id": self.location.id, + "warehouse_id": self.warehouse.id, + "product_min_qty": 5.0, + "product_max_qty": 20.0, + "trigger": "manual", + "supplier_id": self.supplierinfo2.id, # Pre-set to second vendor + } + ) + + # Check that supplier_id remains the pre-set vendor + self.assertEqual( + orderpoint.supplier_id, + self.supplierinfo2, + "Manual orderpoint with existing supplier should not be overridden", + ) + + def test_manual_orderpoint_no_vendors_available(self): + """Test that manual orderpoints for products without vendors don't crash.""" + orderpoint = self.env["stock.warehouse.orderpoint"].create( + { + "product_id": self.product_no_vendor.id, + "location_id": self.location.id, + "warehouse_id": self.warehouse.id, + "product_min_qty": 5.0, + "product_max_qty": 20.0, + "trigger": "manual", + } + ) + + # Check that supplier_id remains empty + self.assertFalse( + orderpoint.supplier_id, + "Manual orderpoint for product without vendors should have no supplier_id", + ) + + def test_vendor_sequence_respected(self): + """Test that the vendor with the lowest sequence is selected.""" + # Update sequences to ensure vendor2 comes first + self.supplierinfo1.sequence = 10 + self.supplierinfo2.sequence = 5 + + orderpoint = self.env["stock.warehouse.orderpoint"].create( + { + "product_id": self.product_mts.id, + "location_id": self.location.id, + "warehouse_id": self.warehouse.id, + "product_min_qty": 5.0, + "product_max_qty": 20.0, + "trigger": "manual", + } + ) + + # Check that supplier_id is set to vendor2 (lower sequence) + self.assertEqual( + orderpoint.supplier_id, + self.supplierinfo2, + "Manual orderpoint should select vendor with lowest sequence", + ) + + def test_vendor_id_field_also_set(self): + """Test that vendor_id field is also properly set when supplier_id is set.""" + orderpoint = self.env["stock.warehouse.orderpoint"].create( + { + "product_id": self.product_mts.id, + "location_id": self.location.id, + "warehouse_id": self.warehouse.id, + "product_min_qty": 5.0, + "product_max_qty": 20.0, + "trigger": "manual", + } + ) + + # Check that supplier_id is set to first vendor's supplierinfo + self.assertEqual( + orderpoint.supplier_id, + self.supplierinfo1, + "supplier_id should be set to first vendor's supplierinfo", + ) + + # Check that the partner (vendor) is accessible through supplier_id + self.assertEqual( + orderpoint.supplier_id.partner_id, + self.vendor1, + "supplier_id.partner_id should be the first vendor", + )