From 07e173114fbf5ed7655fcf81f8a045656f70688f Mon Sep 17 00:00:00 2001 From: mpanarin Date: Tue, 13 Feb 2018 18:11:35 +0200 Subject: [PATCH] [MIG] stock_available_unreserved: migrate Odoo 11 --- stock_available_unreserved/README.rst | 3 +- stock_available_unreserved/__init__.py | 1 - stock_available_unreserved/__manifest__.py | 19 +- stock_available_unreserved/models/__init__.py | 2 +- stock_available_unreserved/models/product.py | 131 +++++------ stock_available_unreserved/models/quant.py | 23 ++ stock_available_unreserved/tests/__init__.py | 1 - .../tests/test_stock_available_unreserved.py | 208 +++++++++--------- .../views/product_view.xml | 6 +- .../views/stock_quant_view.xml | 44 ++-- 10 files changed, 227 insertions(+), 211 deletions(-) create mode 100644 stock_available_unreserved/models/quant.py diff --git a/stock_available_unreserved/README.rst b/stock_available_unreserved/README.rst index dbaebc69e..43b879b86 100644 --- a/stock_available_unreserved/README.rst +++ b/stock_available_unreserved/README.rst @@ -23,7 +23,7 @@ Usage .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/153/10.0 + :target: https://runbot.odoo-community.org/runbot/153/11.0 Bug Tracker @@ -47,6 +47,7 @@ Contributors * Jordi Ballester Alomar * Stefan Rijnhart +* Mykhailo Panarin Maintainer diff --git a/stock_available_unreserved/__init__.py b/stock_available_unreserved/__init__.py index 298ab2340..a00d71200 100644 --- a/stock_available_unreserved/__init__.py +++ b/stock_available_unreserved/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 ACSONE SA/NV () # Copyright 2016 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) diff --git a/stock_available_unreserved/__manifest__.py b/stock_available_unreserved/__manifest__.py index c43c438fe..c3f819b63 100644 --- a/stock_available_unreserved/__manifest__.py +++ b/stock_available_unreserved/__manifest__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# Copyright 2018 Camptocamp SA # Copyright 2016 ACSONE SA/NV () # Copyright 2016 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) @@ -6,16 +6,17 @@ { "name": "Stock Available Unreserved", "summary": "Quantity of stock available for immediate use", - "version": "10.0.1.0.0", + "version": "11.0.1.0.0", "author": "Eficent Business and IT Consulting Services S.L," "Odoo Community Association (OCA)", - "website": "https://www.odoo-community.org", + "website": "https://github.com/OCA/stock-logistics-warehouse", "category": "Warehouse Management", - "depends": ["stock"], - "data": ["views/stock_quant_view.xml", - "views/product_view.xml" - ], + "depends": [ + "stock", + ], + "data": [ + "views/stock_quant_view.xml", + "views/product_view.xml", + ], "license": "AGPL-3", - 'installable': True, - 'application': False, } diff --git a/stock_available_unreserved/models/__init__.py b/stock_available_unreserved/models/__init__.py index a7bcedeaa..e76114f16 100644 --- a/stock_available_unreserved/models/__init__.py +++ b/stock_available_unreserved/models/__init__.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- # Copyright 2016 ACSONE SA/NV () # Copyright 2016 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import product +from . import quant diff --git a/stock_available_unreserved/models/product.py b/stock_available_unreserved/models/product.py index 7001ddeb5..867917fd9 100644 --- a/stock_available_unreserved/models/product.py +++ b/stock_available_unreserved/models/product.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- +# Copyright 2018 Camptocamp SA # Copyright 2016 ACSONE SA/NV () # Copyright 2016 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from odoo import api, fields, models, _ -from odoo.tools.float_utils import float_round +from odoo import api, fields, models from odoo.addons import decimal_precision as dp UNIT = dp.get_precision('Product Unit of Measure') @@ -15,33 +14,36 @@ class ProductTemplate(models.Model): _inherit = "product.template" qty_available_not_res = fields.Float( - string='Quantity On Hand Unreserved', digits=UNIT, - compute='_compute_product_available_not_res') - - qty_available_stock_text = fields.Char( + string='Quantity On Hand Unreserved', + digits=UNIT, compute='_compute_product_available_not_res', - string='Unreserved stock quantity') + ) @api.multi @api.depends('product_variant_ids.qty_available_not_res') def _compute_product_available_not_res(self): - no_new = self.filtered( - lambda x: not isinstance(x.id, models.NewId)) - for tmpl in no_new: - tmpl.qty_available_not_res = sum(tmpl.mapped( - 'product_variant_ids.qty_available_not_res')) - tmpl.qty_available_stock_text = "/".join(tmpl.mapped( - 'product_variant_ids.qty_available_stock_text')) + for tmpl in self: + if isinstance(tmpl.id, models.NewId): + continue + tmpl.qty_available_not_res = sum( + tmpl.mapped('product_variant_ids.qty_available_not_res') + ) @api.multi def action_open_quants_unreserved(self): - products = self.mapped('product_variant_ids').ids + products_ids = self.mapped('product_variant_ids').ids + quants = self.env['stock.quant'].search([ + ('product_id', 'in', products_ids), + ]) + quant_ids = quants.filtered( + lambda x: x.product_id.qty_available_not_res > 0 + ).ids result = self.env.ref('stock.product_open_quants').read()[0] - result['domain'] = "[('product_id','in',[" + ','.join( - map(str, products)) + "]), ('reservation_id', '=', False)]" - result[ - 'context'] = "{'search_default_locationgroup': 1, " \ - "'search_default_internal_loc': 1}" + result['domain'] = [('id', 'in', quant_ids)] + result['context'] = { + 'search_default_locationgroup': 1, + 'search_default_internal_loc': 1, + } return result @@ -49,68 +51,55 @@ class ProductProduct(models.Model): _inherit = 'product.product' qty_available_not_res = fields.Float( - string='Qty Available Not Reserved', digits=UNIT, - compute='_compute_qty_available_not_res') - - qty_available_stock_text = fields.Char( - compute='_compute_qty_available_not_res', string='Available per stock') - - @api.multi - def _compute_qty_available_not_res(self): - res = self._compute_product_available_not_res_dict() - for prod in self: - qty = res[prod.id]['qty_available_not_res'] - text = res[prod.id]['qty_available_stock_text'] - prod.qty_available_not_res = qty - prod.qty_available_stock_text = text - return res - - @api.model - def _prepare_domain_available_not_res(self, products): - domain_products = [('product_id', 'in', products.mapped('id'))] - domain_quant = [] - domain_quant_loc = products._get_domain_locations()[0] - - domain_quant += domain_products - - domain_quant.append(('reservation_id', '=', False)) - - domain_quant += domain_quant_loc - - return domain_quant + string='Qty Available Not Reserved', + digits=UNIT, + compute='_compute_qty_available_not_reserved', + ) @api.multi def _product_available_not_res_hook(self, quants): """Hook used to introduce possible variations""" return False + @api.multi + def _prepare_domain_available_not_reserved(self): + domain_quant = [ + ('product_id', 'in', self.ids), + ('contains_unreserved', '=', True), + ] + domain_quant_locations = self._get_domain_locations()[0] + domain_quant.extend(domain_quant_locations) + return domain_quant + @api.multi def _compute_product_available_not_res_dict(self): res = {} - domain_quant = self._prepare_domain_available_not_res(self) - - quants = self.env['stock.quant'].with_context(lang=False).read_group( + domain_quant = self._prepare_domain_available_not_reserved() + quants = self.env['stock.quant'].with_context(lang=False).search( domain_quant, - ['product_id', 'location_id', 'qty'], - ['product_id', 'location_id'], - lazy=False) - values_prod = {} - for quant in quants: - # create a dictionary with the total value per products - values_prod.setdefault(quant['product_id'][0], 0) - values_prod[quant['product_id'][0]] += quant['qty'] - for product in self.with_context(prefetch_fields=False, lang=''): - res[product.id] = {} - # get total qty for the product - qty = float_round(values_prod.get(product.id, 0.0), - precision_rounding=product.uom_id.rounding) - qty_available_not_res = qty - res[product.id].update({'qty_available_not_res': - qty_available_not_res}) - text = str(qty_available_not_res) + _(" On Hand") - res[product.id].update({'qty_available_stock_text': text}) + ) + # TODO: this should probably be refactored performance-wise + for prod in self: + vals = {} + prod_quant = quants.filtered(lambda x: x.product_id == prod) + quantity = sum(prod_quant.mapped( + lambda x: x._get_available_quantity( + x.product_id, + x.location_id + ) + )) + vals['qty_available_not_res'] = quantity + res[prod.id] = vals self._product_available_not_res_hook(quants) return res + + @api.multi + def _compute_qty_available_not_reserved(self): + res = self._compute_product_available_not_res_dict() + for prod in self: + qty = res[prod.id]['qty_available_not_res'] + prod.qty_available_not_res = qty + return res diff --git a/stock_available_unreserved/models/quant.py b/stock_available_unreserved/models/quant.py new file mode 100644 index 000000000..625634092 --- /dev/null +++ b/stock_available_unreserved/models/quant.py @@ -0,0 +1,23 @@ +# Copyright 2018 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class StockQuant(models.Model): + _inherit = "stock.quant" + + contains_unreserved = fields.Boolean( + string="Contains unreserved products", + compute="_compute_contains_unreserved", + store=True, + ) + + @api.depends('product_id', 'location_id', 'quantity', 'reserved_quantity') + def _compute_contains_unreserved(self): + for record in self: + available = record._get_available_quantity( + record.product_id, + record.location_id, + ) + record.contains_unreserved = True if available > 0 else False diff --git a/stock_available_unreserved/tests/__init__.py b/stock_available_unreserved/tests/__init__.py index 296133ef2..a9fb8e294 100644 --- a/stock_available_unreserved/tests/__init__.py +++ b/stock_available_unreserved/tests/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 ACSONE SA/NV () # Copyright 2016 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) diff --git a/stock_available_unreserved/tests/test_stock_available_unreserved.py b/stock_available_unreserved/tests/test_stock_available_unreserved.py index f0ef3b932..865139a2c 100644 --- a/stock_available_unreserved/tests/test_stock_available_unreserved.py +++ b/stock_available_unreserved/tests/test_stock_available_unreserved.py @@ -1,138 +1,142 @@ -# -*- coding: utf-8 -*- +# Copyright 2018 Camptocamp SA # Copyright 2016 ACSONE SA/NV () # Copyright 2016 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) # Copyright 2016 Therp BV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from odoo.tests.common import TransactionCase +from odoo.tests.common import SavepointCase -class TestStockLogisticsWarehouse(TransactionCase): - - def test01_stock_levels(self): - """checking that qty_available_not_res actually reflects \ - the variations in stock, both on product and template""" - pickingObj = self.env['stock.picking'] - productObj = self.env['product.product'] - templateObj = self.env['product.template'] - supplier_location = self.env.ref('stock.stock_location_suppliers') - stock_location = self.env.ref('stock.stock_location_stock') - customer_location = self.env.ref('stock.stock_location_customers') - uom_unit = self.env.ref('product.product_uom_unit') +class TestStockLogisticsWarehouse(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pickingObj = cls.env['stock.picking'] + cls.productObj = cls.env['product.product'] + cls.templateObj = cls.env['product.template'] + cls.supplier_location = cls.env.ref('stock.stock_location_suppliers') + cls.stock_location = cls.env.ref('stock.stock_location_stock') + cls.customer_location = cls.env.ref('stock.stock_location_customers') + cls.uom_unit = cls.env.ref('product.product_uom_unit') # Create product template - templateAB = templateObj.create( - {'name': 'templAB', - 'uom_id': uom_unit.id, - }) + cls.templateAB = cls.templateObj.create({ + 'name': 'templAB', + 'uom_id': cls.uom_unit.id, + }) # Create product A and B - productA = productObj.create( - {'name': 'product A', - 'standard_price': 1, - 'type': 'product', - 'uom_id': uom_unit.id, - 'default_code': 'A', - 'product_tmpl_id': templateAB.id, - }) + cls.productA = cls.productObj.create({ + 'name': 'product A', + 'standard_price': 1, + 'type': 'product', + 'uom_id': cls.uom_unit.id, + 'default_code': 'A', + 'product_tmpl_id': cls.templateAB.id, + }) - productB = productObj.create( - {'name': 'product B', - 'standard_price': 1, - 'type': 'product', - 'uom_id': uom_unit.id, - 'default_code': 'B', - 'product_tmpl_id': templateAB.id, - }) + cls.productB = cls.productObj.create({ + 'name': 'product B', + 'standard_price': 1, + 'type': 'product', + 'uom_id': cls.uom_unit.id, + 'default_code': 'B', + 'product_tmpl_id': cls.templateAB.id, + }) # Create a picking move from INCOMING to STOCK - pickingInA = pickingObj.create({ - 'picking_type_id': self.ref('stock.picking_type_in'), - 'location_id': supplier_location.id, - 'location_dest_id': stock_location.id, + cls.pickingInA = cls.pickingObj.create({ + 'picking_type_id': cls.env.ref('stock.picking_type_in').id, + 'location_id': cls.supplier_location.id, + 'location_dest_id': cls.stock_location.id, 'move_lines': [ (0, 0, { 'name': 'Test move', - 'product_id': productA.id, - 'product_uom': productA.uom_id.id, + 'product_id': cls.productA.id, + 'product_uom': cls.productA.uom_id.id, 'product_uom_qty': 2, - 'location_id': supplier_location.id, - 'location_dest_id': stock_location.id, - })] + 'quantity_done': 2, + 'location_id': cls.supplier_location.id, + 'location_dest_id': cls.stock_location.id, + }) + ] }) - pickingInB = pickingObj.create({ - 'picking_type_id': self.ref('stock.picking_type_in'), - 'location_id': supplier_location.id, - 'location_dest_id': stock_location.id, + cls.pickingInB = cls.pickingObj.create({ + 'picking_type_id': cls.env.ref('stock.picking_type_in').id, + 'location_id': cls.supplier_location.id, + 'location_dest_id': cls.stock_location.id, 'move_lines': [ (0, 0, { 'name': 'Test move', - 'product_id': productB.id, - 'product_uom': productB.uom_id.id, + 'product_id': cls.productB.id, + 'product_uom': cls.productB.uom_id.id, 'product_uom_qty': 3, - 'location_id': supplier_location.id, - 'location_dest_id': stock_location.id, - })] + 'quantity_done': 3, + 'location_id': cls.supplier_location.id, + 'location_dest_id': cls.stock_location.id, + }) + ] }) - - def compare_qty_available_not_res(product, value): - # Refresh, because the function field is not recalculated between - # transactions - product.refresh() - self.assertEqual(product.qty_available_not_res, value) - - compare_qty_available_not_res(productA, 0) - compare_qty_available_not_res(templateAB, 0) - - pickingInA.action_confirm() - compare_qty_available_not_res(productA, 0) - compare_qty_available_not_res(templateAB, 0) - - pickingInA.action_assign() - compare_qty_available_not_res(productA, 0) - compare_qty_available_not_res(templateAB, 0) - - pickingInA.action_done() - compare_qty_available_not_res(productA, 2) - compare_qty_available_not_res(templateAB, 2) - - # will directly trigger action_done on productB - pickingInB.action_done() - compare_qty_available_not_res(productA, 2) - compare_qty_available_not_res(productB, 3) - compare_qty_available_not_res(templateAB, 5) - - # Create a picking from STOCK to CUSTOMER - pickingOutA = pickingObj.create({ - 'picking_type_id': self.ref('stock.picking_type_out'), - 'location_id': stock_location.id, - 'location_dest_id': customer_location.id, + cls.pickingOutA = cls.pickingObj.create({ + 'picking_type_id': cls.env.ref('stock.picking_type_out').id, + 'location_id': cls.stock_location.id, + 'location_dest_id': cls.customer_location.id, 'move_lines': [ (0, 0, { 'name': 'Test move', - 'product_id': productB.id, - 'product_uom': productB.uom_id.id, + 'product_id': cls.productB.id, + 'product_uom': cls.productB.uom_id.id, 'product_uom_qty': 2, - 'location_id': stock_location.id, - 'location_dest_id': customer_location.id, - })] + 'location_id': cls.stock_location.id, + 'location_dest_id': cls.customer_location.id, + }) + ] }) - compare_qty_available_not_res(productB, 3) - compare_qty_available_not_res(templateAB, 5) + def compare_qty_available_not_res(self, product, value): + product.invalidate_cache() + self.assertEqual(product.qty_available_not_res, value) - pickingOutA.action_confirm() - compare_qty_available_not_res(productB, 3) - compare_qty_available_not_res(templateAB, 5) + def test_stock_levels(self): + """checking that qty_available_not_res actually reflects \ + the variations in stock, both on product and template""" - pickingOutA.action_assign() - compare_qty_available_not_res(productB, 1) - compare_qty_available_not_res(templateAB, 3) + self.compare_qty_available_not_res(self.productA, 0) + self.compare_qty_available_not_res(self.templateAB, 0) - pickingOutA.action_done() - compare_qty_available_not_res(productB, 1) - compare_qty_available_not_res(templateAB, 3) + self.pickingInA.action_confirm() + self.compare_qty_available_not_res(self.productA, 0) + self.compare_qty_available_not_res(self.templateAB, 0) - templateAB.action_open_quants_unreserved() + self.pickingInA.action_assign() + self.compare_qty_available_not_res(self.productA, 0) + self.compare_qty_available_not_res(self.templateAB, 0) + + self.pickingInA.button_validate() + self.compare_qty_available_not_res(self.productA, 2) + self.compare_qty_available_not_res(self.templateAB, 2) + + # will directly trigger action_done on self.productB + self.pickingInB.action_done() + self.compare_qty_available_not_res(self.productA, 2) + self.compare_qty_available_not_res(self.productB, 3) + self.compare_qty_available_not_res(self.templateAB, 5) + + self.compare_qty_available_not_res(self.productB, 3) + self.compare_qty_available_not_res(self.templateAB, 5) + + self.pickingOutA.action_confirm() + self.compare_qty_available_not_res(self.productB, 3) + self.compare_qty_available_not_res(self.templateAB, 5) + + self.pickingOutA.action_assign() + self.compare_qty_available_not_res(self.productB, 1) + self.compare_qty_available_not_res(self.templateAB, 3) + + self.pickingOutA.action_done() + self.compare_qty_available_not_res(self.productB, 1) + self.compare_qty_available_not_res(self.templateAB, 3) + + self.templateAB.action_open_quants_unreserved() diff --git a/stock_available_unreserved/views/product_view.xml b/stock_available_unreserved/views/product_view.xml index d452199d3..1d714ca54 100644 --- a/stock_available_unreserved/views/product_view.xml +++ b/stock_available_unreserved/views/product_view.xml @@ -63,9 +63,9 @@
diff --git a/stock_available_unreserved/views/stock_quant_view.xml b/stock_available_unreserved/views/stock_quant_view.xml index 8d4b42895..28c8c0486 100644 --- a/stock_available_unreserved/views/stock_quant_view.xml +++ b/stock_available_unreserved/views/stock_quant_view.xml @@ -1,29 +1,29 @@ - - stock.quant.search - stock.quant - - - - - - - - - + + stock.quant.search + stock.quant + + + + + - + + + + + - - Stock On Hand (Unreserved) - {'search_default_internal_loc': 1, 'search_default_locationgroup':1} - [('product_id', '=', active_id), ('reservation_id', '=', False)] - stock.quant - + + Stock On Hand (Unreserved) + {'search_default_internal_loc': 1, 'search_default_locationgroup':1} + [('product_id', '=', active_id), ('contains_unreserved', '=', True)] + stock.quant +