diff --git a/sale_order_zero_stock_blockage/__init__.py b/sale_order_zero_stock_blockage/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_order_zero_stock_blockage/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_order_zero_stock_blockage/__manifest__.py b/sale_order_zero_stock_blockage/__manifest__.py new file mode 100644 index 00000000000..8d8cac26216 --- /dev/null +++ b/sale_order_zero_stock_blockage/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Sale Order Zero Stock Blockage", + "description": """ + Zero Stock Blockage is module to prevent order to confirm if product is out of stock. + + But if manager wants then he can aprove that order. + """, + "version": "1.0", + "depends": ['sale_management', 'stock'], + "author": "danal", + "category": "Category", + "license": "LGPL-3", + "data": [ + "views/sale_order_view.xml", + ], + "installable": True, + 'application': False, +} diff --git a/sale_order_zero_stock_blockage/models/__init__.py b/sale_order_zero_stock_blockage/models/__init__.py new file mode 100644 index 00000000000..83d45ea62a4 --- /dev/null +++ b/sale_order_zero_stock_blockage/models/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import sale_order diff --git a/sale_order_zero_stock_blockage/models/sale_order.py b/sale_order_zero_stock_blockage/models/sale_order.py new file mode 100644 index 00000000000..e3ec844d1b8 --- /dev/null +++ b/sale_order_zero_stock_blockage/models/sale_order.py @@ -0,0 +1,39 @@ +from odoo.exceptions import UserError + +from odoo import fields, models, api + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + zero_stock_approval = fields.Boolean( + string="Approval", + help="Order Approval by manager,\nif order has insufficient stock then this approval is required by manager.", + copy=False, + ) + + @api.model + def fields_get(self, allfields=None, attributes=None): + res = super().fields_get(allfields, attributes) + if not self.env.user.has_group("sales_team.group_sale_manager"): + if "zero_stock_approval" in res: + res["zero_stock_approval"]["readonly"] = True + return res + + def action_confirm(self): + for record in self: + if not record.order_line: + raise UserError("You cannot confirm a Quotation without any products.") + if record.zero_stock_approval: + return super().action_confirm() + for line in record.order_line: + if ( + line.product_id.qty_available < line.product_uom_qty + and line.product_id.type == "consu" + and not record.zero_stock_approval + and not self.env.user.has_group("sales_team.group_sale_manager") + ): + raise UserError( + "Cannot confirm this Sale Order due to insufficient stock.\n\nPlease get approval or adjust the quantities." + ) + return super().action_confirm() diff --git a/sale_order_zero_stock_blockage/tests/__init__.py b/sale_order_zero_stock_blockage/tests/__init__.py new file mode 100644 index 00000000000..c95c7283023 --- /dev/null +++ b/sale_order_zero_stock_blockage/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_sale_blockage diff --git a/sale_order_zero_stock_blockage/tests/test_sale_blockage.py b/sale_order_zero_stock_blockage/tests/test_sale_blockage.py new file mode 100644 index 00000000000..4731e787beb --- /dev/null +++ b/sale_order_zero_stock_blockage/tests/test_sale_blockage.py @@ -0,0 +1,59 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + + +class TestSaleZeroStockBlockage(TransactionCase): + + @classmethod + def setUpClass(self): + super().setUpClass() + + self.sale_user = self.env['res.users'].create({ + 'name': 'Salesman Test User', + 'login': 'sales_test_user', + 'email': 'sales_test@example.com', + 'group_ids': [(4, self.env.ref('sales_team.group_sale_salesman').id)], + }) + + self.partner = self.sale_user.partner_id + + self.product_no_stock = self.env['product.product'].create({ + 'name': 'Zero Stock Product', + 'type': 'consu', + 'list_price': 100.0, + }) + + self.sale_order = self.env['sale.order'].with_user(self.sale_user).create({ + 'partner_id': self.partner.id, + 'order_line': [(0, 0, { + 'product_id': self.product_no_stock.id, + 'product_uom_qty': 1.0, + 'price_unit': 100.0, + })], + }) + + def test_block_confirm_no_stock(self): + """ Test that confirming a sale order with zero stock raises an error """ + self.assertEqual(self.product_no_stock.qty_available, 0, "Initial stock should be 0") + with self.assertRaises(UserError): + self.sale_order.action_confirm() + + def test_allow_confirm_no_stock(self): + """ Test that confirming a sale order with zero stock will not raise an error if `zero_stock_approval` is True """ + self.assertEqual(self.product_no_stock.qty_available, 0, "Initial stock should be 0") + self.sale_order.zero_stock_approval = True + self.assertEqual(self.sale_order.zero_stock_approval, True, "Cannot comfirm order of insufficent product") + self.sale_order.action_confirm() + + def test_allow_confirm_with_stock(self): + """ Test that adding stock allows the order to be confirmed """ + stock_location = self.env.ref('stock.stock_location_stock') + self.product_no_stock.is_storable = True + self.env['stock.quant'].create({ + 'product_id': self.product_no_stock.id, + 'location_id': stock_location.id, + 'inventory_quantity': 10.0, + }).action_apply_inventory() + self.assertEqual(self.product_no_stock.qty_available, 10.0, "Stock should be updated to 10") + self.sale_order.action_confirm() + self.assertEqual(self.sale_order.state, 'sale', "Order should be in 'sale' state after confirmation") diff --git a/sale_order_zero_stock_blockage/views/sale_order_view.xml b/sale_order_zero_stock_blockage/views/sale_order_view.xml new file mode 100644 index 00000000000..d24969dc567 --- /dev/null +++ b/sale_order_zero_stock_blockage/views/sale_order_view.xml @@ -0,0 +1,13 @@ + + + + sale.order.view.inherit + sale.order + + + + + + + +