[11.0][MIG] stock_move_location: fixing/improving code + add a roadmap

for further improvements
This commit is contained in:
sebalix
2019-01-24 17:00:01 +01:00
committed by Joan Sisquella
parent 26acb26412
commit f745153635
7 changed files with 79 additions and 97 deletions

View File

@@ -9,12 +9,11 @@ class StockMove(models.Model):
location_move = fields.Boolean(
string="Part of move location",
help="Wether this move is a part of stock_location moves",
help="Whether this move is a part of stock_location moves",
)
@api.depends("location_move")
def _compute_show_details_visible(self):
super()._compute_show_details_visible()
for move in self:
if move.location_move:
move.show_details_visible = True
move.show_details_visible = move.location_move

View File

@@ -0,0 +1,6 @@
Change the current implementation (suggested by Denis Roussel from ACSONE):
* A new parameter on stock picking types : 'Product Change Location' (with a little help).
* With this, go to the dashboard, create a picking with that type.
* Add a button on the picking form which is visible with that type that fill in the picking as now
* Nice to have: add a magic button on locations that with context creates a new picking of that type with the origin location already filled in.

View File

@@ -4,7 +4,7 @@
* Press `ADD ALL` button to add all products available
* Those lines can be edited. Move quantity can't be more than a max available quantity
* Move doesn't care about the reservations and will move stuff anyway
* If during you operation with the wizard the real quantity will change
* If during your operation with the wizard the real quantity will change
it will move only the available quantity at the button press
* Products will be moved and a form view of picking that did that will show up
* If "PLANNED TRANSFER" is used - the picking won't be validated automatically

View File

@@ -8,6 +8,10 @@ from odoo.exceptions import ValidationError
class TestMoveLocation(TestsCommon):
def setUp(self):
super().setUp()
self.setup_product_amounts()
def _create_wizard(self, origin_location, destination_location):
return self.wizard_obj.create({
"origin_location_id": origin_location.id,
@@ -15,9 +19,7 @@ class TestMoveLocation(TestsCommon):
})
def test_move_location_wizard(self):
"""Test a simple move.
"""
self.setup_product_amounts()
"""Test a simple move."""
wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2)
wizard.add_lines()
wizard.action_move_location()
@@ -47,18 +49,14 @@ class TestMoveLocation(TestsCommon):
)
def test_move_location_wizard_amount(self):
"""Can't move more than exists
"""
self.setup_product_amounts()
"""Can't move more than exists."""
wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2)
wizard.add_lines()
with self.assertRaises(ValidationError):
wizard.stock_move_location_line_ids[0].move_quantity += 1
def test_move_location_wizard_ignore_reserved(self):
"""Can't move more than exists
"""
self.setup_product_amounts()
"""Can't move more than exists."""
wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2)
wizard.add_lines()
# reserve some quants
@@ -86,9 +84,7 @@ class TestMoveLocation(TestsCommon):
)
def test_wizard_clear_lines(self):
"""Test lines getting cleared properly
"""
self.setup_product_amounts()
"""Test lines getting cleared properly."""
wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2)
wizard.add_lines()
self.assertEqual(len(wizard.stock_move_location_line_ids), 4)
@@ -96,9 +92,7 @@ class TestMoveLocation(TestsCommon):
self.assertEqual(len(wizard.stock_move_location_line_ids), 0)
def test_planned_transfer(self):
"""Test planned transfer
"""
self.setup_product_amounts()
"""Test planned transfer."""
wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2)
wizard.add_lines()
wizard.with_context({'planned': True}).action_move_location()

View File

@@ -36,19 +36,6 @@ class StockMoveLocationWizard(models.TransientModel):
def _onchange_locations(self):
self._clear_lines()
@api.onchange("stock_move_location_line_ids")
def _onchange_stock_move_location_line_ids(self):
lines_to_update = self.stock_move_location_line_ids.filtered(
lambda x: x.custom is True and
not all([x.origin_location_id, x.destination_location_id])
)
lines_to_update.update({
"origin_location_id": self.origin_location_id,
"destination_location_id": self.destination_location_id,
})
# for an easier extension of this function
return lines_to_update
def _clear_lines(self):
origin = self.origin_location_id
destination = self.destination_location_id
@@ -141,29 +128,27 @@ class StockMoveLocationWizard(models.TransientModel):
})
return action
def _get_group_quants_sql(self):
def _get_group_quants(self):
location_id = self.origin_location_id.id
company = self.env['res.company']._company_default_get(
'stock.inventory',
)
return """
SELECT product_id, lot_id, SUM(quantity)
FROM stock_quant
WHERE location_id = {location_id} AND company_id = {company_id}
GROUP BY product_id, lot_id
""".format(
location_id=location_id,
company_id=company.id,
)
# Using sql as search_group doesn't support aggregation functions
# leading to overhead in queries to DB
query = """
SELECT product_id, lot_id, SUM(quantity)
FROM stock_quant
WHERE location_id = %s
AND company_id = %s
GROUP BY product_id, lot_id
"""
self.env.cr.execute(query, (location_id, company.id))
return self.env.cr.dictfetchall()
def _get_stock_move_location_lines_values(self):
product_obj = self.env['product.product']
# Using sql as search_group doesn't support aggregation functions
# leading to overhead in queries to DB
self.env.cr.execute(self._get_group_quants_sql())
product_data = []
for group in self.env.cr.dictfetchall():
for group in self._get_group_quants():
product = product_obj.browse(group.get("product_id")).exists()
product_data.append({
'product_id': product.id,
@@ -181,11 +166,13 @@ class StockMoveLocationWizard(models.TransientModel):
def add_lines(self):
self.ensure_one()
line_model = self.env["wiz.stock.move.location.line"]
if not self.stock_move_location_line_ids:
for line_val in self._get_stock_move_location_lines_values():
if line_val.get('max_quantity') <= 0:
continue
self.env["wiz.stock.move.location.line"].create(line_val)
line = line_model.create(line_val)
line.onchange_product_id()
return {
"type": "ir.actions.do_nothing",
}

View File

@@ -20,8 +20,8 @@
<tree string="Inventory Details" editable="bottom" decoration-info="move_quantity != max_quantity" decoration-danger="(move_quantity &lt; 0) or (move_quantity > max_quantity)">
<field name="product_id" domain="[('type','=','product')]"/>
<field name="product_uom_id" string="UoM" groups="product.group_uom"/>
<field name="origin_location_id" readonly="1" />
<field name="destination_location_id" readonly="1" />
<field name="origin_location_id" readonly="1" force_save="1"/>
<field name="destination_location_id" readonly="1" force_save="1"/>
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot" options="{'no_create': True}"/>
<field name="move_quantity"/>
<field name="custom" invisible="1" />

View File

@@ -52,22 +52,43 @@ class StockMoveLocationWizardLine(models.TransientModel):
default=True,
)
@api.model
def get_rounding(self):
return self.env.ref("product.decimal_product_uom").digits or 3
@staticmethod
def _compare(qty1, qty2, precision_rounding):
return float_compare(
qty1, qty2,
precision_rounding=precision_rounding)
@api.constrains("max_quantity", "move_quantity")
def _constraint_max_move_quantity(self):
for record in self:
if (float_compare(
record.move_quantity,
record.max_quantity, self.get_rounding()) == 1 or
float_compare(record.move_quantity, 0.0,
self.get_rounding()) == -1):
rounding = record.product_uom_id.rounding
move_qty_gt_max_qty = self._compare(
record.move_quantity, record.max_quantity, rounding) == 1
move_qty_lt_0 = self._compare(
record.move_quantity, 0.0, rounding) == -1
if (move_qty_gt_max_qty or move_qty_lt_0):
raise ValidationError(_(
"Move quantity can not exceed max quantity or be negative"
))
@api.onchange('product_id', 'lot_id')
def onchange_product_id(self):
self.product_uom_id = self.product_id.uom_id
wiz = self.move_location_wizard_id
search_args = [
('location_id', '=', wiz.origin_location_id.id),
('product_id', '=', self.product_id.id),
]
if self.lot_id:
search_args.append(('lot_id', '=', self.lot_id.id))
else:
search_args.append(('lot_id', '=', False))
res = self.env['stock.quant'].read_group(search_args, ['quantity'], [])
max_quantity = res[0]['quantity']
self.max_quantity = max_quantity
self.origin_location_id = wiz.origin_location_id
self.destination_location_id = wiz.destination_location_id
def create_move_lines(self, picking, move):
for line in self:
values = line._get_move_line_values(picking, move)
@@ -103,48 +124,23 @@ class StockMoveLocationWizardLine(models.TransientModel):
if self.env.context.get("planned"):
# for planned transfer we don't care about the amounts at all
return self.move_quantity
# switched to sql here to improve performance and lower db queries
self.env.cr.execute(self._get_specific_quants_sql())
available_qty = self.env.cr.fetchone()
search_args = [
('location_id', '=', self.origin_location_id.id),
('product_id', '=', self.product_id.id),
]
if self.lot_id:
search_args.append(('lot_id', '=', self.lot_id.id))
else:
search_args.append(('lot_id', '=', False))
res = self.env['stock.quant'].read_group(search_args, ['quantity'], [])
available_qty = res[0]['quantity']
if not available_qty:
# if it is immediate transfer and product doesn't exist in that
# location -> make the transfer of 0.
return 0
available_qty = available_qty[0]
if float_compare(
available_qty,
self.move_quantity, self.get_rounding()) == -1:
rounding = self.product_uom_id.rounding
available_qty_lt_move_qty = self._compare(
available_qty, self.move_quantity, rounding) == -1
if available_qty_lt_move_qty:
return available_qty
return self.move_quantity
def _get_specific_quants_sql(self):
self.ensure_one()
lot = "AND lot_id = {}".format(self.lot_id.id)
if not self.lot_id:
lot = "AND lot_id is null"
return """
SELECT sum(quantity)
FROM stock_quant
WHERE location_id = {location}
{lot}
AND product_id = {product}
GROUP BY location_id, product_id, lot_id
""".format(
location=self.origin_location_id.id,
product=self.product_id.id,
lot=lot,
)
@api.model
def create(self, vals):
res = super().create(vals)
# update of wizard lines is extremely buggy
# so i have to handle this additionally in create
if not all([res.origin_location_id, res.destination_location_id]):
or_loc_id = res.move_location_wizard_id.origin_location_id.id
des_loc_id = res.move_location_wizard_id.destination_location_id.id
res.write({
"origin_location_id": or_loc_id,
"destination_location_id": des_loc_id,
})
return res