mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[11.0][MIG] stock_move_location: fixing/improving code + add a roadmap
for further improvements
This commit is contained in:
@@ -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
|
||||
|
||||
6
stock_move_location/readme/ROADMAP.rst
Normal file
6
stock_move_location/readme/ROADMAP.rst
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
<tree string="Inventory Details" editable="bottom" decoration-info="move_quantity != max_quantity" decoration-danger="(move_quantity < 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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user