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
-
-
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
-
-
-
-
-
+
+