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()