diff --git a/setup/stock_no_negative/odoo/__init__.py b/setup/stock_no_negative/odoo/__init__.py new file mode 100644 index 000000000000..de40ea7ca058 --- /dev/null +++ b/setup/stock_no_negative/odoo/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/stock_no_negative/odoo/addons/__init__.py b/setup/stock_no_negative/odoo/addons/__init__.py new file mode 100644 index 000000000000..de40ea7ca058 --- /dev/null +++ b/setup/stock_no_negative/odoo/addons/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/stock_no_negative/odoo/addons/stock_no_negative b/setup/stock_no_negative/odoo/addons/stock_no_negative new file mode 120000 index 000000000000..6d72769aa170 --- /dev/null +++ b/setup/stock_no_negative/odoo/addons/stock_no_negative @@ -0,0 +1 @@ +../../../../stock_no_negative/ \ No newline at end of file diff --git a/setup/stock_no_negative/setup.py b/setup/stock_no_negative/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_no_negative/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_no_negative/README.rst b/stock_no_negative/README.rst new file mode 100644 index 000000000000..a12f4658b4a4 --- /dev/null +++ b/stock_no_negative/README.rst @@ -0,0 +1,58 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +======================= +Stock Disallow Negative +======================= + +By default, Odoo allows negative stock. The advantage of negative stock is that, if some stock levels are wrong in the ERP, you will not be blocked when validating the picking for a customer... so you will still be able to ship the products on time (it's an example !). The problem is that, after you forced the stock level to negative, you are supposed to fix the stock level later via an inventory ; but this action is often forgotten by users, so you end up with negative stock levels in your ERP and it can stay like this forever (or at least until the next full inventory). + +If you disallow negative stock in Odoo with this module, you will be blocked when trying to validate a stock operation that will set the stock level of a product as negative. So you will have to fix the wrong stock level of that product without delay, in order to validate the stock operation in Odoo... you can't forget it anymore ! + +Configuration +============= + +By default, the stockable products will not be allowed to have a negative stock. If you want to make some exceptions for some products or some product categories, you can activate the option *Allow Negative Stock* on some products or some products categories. + +Usage +===== + +When you validate a stock operation (a stock move, a picking, a manufacturing order, etc.) that will set the stock level of a stockable product as negative, you will be blocked by an error message. The consumable products can still have a negative stock level. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/154/10.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre +* Eficent Business and IT Consulting Services S.L. +* Serpent Consulting Services Pvt. Ltd. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/stock_no_negative/__init__.py b/stock_no_negative/__init__.py new file mode 100644 index 000000000000..c4b4c3051009 --- /dev/null +++ b/stock_no_negative/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/stock_no_negative/__manifest__.py b/stock_no_negative/__manifest__.py new file mode 100644 index 000000000000..8466227a0524 --- /dev/null +++ b/stock_no_negative/__manifest__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +{ + 'name': 'Stock Disallow Negative', + 'version': '10.0.1.0.2', + 'category': 'Inventory, Logistic, Storage', + 'license': 'AGPL-3', + 'summary': 'Disallow negative stock levels by default', + 'author': 'Akretion,Odoo Community Association (OCA)', + 'website': 'http://www.akretion.com', + 'depends': ['stock'], + 'data': ['views/product.xml'], + 'installable': True, +} diff --git a/stock_no_negative/i18n/fr.po b/stock_no_negative/i18n/fr.po new file mode 100644 index 000000000000..f6b3cc0428a2 --- /dev/null +++ b/stock_no_negative/i18n/fr.po @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_no_negative +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-10-07 09:43+0000\n" +"PO-Revision-Date: 2015-10-07 09:43+0000\n" +"Last-Translator: Alexis de Lattre \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_no_negative +#: field:product.category,allow_negative_stock:0 +#: field:product.template,allow_negative_stock:0 +msgid "Allow Negative Stock" +msgstr "Autoriser le stock négatif" + +#. module: stock_no_negative +#: help:product.category,allow_negative_stock:0 +msgid "Allow negative stock levels for the stockable products attached to this category. The options doesn't apply to products attached to sub-categories of this category." +msgstr "Autorise les niveaux de stock négatif pour les articles stockables attachés à cette catégorie. Cette option ne s'applique pas aux articles attachés à des sous-catégories de cette catégorie." + +#. module: stock_no_negative +#: help:product.template,allow_negative_stock:0 +msgid "If this option is not active on this product nor on its product category and that this product is a stockable product, then the validation of the related stock moves will be blocked if the stock level becomes negative with the stock move." +msgstr "Si cette option n'est pas activée sur cet article ni sur la catégorie à laquelle il est rattaché et que cet article est un produit stockable, alors la validation des mouvements de stock sera bloquée si le niveau de stock devient négatif avec ce mouvement de stock." + +#. module: stock_no_negative +#: model:ir.model,name:stock_no_negative.model_product_category +msgid "Product Category" +msgstr "Catégorie d'articles" + +#. module: stock_no_negative +#: model:ir.model,name:stock_no_negative.model_product_template +msgid "Product Template" +msgstr "Modèle d'article" + +#. module: stock_no_negative +#: model:ir.model,name:stock_no_negative.model_stock_quant +msgid "Quants" +msgstr "Quants" + +#. module: stock_no_negative +#: view:product.template:stock_no_negative.product_template_form_view +msgid "Stock and Expected Variations" +msgstr "Stock et variations prévues" + +#. module: stock_no_negative +#: code:addons/stock_no_negative/stock_no_negative.py:60 +#, python-format +msgid "You cannot valide this stock operation because the stock level of the product '%s' would become negative on the stock location '%s' and negative stock is not allowed for this product." +msgstr "Vous ne pouvez pas valider cette opération de stock car le niveau de stock de l'article '%s' deviendrait négatif sur l'emplacement de stock '%s' et le stock négatif n'est pas autorisé pour cet article." + diff --git a/stock_no_negative/models/__init__.py b/stock_no_negative/models/__init__.py new file mode 100644 index 000000000000..feac848aa8d4 --- /dev/null +++ b/stock_no_negative/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import product +from . import stock_quant diff --git a/stock_no_negative/models/product.py b/stock_no_negative/models/product.py new file mode 100644 index 000000000000..c2f4d32162b8 --- /dev/null +++ b/stock_no_negative/models/product.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields + + +class ProductCategory(models.Model): + _inherit = 'product.category' + + allow_negative_stock = fields.Boolean( + string='Allow Negative Stock', + help="Allow negative stock levels for the stockable products " + "attached to this category. The options doesn't apply to products " + "attached to sub-categories of this category.") + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + allow_negative_stock = fields.Boolean( + string='Allow Negative Stock', + help="If this option is not active on this product nor on its " + "product category and that this product is a stockable product, " + "then the validation of the related stock moves will be blocked if " + "the stock level becomes negative with the stock move.") diff --git a/stock_no_negative/models/stock_quant.py b/stock_no_negative/models/stock_quant.py new file mode 100644 index 000000000000..2227080172a8 --- /dev/null +++ b/stock_no_negative/models/stock_quant.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# © 2015-2017 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, api, _ +from odoo.exceptions import ValidationError +from odoo.tools import float_compare + + +class StockQuant(models.Model): + _inherit = 'stock.quant' + + @api.multi + @api.constrains('product_id', 'qty') + def check_negative_qty(self): + p = self.env['decimal.precision'].precision_get( + 'Product Unit of Measure') + for quant in self: + if ( + float_compare(quant.qty, 0, precision_digits=p) == -1 and + quant.product_id.type == 'product' and + not quant.product_id.allow_negative_stock and + not quant.product_id.categ_id.allow_negative_stock): + msg_add = '' + if quant.lot_id: + msg_add = _(" lot '%s'") % quant.lot_id.name_get()[0][1] + raise ValidationError(_( + "You cannot validate this stock operation because the " + "stock level of the product '%s'%s would become negative " + "(%s) on the stock location '%s' and negative stock is " + "not allowed for this product.") % ( + quant.product_id.name, msg_add, quant.qty, + quant.location_id.complete_name)) diff --git a/stock_no_negative/tests/__init__.py b/stock_no_negative/tests/__init__.py new file mode 100644 index 000000000000..f03565a82eda --- /dev/null +++ b/stock_no_negative/tests/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# © 2016 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# © 2016 Serpent Consulting Services Pvt. Ltd. () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_stock_no_negative diff --git a/stock_no_negative/tests/test_stock_no_negative.py b/stock_no_negative/tests/test_stock_no_negative.py new file mode 100644 index 000000000000..ee0602f2a74f --- /dev/null +++ b/stock_no_negative/tests/test_stock_no_negative.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# © 2016 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# © 2016 Serpent Consulting Services Pvt. Ltd. () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + + +class TestStockNoNegative(TransactionCase): + + def setUp(self): + super(TestStockNoNegative, self).setUp() + self.product_model = self.env['product.product'] + self.product_ctg_model = self.env['product.category'] + self.picking_type_id = self.env.ref('stock.picking_type_out') + self.location_id = self.env.ref('stock.stock_location_stock') + self.location_dest_id = self.env.ref('stock.stock_location_customers') + # Create product category + self.product_ctg = self._create_product_category() + # Create a Product + self.product = self._create_product('test_product1') + self._create_picking() + + def _create_product_category(self): + product_ctg = self.product_ctg_model.create({ + 'name': 'test_product_ctg', + }) + return product_ctg + + def _create_product(self, name): + product = self.product_model.create({ + 'name': name, + 'categ_id': self.product_ctg.id, + 'type': 'product', + }) + return product + + def _create_picking(self): + self.stock_picking = self.env['stock.picking'].create({ + 'picking_type_id': self.picking_type_id.id, + 'move_type': 'direct', + 'location_id': self.location_id.id, + 'location_dest_id': self.location_dest_id.id + }) + + self.stock_move = self.env['stock.move'].create({ + 'name': 'Test Move', + 'product_id': self.product.id, + 'product_uom_qty': 100.0, + 'product_uom': self.product.uom_id.id, + 'picking_id': self.stock_picking.id, + 'state': 'draft', + 'location_id': self.location_id.id, + 'location_dest_id': self.location_dest_id.id + }) + + def test_check_constrains(self): + """Assert that constraint is raised when user + tries to validate the stock operation which would + make the stock level of the product negative """ + self.stock_picking.action_confirm() + self.stock_picking.action_assign() + self.stock_picking.force_assign() + self.stock_picking.do_new_transfer() + self.stock_immediate_transfer = self.env['stock.immediate.transfer'].\ + create({'pick_id': self.stock_picking.id}) + with self.assertRaises(ValidationError): + self.stock_immediate_transfer.process() + + def test_true_allow_negative_stock(self): + """Assert that negative stock levels are allowed when + the allow_negative_stock is set active in the product""" + self.product.product_tmpl_id.write({'allow_negative_stock': True}) + self.stock_picking.action_confirm() + self.stock_picking.action_assign() + self.stock_picking.force_assign() + self.stock_picking.do_new_transfer() + self.stock_immediate_transfer = self.env['stock.immediate.transfer'].\ + create({'pick_id': self.stock_picking.id}) + self.stock_immediate_transfer.process() + quant = self.env['stock.quant'].search([('product_id', '=', + self.product.id), + ('location_id', '=', + self.location_id.id)]) + self.assertEqual(quant.qty, + -self.stock_move.product_uom_qty) diff --git a/stock_no_negative/views/product.xml b/stock_no_negative/views/product.xml new file mode 100644 index 000000000000..dbc755979188 --- /dev/null +++ b/stock_no_negative/views/product.xml @@ -0,0 +1,34 @@ + + + + + + + stock_no_negative.product.template.form + product.template + + + + + + + + + + stock_no_negative.product.category.form + product.category + + + + + + + + +