changeset de6f02fc67b1 in modules/stock:default
details: https://hg.tryton.org/modules/stock?cmd=changeset&node=de6f02fc67b1
description:
        Warn user when deactivating a product that has stock

        issue11186
        review376851002
diffstat:

 CHANGELOG           |   1 +
 exceptions.py       |   4 +++
 message.xml         |   6 ++++
 product.py          |  68 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 tests/test_stock.py |  47 ++++++++++++++++++++++++++++++++++++
 5 files changed, 125 insertions(+), 1 deletions(-)

diffs (183 lines):

diff -r 1514995d057f -r de6f02fc67b1 CHANGELOG
--- a/CHANGELOG Sat Mar 26 13:03:39 2022 +0100
+++ b/CHANGELOG Mon Mar 28 13:05:07 2022 +0200
@@ -1,3 +1,4 @@
+* Warn user when deactivating a product that has stock
 * Add support for Python 3.10
 * Remove support for Python 3.6
 
diff -r 1514995d057f -r de6f02fc67b1 exceptions.py
--- a/exceptions.py     Sat Mar 26 13:03:39 2022 +0100
+++ b/exceptions.py     Mon Mar 28 13:05:07 2022 +0200
@@ -42,3 +42,7 @@
 
 class ProductCostPriceError(ValidationError):
     pass
+
+
+class ProductStockWarning(UserWarning):
+    pass
diff -r 1514995d057f -r de6f02fc67b1 message.xml
--- a/message.xml       Sat Mar 26 13:03:39 2022 +0100
+++ b/message.xml       Mon Mar 28 13:05:07 2022 +0200
@@ -119,5 +119,11 @@
         <record model="ir.message" id="msg_stock_reporting_currency">
             <field name="text">Currency</field>
         </record>
+        <record model="ir.message" id="msg_product_location_quantity">
+            <field name="text">The product "%(product)s" has still some 
quantities in locations "%(locations)s" for company "%(company)s".</field>
+        </record>
+        <record model="ir.message" 
id="msg_product_location_quantity_description">
+            <field name="text">It is recommended to clear the stock from the 
storage locations before deactivating the product.</field>
+        </record>
     </data>
 </tryton>
diff -r 1514995d057f -r de6f02fc67b1 product.py
--- a/product.py        Sat Mar 26 13:03:39 2022 +0100
+++ b/product.py        Mon Mar 28 13:05:07 2022 +0200
@@ -24,7 +24,7 @@
 from trytond.wizard import (
     Button, StateAction, StateTransition, StateView, Wizard)
 
-from .exceptions import ProductCostPriceError
+from .exceptions import ProductCostPriceError, ProductStockWarning
 from .move import StockMixin
 from .shipment import ShipmentAssignMixin
 
@@ -80,6 +80,71 @@
     return decorator
 
 
+def check_no_stock_if_inactive(func):
+
+    def get_product_locations(company, location_ids, sub_products):
+        pool = Pool()
+        Product = pool.get('product.product')
+
+        product2locations = defaultdict(list)
+        product_ids = [(p.id,) for p in sub_products]
+        with Transaction().set_context(company=company.id):
+            quantities = Product.products_by_location(
+                location_ids, with_childs=True,
+                grouping=('product',), grouping_filter=product_ids)
+        for key, quantity in quantities.items():
+            location_id, product_id, = key
+            if quantity:
+                product2locations[product_id].append(location_id)
+        return product2locations
+
+    def raise_warning(company, product2locations):
+        pool = Pool()
+        Location = pool.get('stock.location')
+        Warning = pool.get('res.user.warning')
+        Product = pool.get('product.product')
+
+        for product_id, location_ids in product2locations.items():
+            product = Product(product_id)
+            locations = ','.join(
+                l.rec_name for l in Location.browse(location_ids[:5]))
+            if len(location_ids) > 5:
+                locations += '...'
+            warning_name = Warning.format(
+                'deactivate_product_with_stock', [product])
+            if Warning.check(warning_name):
+                raise ProductStockWarning(warning_name,
+                    gettext(
+                        'stock.msg_product_location_quantity',
+                        product=product.rec_name,
+                        company=company.rec_name,
+                        locations=locations),
+                    gettext('stock.msg_product_location_quantity_description'))
+
+    @functools.wraps(func)
+    def decorator(cls, *args):
+        pool = Pool()
+        Company = pool.get('company.company')
+        Location = pool.get('stock.location')
+
+        to_check = []
+        actions = iter(args)
+        for products, values in zip(actions, actions):
+            if not values.get('active', True):
+                to_check.extend(products)
+        if to_check:
+            with Transaction().set_context(_check_access=False):
+                locations = Location.search([('type', '=', 'storage')])
+                location_ids = list(map(int, locations))
+                for company in Company.search([]):
+                    for sub_products in grouped_slice(to_check):
+                        product2locations = get_product_locations(
+                            company, location_ids, sub_products)
+                        raise_warning(company, product2locations)
+        return func(cls, *args)
+    return decorator
+
+
 class Template(metaclass=PoolMeta):
     __name__ = "product.template"
     quantity = fields.Function(fields.Float('Quantity',
@@ -174,6 +239,7 @@
 
     @classmethod
     @check_no_move
+    @check_no_stock_if_inactive
     def write(cls, *args):
         super(Product, cls).write(*args)
 
diff -r 1514995d057f -r de6f02fc67b1 tests/test_stock.py
--- a/tests/test_stock.py       Sat Mar 26 13:03:39 2022 +0100
+++ b/tests/test_stock.py       Mon Mar 28 13:05:07 2022 +0200
@@ -1476,6 +1476,53 @@
                 location.active = False
                 location.save()
 
+    @with_transaction()
+    def test_inactive_product_with_stock(self):
+        "Test inactivate product with stock"
+        pool = Pool()
+        Location = pool.get('stock.location')
+        Move = pool.get('stock.move')
+        Template = pool.get('product.template')
+        Product = pool.get('product.product')
+        Uom = pool.get('product.uom')
+
+        storage, = Location.search([('code', '=', 'STO')])
+        supplier, = Location.search([('code', '=', 'SUP')])
+        unit, = Uom.search([('name', '=', "Unit")])
+        template, = Template.create([{
+                    'name': "Product",
+                    'type': 'goods',
+                    'default_uom': unit.id,
+                    }])
+        product, = Product.create([{'template': template.id}])
+
+        company = create_company()
+        with set_company(company):
+            Move.create([{
+                        'product': product.id,
+                        'uom': unit.id,
+                        'quantity': 1,
+                        'from_location': supplier.id,
+                        'to_location': storage.id,
+                        'unit_price': Decimal('5'),
+                        'company': company.id,
+                        }])
+            with self.assertRaises(UserWarning):
+                product.active = False
+                product.save()
+
+            Move.create([{
+                        'product': product.id,
+                        'uom': unit.id,
+                        'quantity': 1,
+                        'from_location': storage.id,
+                        'to_location': supplier.id,
+                        'unit_price': Decimal('5'),
+                        'company': company.id,
+                        }])
+            product.active = False
+            product.save()
+
 
 def suite():
     suite = trytond.tests.test_tryton.suite()

Reply via email to