diff --git a/setup/stock_orderpoint_generator/odoo/addons/stock_orderpoint_generator b/setup/stock_orderpoint_generator/odoo/addons/stock_orderpoint_generator new file mode 120000 index 000000000..280f25ec6 --- /dev/null +++ b/setup/stock_orderpoint_generator/odoo/addons/stock_orderpoint_generator @@ -0,0 +1 @@ +../../../../stock_orderpoint_generator \ No newline at end of file diff --git a/setup/stock_orderpoint_generator/setup.py b/setup/stock_orderpoint_generator/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/stock_orderpoint_generator/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_orderpoint_generator/__manifest__.py b/stock_orderpoint_generator/__manifest__.py index 8c060ed6e..0812ce78e 100644 --- a/stock_orderpoint_generator/__manifest__.py +++ b/stock_orderpoint_generator/__manifest__.py @@ -3,22 +3,20 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { - 'name': 'Order point generator', - 'summary': 'Mass configuration of stock order points', - 'version': '12.0.1.0.0', - 'author': "Camptocamp, " - "Tecnativa, " - "Odoo Community Association (OCA)", - 'category': 'Warehouse', - 'license': 'AGPL-3', - 'website': "https://github.com/OCA/stock-logistics-warehouse", - 'depends': ['stock'], - 'data': [ - 'views/orderpoint_template_views.xml', + "name": "Order point generator", + "summary": "Mass configuration of stock order points", + "version": "12.0.1.0.0", + "author": "Camptocamp, " "Tecnativa, " "Odoo Community Association (OCA)", + "category": "Warehouse", + "license": "AGPL-3", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "depends": ["stock"], + "data": [ + "views/orderpoint_template_views.xml", "wizard/orderpoint_generator_view.xml", "data/ir_cron.xml", "security/ir.model.access.csv", ], - 'installable': True, - 'auto_install': False, + "installable": True, + "auto_install": False, } diff --git a/stock_orderpoint_generator/data/ir_cron.xml b/stock_orderpoint_generator/data/ir_cron.xml index 3804f0cdf..85f4902aa 100644 --- a/stock_orderpoint_generator/data/ir_cron.xml +++ b/stock_orderpoint_generator/data/ir_cron.xml @@ -1,15 +1,13 @@ - + - Reordering Rule Templates Generator 1 hours -1 - - + + model._cron_create_auto_orderpoints() - diff --git a/stock_orderpoint_generator/models/orderpoint_template.py b/stock_orderpoint_generator/models/orderpoint_template.py index 0d0b1f7b7..b02e3f040 100644 --- a/stock_orderpoint_generator/models/orderpoint_template.py +++ b/stock_orderpoint_generator/models/orderpoint_template.py @@ -3,9 +3,10 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, fields, models from statistics import mean, median_high +from odoo import api, fields, models + class OrderpointTemplate(models.Model): """ Template for orderpoints @@ -18,11 +19,12 @@ class OrderpointTemplate(models.Model): _table is redefined to separate templates from orderpoints """ - _name = 'stock.warehouse.orderpoint.template' - _description = 'Reordering Rule Templates' - _inherit = 'stock.warehouse.orderpoint' - _table = 'stock_warehouse_orderpoint_template' + _name = "stock.warehouse.orderpoint.template" + _description = "Reordering Rule Templates" + + _inherit = "stock.warehouse.orderpoint" + _table = "stock_warehouse_orderpoint_template" name = fields.Char(copy=True) group_id = fields.Many2one(copy=True) @@ -33,137 +35,138 @@ class OrderpointTemplate(models.Model): auto_min_qty = fields.Boolean( string="Auto Minimum", - help="Auto compute minimum quantity " - "per product for a given a date range", + help="Auto compute minimum quantity " "per product for a given a date range", ) auto_min_date_start = fields.Datetime() auto_min_date_end = fields.Datetime() auto_min_qty_criteria = fields.Selection( selection=[ - ('max', 'Maximum'), - ('median', 'Most frequent'), - ('avg', 'Average'), - ('min', 'Minimum'), + ("max", "Maximum"), + ("median", "Most frequent"), + ("avg", "Average"), + ("min", "Minimum"), ], - default='max', + default="max", help="Select a criteria to auto compute the minimum", ) auto_max_qty = fields.Boolean( string="Auto Maximum", - help="Auto compute maximum quantity " - "per product for a given a date range", + help="Auto compute maximum quantity " "per product for a given a date range", ) auto_max_qty_criteria = fields.Selection( selection=[ - ('max', 'Maximum'), - ('median', 'Most frequent'), - ('avg', 'Average'), - ('min', 'Minimum'), + ("max", "Maximum"), + ("median", "Most frequent"), + ("avg", "Average"), + ("min", "Minimum"), ], help="Select a criteria to auto compute the maximum", ) auto_max_date_start = fields.Datetime() auto_max_date_end = fields.Datetime() auto_generate = fields.Boolean( - string='Create Rules Automatically', + string="Create Rules Automatically", help="When checked, the 'Reordering Rule Templates Generator' " - "scheduled action will automatically update the rules of a " - "selection of products." + "scheduled action will automatically update the rules of a " + "selection of products.", ) auto_product_ids = fields.Many2many( - comodel_name='product.product', - string='Products', + comodel_name="product.product", + string="Products", help="A reordering rule will be automatically created by the " - "scheduled action for every product in this list." + "scheduled action for every product in this list.", ) - auto_last_generation = fields.Datetime(string='Last Automatic Generation') + auto_last_generation = fields.Datetime(string="Last Automatic Generation") def _template_fields_to_discard(self): """In order to create every orderpoint we should pop this template customization fields """ return [ - 'auto_generate', 'auto_product_ids', 'auto_last_generation', - 'auto_min_qty', 'auto_min_date_start', 'auto_min_qty_criteria', - 'auto_min_date_end', 'auto_max_date_start', 'auto_max_date_end', - 'auto_max_qty_criteria', 'auto_max_qty', + "auto_generate", + "auto_product_ids", + "auto_last_generation", + "auto_min_qty", + "auto_min_date_start", + "auto_min_qty_criteria", + "auto_min_date_end", + "auto_max_date_start", + "auto_max_date_end", + "auto_max_qty_criteria", + "auto_max_qty", ] def _disable_old_instances(self, products): """Clean old instance by setting those inactives""" - orderpoints = self.env['stock.warehouse.orderpoint'].search( - [('product_id', 'in', products.ids)] + orderpoints = self.env["stock.warehouse.orderpoint"].search( + [("product_id", "in", products.ids)] ) - orderpoints.write({'active': False}) + orderpoints.write({"active": False}) @api.model def _get_criteria_methods(self): """Allows to extend methods with other statistical aproaches""" return { - 'max': max, - 'median': median_high, - 'avg': mean, - 'min': min, + "max": max, + "median": median_high, + "avg": mean, + "min": min, } @api.model def _get_product_qty_by_criteria( - self, products, location_id, from_date, to_date, criteria): + self, products, location_id, from_date, to_date, criteria + ): """Returns a dict with product ids as keys and the resulting calculation of historic moves according to criteria""" stock_qty_history = products._compute_historic_quantities_dict( - location_id=location_id, - from_date=from_date, - to_date=to_date) + location_id=location_id, from_date=from_date, to_date=to_date + ) criteria_methods = self._get_criteria_methods() - return {x: criteria_methods[criteria](y['stock_history']) - for x, y in stock_qty_history.items()} + return { + x: criteria_methods[criteria](y["stock_history"]) + for x, y in stock_qty_history.items() + } def _create_instances(self, product_ids): """Create instances of model using template inherited model and compute autovalues if needed""" - orderpoint_model = self.env['stock.warehouse.orderpoint'] + orderpoint_model = self.env["stock.warehouse.orderpoint"] for record in self: # Flag equality so we compute the values just once auto_same_values = ( - record.auto_max_date_start == record.auto_min_date_start - ) and ( - record.auto_max_date_end == record.auto_max_date_end - ) and ( - record.auto_max_qty_criteria == - record.auto_min_qty_criteria) + (record.auto_max_date_start == record.auto_min_date_start) + and (record.auto_max_date_end == record.auto_max_date_end) + and (record.auto_max_qty_criteria == record.auto_min_qty_criteria) + ) stock_min_qty = stock_max_qty = {} if record.auto_min_qty: - stock_min_qty = ( - self._get_product_qty_by_criteria( - product_ids, - location_id=record.location_id, - from_date=record.auto_min_date_start, - to_date=record.auto_min_date_end, - criteria=record.auto_min_qty_criteria, - )) + stock_min_qty = self._get_product_qty_by_criteria( + product_ids, + location_id=record.location_id, + from_date=record.auto_min_date_start, + to_date=record.auto_min_date_end, + criteria=record.auto_min_qty_criteria, + ) if auto_same_values: stock_max_qty = stock_min_qty if record.auto_max_qty and not stock_max_qty: - stock_max_qty = ( - self._get_product_qty_by_criteria( - product_ids, - location_id=record.location_id, - from_date=record.auto_max_date_start, - to_date=record.auto_max_date_end, - criteria=record.auto_max_qty_criteria, - )) + stock_max_qty = self._get_product_qty_by_criteria( + product_ids, + location_id=record.location_id, + from_date=record.auto_max_date_start, + to_date=record.auto_max_date_end, + criteria=record.auto_max_qty_criteria, + ) for data in record.copy_data(): for discard_field in self._template_fields_to_discard(): data.pop(discard_field) for product_id in product_ids: vals = data.copy() - vals['product_id'] = product_id.id + vals["product_id"] = product_id.id if record.auto_min_qty: - vals['product_min_qty'] = stock_min_qty.get( - product_id.id, 0) + vals["product_min_qty"] = stock_min_qty.get(product_id.id, 0) if record.auto_max_qty: - vals['product_max_qty'] = stock_max_qty.get( - product_id.id, 0) + vals["product_max_qty"] = stock_max_qty.get(product_id.id, 0) orderpoint_model.create(vals) @api.multi @@ -179,11 +182,13 @@ class OrderpointTemplate(models.Model): for template in self: if not template.auto_generate: continue - if (not template.auto_last_generation or - template.write_date > template.auto_last_generation): + if ( + not template.auto_last_generation + or template.write_date > template.auto_last_generation + ): template.auto_last_generation = fields.Datetime.now() template.create_orderpoints(template.auto_product_ids) @api.model def _cron_create_auto_orderpoints(self): - self.search([('auto_generate', '=', True)]).create_auto_orderpoints() + self.search([("auto_generate", "=", True)]).create_auto_orderpoints() diff --git a/stock_orderpoint_generator/models/product.py b/stock_orderpoint_generator/models/product.py index 66b42010b..e589bea43 100644 --- a/stock_orderpoint_generator/models/product.py +++ b/stock_orderpoint_generator/models/product.py @@ -2,72 +2,75 @@ # Copyright 2019 Tecnativa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from odoo import api, fields, models from collections import OrderedDict +from odoo import api, fields, models + class ProductProduct(models.Model): - _inherit = 'product.product' + _inherit = "product.product" auto_orderpoint_template_ids = fields.Many2many( - comodel_name='stock.warehouse.orderpoint.template', + comodel_name="stock.warehouse.orderpoint.template", string="Automatic Reordering Rules", - domain=[('auto_generate', '=', True)], + domain=[("auto_generate", "=", True)], help="When one or several automatic reordering rule is selected, " - "a Scheduled Action will automatically generate or update " - "the reordering rules of the product." + "a Scheduled Action will automatically generate or update " + "the reordering rules of the product.", ) @api.model def create(self, vals): record = super().create(vals) - if vals.get('auto_orderpoint_template_ids'): + if vals.get("auto_orderpoint_template_ids"): record.auto_orderpoint_template_ids.create_orderpoints(record) return record @api.multi def write(self, vals): result = super().write(vals) - if vals.get('auto_orderpoint_template_ids'): - orderpoint_templates = self.mapped('auto_orderpoint_template_ids') + if vals.get("auto_orderpoint_template_ids"): + orderpoint_templates = self.mapped("auto_orderpoint_template_ids") orderpoint_templates.create_orderpoints(self) return result def _compute_historic_quantities_dict( - self, location_id=False, from_date=False, to_date=False): + self, location_id=False, from_date=False, to_date=False + ): """Returns a dict of products with a dict of historic moves as for a list of historic stock values resulting from those moves. If a location_id is passed, we can restrict it to such location""" location = location_id and location_id.id - domain_quant_loc, domain_move_in_loc, domain_move_out_loc = ( - self.with_context(location=location)._get_domain_locations()) + domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self.with_context( + location=location + )._get_domain_locations() if not to_date: to_date = fields.Datetime.now() - domain_move_in = domain_move_out = ([ - ('product_id', 'in', self.ids), - ('state', '=', 'done'), - ] + domain_move_in_loc) - domain_move_out = ([ - ('product_id', 'in', self.ids), - ('state', '=', 'done'), - ] + domain_move_out_loc) + domain_move_in = domain_move_out = [ + ("product_id", "in", self.ids), + ("state", "=", "done"), + ] + domain_move_in_loc + domain_move_out = [ + ("product_id", "in", self.ids), + ("state", "=", "done"), + ] + domain_move_out_loc if from_date: - domain_move_in += [('date', '>=', from_date)] - domain_move_out += [('date', '>=', from_date)] - domain_move_in += [('date', '<=', to_date)] - domain_move_out += [('date', '<=', to_date)] - move_obj = self.env['stock.move'] + domain_move_in += [("date", ">=", from_date)] + domain_move_out += [("date", ">=", from_date)] + domain_move_in += [("date", "<=", to_date)] + domain_move_out += [("date", "<=", to_date)] + move_obj = self.env["stock.move"] # Positive moves moves_in = move_obj.search_read( - domain_move_in, ['product_id', 'product_qty', 'date'], - order='date asc') + domain_move_in, ["product_id", "product_qty", "date"], order="date asc" + ) # We'll convert to negative these quantities to operate with them # to obtain the stock snapshot in every moment moves_out = move_obj.search_read( - domain_move_out, ['product_id', 'product_qty', 'date'], - order='date asc') + domain_move_out, ["product_id", "product_qty", "date"], order="date asc" + ) for move in moves_out: - move['product_qty'] *= -1 + move["product_qty"] *= -1 # Merge both results and group them by product id as key moves = moves_in + moves_out # Obtain a dict with the stock snapshot for the relative date_from @@ -75,17 +78,15 @@ class ProductProduct(models.Model): # default the compute the stock value anyway to default the value # for products with no moves for the given period initial_stock = {} - initial_stock = self.with_context( - location=location)._compute_quantities_dict( - False, False, False, to_date=from_date or to_date) + initial_stock = self.with_context(location=location)._compute_quantities_dict( + False, False, False, to_date=from_date or to_date + ) product_moves_dict = {} for move in moves: - product_moves_dict.setdefault(move['product_id'][0], {}) - product_moves_dict[move['product_id'][0]].update({ - move['date']: { - 'prod_qty': move['product_qty'], - } - }) + product_moves_dict.setdefault(move["product_id"][0], {}) + product_moves_dict[move["product_id"][0]].update( + {move["date"]: {"prod_qty": move["product_qty"],}} + ) for product in self.with_context(prefetch_fields=False): # If no there are no moves for a product we default the stock # to the one for the given period nevermind the dates @@ -94,11 +95,10 @@ class ProductProduct(models.Model): if not product_moves: product_moves_dict[product.id] = { to_date: { - 'prod_qty': 0, - 'stock': prod_initial_stock.get('qty_available', 0), + "prod_qty": 0, + "stock": prod_initial_stock.get("qty_available", 0), }, - 'stock_history': [ - prod_initial_stock.get('qty_available', 0)], + "stock_history": [prod_initial_stock.get("qty_available", 0)], } continue # Now we'll sort the moves by date and assign an initial stock so @@ -108,15 +108,16 @@ class ProductProduct(models.Model): stock = False first_item = product_moves[next(iter(product_moves))] if from_date: - stock = prod_initial_stock.get('qty_available') + stock = prod_initial_stock.get("qty_available") if not stock: - stock = first_item['prod_qty'] - first_item['stock'] = stock + stock = first_item["prod_qty"] + first_item["stock"] = stock iter_moves = iter(product_moves) next(iter_moves, None) for date in iter_moves: - stock += product_moves[date]['prod_qty'] - product_moves[date]['stock'] = stock - product_moves_dict[product.id]['stock_history'] = ( - [v['stock'] for k, v in product_moves.items()]) + stock += product_moves[date]["prod_qty"] + product_moves[date]["stock"] = stock + product_moves_dict[product.id]["stock_history"] = [ + v["stock"] for k, v in product_moves.items() + ] return product_moves_dict diff --git a/stock_orderpoint_generator/tests/test_orderpoint_generator.py b/stock_orderpoint_generator/tests/test_orderpoint_generator.py index a39d70a2d..8eb501ae7 100644 --- a/stock_orderpoint_generator/tests/test_orderpoint_generator.py +++ b/stock_orderpoint_generator/tests/test_orderpoint_generator.py @@ -7,242 +7,270 @@ from odoo.tests.common import SavepointCase class TestOrderpointGenerator(SavepointCase): - @classmethod def setUpClass(cls): super().setUpClass() - cls.wizard_model = cls.env['stock.warehouse.orderpoint.generator'] - cls.orderpoint_model = cls.env['stock.warehouse.orderpoint'] - cls.orderpoint_template_model = ( - cls.env['stock.warehouse.orderpoint.template']) - cls.product_model = cls.env['product.product'] - cls.p1 = cls.product_model.create({ - 'name': 'Unittest P1', - 'type': 'product', - }) - cls.p2 = cls.product_model.create({ - 'name': 'Unittest P2', - 'type': 'product', - }) - cls.wh1 = cls.env['stock.warehouse'].create({ - 'name': 'TEST WH1', - 'code': 'TST1', - }) - location_obj = cls.env['stock.location'] - cls.supplier_loc = location_obj.create({ - 'name': 'Test supplier location', - 'usage': 'supplier', - }) - cls.customer_loc = location_obj.create({ - 'name': 'Test customer location', - 'usage': 'customer', - }) + cls.wizard_model = cls.env["stock.warehouse.orderpoint.generator"] + cls.orderpoint_model = cls.env["stock.warehouse.orderpoint"] + cls.orderpoint_template_model = cls.env["stock.warehouse.orderpoint.template"] + cls.product_model = cls.env["product.product"] + cls.p1 = cls.product_model.create({"name": "Unittest P1", "type": "product",}) + cls.p2 = cls.product_model.create({"name": "Unittest P2", "type": "product",}) + cls.wh1 = cls.env["stock.warehouse"].create( + {"name": "TEST WH1", "code": "TST1",} + ) + location_obj = cls.env["stock.location"] + cls.supplier_loc = location_obj.create( + {"name": "Test supplier location", "usage": "supplier",} + ) + cls.customer_loc = location_obj.create( + {"name": "Test customer location", "usage": "customer",} + ) cls.orderpoint_fields_dict = { - 'warehouse_id': cls.wh1.id, - 'location_id': cls.wh1.lot_stock_id.id, - 'name': 'TEST-ORDERPOINT-001', - 'product_max_qty': 15.0, - 'product_min_qty': 5.0, - 'qty_multiple': 1, + "warehouse_id": cls.wh1.id, + "location_id": cls.wh1.lot_stock_id.id, + "name": "TEST-ORDERPOINT-001", + "product_max_qty": 15.0, + "product_min_qty": 5.0, + "qty_multiple": 1, } - cls.template = cls.orderpoint_template_model.create( - cls.orderpoint_fields_dict) + cls.template = cls.orderpoint_template_model.create(cls.orderpoint_fields_dict) # Create some moves for p1 and p2 so we can have a history to test # p1 [100, 50, 45, 55, 52] # t1 - p1 - stock.move location1 100 # 100 - cls.p1m1 = cls.env['stock.move'].create({ - 'name': cls.p1.name, - 'product_id': cls.p1.id, - 'product_uom_qty': 100, - 'product_uom': cls.p1.uom_id.id, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 01:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p1m1.id, - 'product_id': cls.p1.id, - 'qty_done': 100, - 'product_uom_id': cls.p1.uom_id.id, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 01:00:00', - }) + cls.p1m1 = cls.env["stock.move"].create( + { + "name": cls.p1.name, + "product_id": cls.p1.id, + "product_uom_qty": 100, + "product_uom": cls.p1.uom_id.id, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 01:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p1m1.id, + "product_id": cls.p1.id, + "qty_done": 100, + "product_uom_id": cls.p1.uom_id.id, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 01:00:00", + } + ) # t2 - p1 - stock.move location1 -50 # 50 - cls.p1m2 = cls.p1m1.copy({ - 'product_uom_qty': 50, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 02:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p1m2.id, - 'product_id': cls.p1.id, - 'qty_done': 50, - 'product_uom_id': cls.p1.uom_id.id, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 02:00:00', - }) + cls.p1m2 = cls.p1m1.copy( + { + "product_uom_qty": 50, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 02:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p1m2.id, + "product_id": cls.p1.id, + "qty_done": 50, + "product_uom_id": cls.p1.uom_id.id, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 02:00:00", + } + ) # t3 - p1 - stock.move location1 -5 # 45 - cls.p1m3 = cls.p1m1.copy({ - 'name': cls.p1.name, - 'product_id': cls.p1.id, - 'product_uom_qty': 5, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 03:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p1m3.id, - 'product_id': cls.p1.id, - 'qty_done': 5, - 'product_uom_id': cls.p1.uom_id.id, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 03:00:00', - }) + cls.p1m3 = cls.p1m1.copy( + { + "name": cls.p1.name, + "product_id": cls.p1.id, + "product_uom_qty": 5, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 03:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p1m3.id, + "product_id": cls.p1.id, + "qty_done": 5, + "product_uom_id": cls.p1.uom_id.id, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 03:00:00", + } + ) # t4 - p1 - stock.move location1 10 # 55 - cls.p1m4 = cls.p1m1.copy({ - 'product_uom_qty': 10, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 04:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p1m4.id, - 'product_id': cls.p1.id, - 'qty_done': 10, - 'product_uom_id': cls.p1.uom_id.id, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 04:00:00', - }) + cls.p1m4 = cls.p1m1.copy( + { + "product_uom_qty": 10, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 04:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p1m4.id, + "product_id": cls.p1.id, + "qty_done": 10, + "product_uom_id": cls.p1.uom_id.id, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 04:00:00", + } + ) # t5 - p1 - stock.move location1 -3 # 52 - cls.p1m5 = cls.p1m1.copy({ - 'product_uom_qty': 3, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 05:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p1m5.id, - 'product_id': cls.p1.id, - 'qty_done': 3, - 'product_uom_id': cls.p1.uom_id.id, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 05:00:00', - }) + cls.p1m5 = cls.p1m1.copy( + { + "product_uom_qty": 3, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 05:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p1m5.id, + "product_id": cls.p1.id, + "qty_done": 3, + "product_uom_id": cls.p1.uom_id.id, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 05:00:00", + } + ) # p2 # t1 - p2 - stock.move location1 1000 # 1000 - cls.p2m1 = cls.env['stock.move'].create({ - 'name': cls.p2.name, - 'product_id': cls.p2.id, - 'product_uom': cls.p2.uom_id.id, - 'product_uom_qty': 1000, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 01:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p2m1.id, - 'product_id': cls.p2.id, - 'qty_done': 1000, - 'product_uom_id': cls.p2.uom_id.id, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 01:00:00', - }) + cls.p2m1 = cls.env["stock.move"].create( + { + "name": cls.p2.name, + "product_id": cls.p2.id, + "product_uom": cls.p2.uom_id.id, + "product_uom_qty": 1000, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 01:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p2m1.id, + "product_id": cls.p2.id, + "qty_done": 1000, + "product_uom_id": cls.p2.uom_id.id, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 01:00:00", + } + ) # t2 - p2 - stock.move location1 -50 # 950 - cls.p2m2 = cls.p2m1.copy({ - 'product_uom_qty': 50, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 02:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p2m2.id, - 'product_id': cls.p2.id, - 'qty_done': 50, - 'product_uom_id': cls.p2.uom_id.id, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 02:00:00', - }) + cls.p2m2 = cls.p2m1.copy( + { + "product_uom_qty": 50, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 02:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p2m2.id, + "product_id": cls.p2.id, + "qty_done": 50, + "product_uom_id": cls.p2.uom_id.id, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 02:00:00", + } + ) # t3 - p2 - stock.move location1 -7 # 943 - cls.p2m3 = cls.p2m1.copy({ - 'product_uom_qty': 7, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 03:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p2m3.id, - 'product_id': cls.p2.id, - 'qty_done': 7, - 'product_uom_id': cls.p2.uom_id.id, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 03:00:00', - }) + cls.p2m3 = cls.p2m1.copy( + { + "product_uom_qty": 7, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 03:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p2m3.id, + "product_id": cls.p2.id, + "qty_done": 7, + "product_uom_id": cls.p2.uom_id.id, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 03:00:00", + } + ) # t4 - p2 - stock.move location1 100 # 1043 - cls.p2m4 = cls.p2m1.copy({ - 'product_uom_qty': 100, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 04:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p2m4.id, - 'product_id': cls.p2.id, - 'qty_done': 100, - 'product_uom_id': cls.p2.uom_id.id, - 'location_id': cls.supplier_loc.id, - 'location_dest_id': cls.wh1.lot_stock_id.id, - 'state': 'done', - 'date': '2019-01-01 04:00:00', - }) + cls.p2m4 = cls.p2m1.copy( + { + "product_uom_qty": 100, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 04:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p2m4.id, + "product_id": cls.p2.id, + "qty_done": 100, + "product_uom_id": cls.p2.uom_id.id, + "location_id": cls.supplier_loc.id, + "location_dest_id": cls.wh1.lot_stock_id.id, + "state": "done", + "date": "2019-01-01 04:00:00", + } + ) # t5 - p2 - stock.move location1 -3 # 1040 - cls.p2m5 = cls.p2m1.copy({ - 'product_uom_qty': 3, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 05:00:00', - }) - cls.env['stock.move.line'].create({ - 'move_id': cls.p2m5.id, - 'product_id': cls.p2.id, - 'qty_done': 3, - 'product_uom_id': cls.p2.uom_id.id, - 'location_id': cls.wh1.lot_stock_id.id, - 'location_dest_id': cls.customer_loc.id, - 'state': 'done', - 'date': '2019-01-01 05:00:00', - }) + cls.p2m5 = cls.p2m1.copy( + { + "product_uom_qty": 3, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 05:00:00", + } + ) + cls.env["stock.move.line"].create( + { + "move_id": cls.p2m5.id, + "product_id": cls.p2.id, + "qty_done": 3, + "product_uom_id": cls.p2.uom_id.id, + "location_id": cls.wh1.lot_stock_id.id, + "location_dest_id": cls.customer_loc.id, + "state": "done", + "date": "2019-01-01 05:00:00", + } + ) def check_orderpoint(self, products, template, fields_dict): - orderpoints = self.orderpoint_model.search([ - ('name', '=', template.name) - ], order='product_id') + orderpoints = self.orderpoint_model.search( + [("name", "=", template.name)], order="product_id" + ) self.assertEqual(len(products), len(orderpoints)) for i, product in enumerate(products): self.assertEqual(product, orderpoints[i].product_id) @@ -256,135 +284,139 @@ class TestOrderpointGenerator(SavepointCase): def wizard_over_products(self, product, template): return self.wizard_model.with_context( - active_model=product._name, - active_ids=product.ids, - ).create({ - 'orderpoint_template_id': [(6, 0, template.ids)] - }) + active_model=product._name, active_ids=product.ids, + ).create({"orderpoint_template_id": [(6, 0, template.ids)]}) def test_product_orderpoint(self): products = self.p1 + self.p2 wizard = self.wizard_over_products(products, self.template) wizard.action_configure() - self.check_orderpoint( - products, self.template, self.orderpoint_fields_dict) + self.check_orderpoint(products, self.template, self.orderpoint_fields_dict) def test_template_orderpoint(self): prod_tmpl = self.p1.product_tmpl_id + self.p2.product_tmpl_id wizard = self.wizard_over_products(prod_tmpl, self.template) wizard.action_configure() products = self.p1 + self.p2 - self.check_orderpoint( - products, self.template, self.orderpoint_fields_dict) + self.check_orderpoint(products, self.template, self.orderpoint_fields_dict) def test_template_variants_orderpoint(self): - self.product_model.create({ - 'product_tmpl_id': self.p1.product_tmpl_id.id, - 'name': 'Unittest P1 variant' - }) - wizard = self.wizard_over_products( - self.p1.product_tmpl_id, self.template) + self.product_model.create( + { + "product_tmpl_id": self.p1.product_tmpl_id.id, + "name": "Unittest P1 variant", + } + ) + wizard = self.wizard_over_products(self.p1.product_tmpl_id, self.template) with self.assertRaises(UserError): wizard.action_configure() def test_auto_qty(self): """Compute min and max qty according to criteria""" # Max stock for p1: 100 - self.template.write({ - 'auto_min_qty': True, - 'auto_min_date_start': '2019-01-01 00:00:00', - 'auto_min_date_end': '2019-02-01 00:00:00', - 'auto_min_qty_criteria': 'max', - }) + self.template.write( + { + "auto_min_qty": True, + "auto_min_date_start": "2019-01-01 00:00:00", + "auto_min_date_end": "2019-02-01 00:00:00", + "auto_min_qty_criteria": "max", + } + ) wizard = self.wizard_over_products(self.p1, self.template) wizard.action_configure() orderpoint_auto_dict = self.orderpoint_fields_dict.copy() - orderpoint_auto_dict.update({ - 'product_min_qty': 100.0, - }) + orderpoint_auto_dict.update( + {"product_min_qty": 100.0,} + ) self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict) # Min stock for p1: 45 - self.template.write({ - 'auto_min_qty_criteria': 'min', - }) + self.template.write( + {"auto_min_qty_criteria": "min",} + ) wizard = self.wizard_over_products(self.p1, self.template) wizard.action_configure() - orderpoint_auto_dict.update({ - 'product_min_qty': 45.0, - }) + orderpoint_auto_dict.update( + {"product_min_qty": 45.0,} + ) self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict) # Median of stock for p1: 52 - self.template.write({ - 'auto_min_qty_criteria': 'median', - }) + self.template.write( + {"auto_min_qty_criteria": "median",} + ) wizard = self.wizard_over_products(self.p1, self.template) wizard.action_configure() - orderpoint_auto_dict.update({ - 'product_min_qty': 52.0, - }) + orderpoint_auto_dict.update( + {"product_min_qty": 52.0,} + ) self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict) # Average of stock for p1: 60.4 - self.template.write({ - 'auto_min_qty_criteria': 'avg', - }) + self.template.write( + {"auto_min_qty_criteria": "avg",} + ) wizard = self.wizard_over_products(self.p1, self.template) wizard.action_configure() - orderpoint_auto_dict.update({ - 'product_min_qty': 60.4, - }) + orderpoint_auto_dict.update( + {"product_min_qty": 60.4,} + ) self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict) # Set auto values for min and max: 60.4 (avg) 100 (max) - self.template.write({ - 'auto_max_qty': True, - 'auto_max_date_start': '2019-01-01 00:00:00', - 'auto_max_date_end': '2019-02-01 00:00:00', - 'auto_max_qty_criteria': 'max', - }) + self.template.write( + { + "auto_max_qty": True, + "auto_max_date_start": "2019-01-01 00:00:00", + "auto_max_date_end": "2019-02-01 00:00:00", + "auto_max_qty_criteria": "max", + } + ) wizard = self.wizard_over_products(self.p1, self.template) wizard.action_configure() - orderpoint_auto_dict.update({ - 'product_max_qty': 100, - }) + orderpoint_auto_dict.update( + {"product_max_qty": 100,} + ) self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict) # If they have the same values, only one is computed: - self.template.write({ - 'auto_min_qty_criteria': 'max', - }) + self.template.write( + {"auto_min_qty_criteria": "max",} + ) wizard = self.wizard_over_products(self.p1, self.template) wizard.action_configure() - orderpoint_auto_dict.update({ - 'product_min_qty': 100, - }) + orderpoint_auto_dict.update( + {"product_min_qty": 100,} + ) self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict) # Auto min max over a shorter period - self.template.write({ - 'auto_max_date_start': '2019-01-01 02:00:00', - 'auto_max_date_end': '2019-01-01 03:00:00', - 'auto_min_date_start': '2019-01-01 04:00:00', - 'auto_min_date_end': '2019-01-01 06:00:00', - }) + self.template.write( + { + "auto_max_date_start": "2019-01-01 02:00:00", + "auto_max_date_end": "2019-01-01 03:00:00", + "auto_min_date_start": "2019-01-01 04:00:00", + "auto_min_date_end": "2019-01-01 06:00:00", + } + ) wizard = self.wizard_over_products(self.p1, self.template) wizard.action_configure() - orderpoint_auto_dict.update({ - 'product_min_qty': 55, - 'product_max_qty': 50, - }) + orderpoint_auto_dict.update( + {"product_min_qty": 55, "product_max_qty": 50,} + ) self.check_orderpoint(self.p1, self.template, orderpoint_auto_dict) def test_auto_qty_multi_products(self): """Each product has a different history""" products = self.p1 + self.p2 - self.template.write({ - 'auto_min_qty': True, - 'auto_min_date_start': '2019-01-01 00:00:00', - 'auto_min_date_end': '2019-02-01 00:00:00', - 'auto_min_qty_criteria': 'max', - }) + self.template.write( + { + "auto_min_qty": True, + "auto_min_date_start": "2019-01-01 00:00:00", + "auto_min_date_end": "2019-02-01 00:00:00", + "auto_min_qty_criteria": "max", + } + ) wizard = self.wizard_over_products(products, self.template) wizard.action_configure() orderpoint_auto_dict = self.orderpoint_fields_dict.copy() - del orderpoint_auto_dict['product_min_qty'] + del orderpoint_auto_dict["product_min_qty"] orderpoints = self.check_orderpoint( - products, self.template, orderpoint_auto_dict) + products, self.template, orderpoint_auto_dict + ) self.assertEqual(orderpoints[0].product_min_qty, 100) self.assertEqual(orderpoints[1].product_min_qty, 1043) diff --git a/stock_orderpoint_generator/views/orderpoint_template_views.xml b/stock_orderpoint_generator/views/orderpoint_template_views.xml index 6560e1b47..10a3d5394 100644 --- a/stock_orderpoint_generator/views/orderpoint_template_views.xml +++ b/stock_orderpoint_generator/views/orderpoint_template_views.xml @@ -1,51 +1,68 @@ - + - stock.warehouse.orderpoint.template.tree stock.warehouse.orderpoint.template primary - - - - - - - - + + + + + + + + - stock.warehouse.orderpoint.template.search stock.warehouse.orderpoint.template primary - - - - + + + + - - + + - stock.warehouse.orderpoint.template.form stock.warehouse.orderpoint.template
-
@@ -53,58 +70,105 @@ - - - - + + + + - - - - - + + + + + - - - - - - + + + + - - - - + + + + - +
-
- Reordering Rule Templates stock.warehouse.orderpoint.template ir.actions.act_window form tree,form - +

@@ -112,11 +176,10 @@

- -
diff --git a/stock_orderpoint_generator/wizard/orderpoint_generator.py b/stock_orderpoint_generator/wizard/orderpoint_generator.py index b83eaf8aa..76edb0c17 100644 --- a/stock_orderpoint_generator/wizard/orderpoint_generator.py +++ b/stock_orderpoint_generator/wizard/orderpoint_generator.py @@ -3,10 +3,9 @@ from odoo import _, api, fields, models - from odoo.exceptions import UserError -_template_register = ['orderpoint_template_id'] +_template_register = ["orderpoint_template_id"] class OrderpointGenerator(models.TransientModel): @@ -14,29 +13,31 @@ class OrderpointGenerator(models.TransientModel): products. Those configs are generated using templates """ - _name = 'stock.warehouse.orderpoint.generator' - _description = 'Orderpoint Generator' + _name = "stock.warehouse.orderpoint.generator" + _description = "Orderpoint Generator" orderpoint_template_id = fields.Many2many( - comodel_name='stock.warehouse.orderpoint.template', - relation='order_point_generator_rel', - string='Reordering Rule Templates' + comodel_name="stock.warehouse.orderpoint.template", + relation="order_point_generator_rel", + string="Reordering Rule Templates", ) @api.multi def action_configure(self): """Action to retrieve wizard data and launch creation of items.""" self.ensure_one() - model_obj = self.env[self.env.context.get('active_model')] - record_ids = model_obj.browse(self.env.context.get('active_ids')) + model_obj = self.env[self.env.context.get("active_model")] + record_ids = model_obj.browse(self.env.context.get("active_ids")) if not record_ids: return model_obj - if self.env.context.get('active_model') == 'product.template': - product_ids = record_ids.mapped('product_variant_ids') + if self.env.context.get("active_model") == "product.template": + product_ids = record_ids.mapped("product_variant_ids") if len(product_ids) != len(record_ids): - raise UserError(_( - 'Cannot apply because some of selected ' - 'products has multiple variants.' - )) + raise UserError( + _( + "Cannot apply because some of selected " + "products has multiple variants." + ) + ) record_ids = product_ids self.orderpoint_template_id.create_orderpoints(record_ids) diff --git a/stock_orderpoint_generator/wizard/orderpoint_generator_view.xml b/stock_orderpoint_generator/wizard/orderpoint_generator_view.xml index 405e054a4..1e1b1aa80 100644 --- a/stock_orderpoint_generator/wizard/orderpoint_generator_view.xml +++ b/stock_orderpoint_generator/wizard/orderpoint_generator_view.xml @@ -1,37 +1,45 @@ - + - stock.warehouse.orderpoint.generator stock.warehouse.orderpoint.generator
-
- - - - - + +