Store internal measurements for final states of shipments

This prevents the searcher to compute the sum for all the shipments with a
join to all the stock moves.

Closes #13438
This commit is contained in:
Cédric Krier committed 2024-08-06 08:24:46 +02:00
1 parent 05b5a79564
commit ea4782565a
3 files changed
+237 -17

No files matched your search

@@ -1,3 +1,4 @@
* Store internal measurements for final states of shipments
Version 7.2.0 - 2024-04-29
--------------------------
+214 -14
View File
@@ -1,12 +1,15 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from collections import defaultdict
from functools import wraps
from itertools import groupby
from sql import Literal, Null
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from sql.operators import Concat
from trytond.model import ModelSQL, fields
from trytond.model import Index, ModelSQL, ModelView, Workflow, fields
from trytond.modules.company.model import CompanyValueMixin
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
@@ -180,6 +183,7 @@ class MeasurementsMixin(object):
},
help="The total weight of the record's moves."),
'get_measurements', searcher='search_measurements')
internal_weight = fields.Float("Internal Weight", readonly=True)
weight_uom = fields.Function(
fields.Many2One(
'product.uom', "Weight UoM",
@@ -193,12 +197,55 @@ class MeasurementsMixin(object):
},
help="The total volume of the record's moves."),
'get_measurements', searcher='search_measurements')
internal_volume = fields.Float("Internal Volume", readonly=True)
volume_uom = fields.Function(
fields.Many2One(
'product.uom', "Volume UoM",
help="The Unit of Measure of volume."),
'get_measurements_uom')
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
states = cls._measurements_states()
if states:
cls._sql_indexes.update({
Index(
t,
(t.id, Index.Range()),
where=(~t.state.in_(states)
| (t.internal_weight == Null)
| (t.internal_volume == Null))),
Index(t,
(t.internal_weight, Index.Range()),
where=(t.state.in_(states)
& (t.internal_weight != Null)
& (t.internal_volume != Null))),
Index(t,
(t.internal_volume, Index.Range()),
where=(t.state.in_(states)
& (t.internal_weight != Null)
& (t.internal_volume != Null))),
})
@classmethod
def __register__(cls, module):
table_h = cls.__table_handler__(module)
set_measurements = (
not table_h.column_exist('internal_weight')
or not table_h.column_exist('internal_volume'))
super().__register__(module)
if set_measurements:
cls.set_measurements()
@classmethod
def _measurements_states(cls):
return []
@classmethod
def get_measurements_uom(cls, shipments, name):
pool = Pool()
@@ -212,6 +259,50 @@ class MeasurementsMixin(object):
uoms[shipment.id] = uom.id
return uoms
@classmethod
def set_measurements(cls, shipments=None):
pool = Pool()
Location = pool.get('stock.location')
Move = pool.get('stock.move')
cursor = Transaction().connection.cursor()
table = cls.__table__()
move = Move.__table__()
location = Location.__table__()
if not (states := cls._measurements_states()):
return
query = (table.update(
[table.internal_weight, table.internal_volume],
[move
.join(location,
condition=cls._measurements_location_condition(
table, move, location))
.select(
Sum(Coalesce(move.internal_weight, 0)),
where=cls._measurements_move_condition(table, move)),
move
.join(location,
condition=cls._measurements_location_condition(
table, move, location))
.select(
Sum(Coalesce(move.internal_volume, 0)),
where=cls._measurements_move_condition(table, move))],
where=table.state.in_(states)))
if shipments:
where = query.where
for sub_shipments in grouped_slice(shipments):
query.where = (
where & reduce_ids(table.id, map(int, sub_shipments)))
cursor.execute(*query)
else:
cursor.execute(*query)
if shipments:
cls.write(shipments, {})
@classmethod
def get_measurements(cls, shipments, names):
pool = Pool()
@@ -238,23 +329,35 @@ class MeasurementsMixin(object):
table, move, location)
).select(
table.id,
Sum(move.internal_weight),
Sum(move.internal_volume),
Sum(Coalesce(move.internal_weight, 0)),
Sum(Coalesce(move.internal_volume, 0)),
group_by=[table.id])
id2shipment = {s.id: s for s in shipments}
for sub_shipments in grouped_slice(shipments):
weights, volumes = {}, {}
states = cls._measurements_states()
for sub_shipments in grouped_slice(
(s for s in shipments
if s.state not in states
or s.internal_weight is None
or s.internal_volume is None)):
query.where = reduce_ids(
table.id, [s.id for s in sub_shipments])
cursor.execute(*query)
for id_, weight, volume in cursor:
shipment = id2shipment[id_]
if 'weight' in names:
measurements['weight'][id_] = Uom.compute_qty(
kg, weight, shipment.weight_uom)
if 'volume' in names:
measurements['volume'][id_] = Uom.compute_qty(
liter, volume, shipment.volume_uom)
weights[id_] = weight
volumes[id_] = volume
for shipment in shipments:
if 'weight' in names:
weight = weights.get(
shipment.id, shipment.internal_weight)
measurements['weight'][shipment.id] = Uom.compute_qty(
kg, weight, shipment.weight_uom)
if 'volume' in names:
volume = volumes.get(
shipment.id, shipment.internal_volume)
measurements['volume'][shipment.id] = Uom.compute_qty(
liter, volume, shipment.volume_uom)
return measurements
@classmethod
@@ -276,7 +379,8 @@ class MeasurementsMixin(object):
Operator = fields.SQL_OPERATORS[operator]
if name == 'weight':
measurement = Sum(move.internal_weight)
measurement = Sum(Coalesce(move.internal_weight, 0))
column = table.internal_weight
if value is not None:
kg = Uom(ModelData.get_id('product', 'uom_kilogram'))
if isinstance(value, (int, float)):
@@ -287,7 +391,8 @@ class MeasurementsMixin(object):
if v is not None else v
for v in value]
else:
measurement = Sum(move.internal_volume)
measurement = Sum(Coalesce(move.internal_volume, 0))
column = table.internal_volume
if value is not None:
liter = Uom(ModelData.get_id('product', 'uom_liter'))
if isinstance(value, (int, float)):
@@ -298,6 +403,11 @@ class MeasurementsMixin(object):
if v is not None else v
for v in value]
if states := cls._measurements_states():
state_clause = table.state.in_(states)
else:
state_clause = Literal(False)
query = table.join(
move, type_='LEFT',
condition=cls._measurements_move_condition(table, move)
@@ -307,8 +417,17 @@ class MeasurementsMixin(object):
table, move, location)
).select(
table.id,
where=~state_clause
| (table.internal_weight == Null)
| (table.internal_volume == Null),
group_by=[table.id],
having=Operator(measurement, value))
query |= table.select(
table.id,
where=state_clause
& (table.internal_weight != Null)
& (table.internal_volume != Null)
& Operator(column, value))
return [('id', 'in', query)]
@classmethod
@@ -320,27 +439,76 @@ class MeasurementsMixin(object):
raise NotImplementedError
def set_measurements(func):
@wraps(func)
def wrapper(cls, shipments, *args, **kwargs):
result = func(cls, shipments, *args, **kwargs)
cls.set_measurements(shipments)
return result
return wrapper
class ShipmentIn(MeasurementsMixin, object, metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
@classmethod
def _measurements_states(cls):
return super()._measurements_states() + ['done', 'cancelled']
@classmethod
def _measurements_location_condition(cls, shipment, move, location):
return (
(move.from_location == location.id)
& (location.type == 'supplier'))
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('done')
def do(cls, shipments):
super().do(shipments)
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('cancelled')
def cancel(cls, shipments):
super().cancel(shipments)
class ShipmentInReturn(MeasurementsMixin, object, metaclass=PoolMeta):
__name__ = 'stock.shipment.in.return'
@classmethod
def _measurements_states(cls):
return super()._measurements_states() + ['done', 'cancelled']
@classmethod
def _measurements_location_condition(cls, shipment, move, location):
return move.from_location == location.id
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('done')
def do(cls, shipments):
super().do(shipments)
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('cancelled')
def cancel(cls, shipments):
super().cancel(shipments)
class ShipmentOut(MeasurementsMixin, object, metaclass=PoolMeta):
__name__ = 'stock.shipment.out'
@classmethod
def _measurements_states(cls):
return super()._measurements_states() + ['done', 'cancelled']
@classmethod
def _measurements_location_condition(cls, shipment, move, location):
return (
@@ -364,16 +532,48 @@ class ShipmentOut(MeasurementsMixin, object, metaclass=PoolMeta):
round=False)
return weight
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('done')
def do(cls, shipments):
super().do(shipments)
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('cancelled')
def cancel(cls, shipments):
super().cancel(shipments)
class ShipmentOutReturn(MeasurementsMixin, object, metaclass=PoolMeta):
__name__ = 'stock.shipment.out.return'
@classmethod
def _measurements_states(cls):
return super()._measurements_states() + ['done', 'cancelled']
@classmethod
def _measurements_location_condition(cls, shipment, move, location):
return (
(move.from_location == location.id)
& (location.type == 'customer'))
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('done')
def do(cls, shipments):
super().do(shipments)
@classmethod
@ModelView.button
@set_measurements
@Workflow.transition('cancelled')
def cancel(cls, shipments):
super().cancel(shipments)
# TODO ShipmentInternal
@@ -130,7 +130,6 @@ class StockShipmentMeasurementsTestCase(CompanyTestMixin, ModuleTestCase):
kg, = Uom.search([('name', '=', 'Kilogram')])
liter, = Uom.search([('name', '=', 'Liter')])
customer, = Location.search([('code', '=', 'CUS')])
storage, = Location.search([('code', '=', 'STO')])
party = Party(name='Customer')
party.addresses = [{}]
party.save()
@@ -166,7 +165,7 @@ class StockShipmentMeasurementsTestCase(CompanyTestMixin, ModuleTestCase):
product=product,
unit=kg,
quantity=10,
from_location=storage,
from_location=shipment.warehouse.output_location,
to_location=customer,
company=company,
unit_price=Decimal(1),
@@ -179,6 +178,7 @@ class StockShipmentMeasurementsTestCase(CompanyTestMixin, ModuleTestCase):
self.assertEqual(shipment.volume, 2)
Shipment.wait([shipment])
Shipment.pick([shipment])
# with inventory moves
self.assertEqual(shipment.weight, 10)
@@ -207,7 +207,7 @@ class StockShipmentMeasurementsTestCase(CompanyTestMixin, ModuleTestCase):
package = Package()
package.type = package_type
package.shipment = shipment
package.moves = shipment.moves
package.moves = shipment.outgoing_moves
package.parent = package_root
package.save()
@@ -223,5 +223,24 @@ class StockShipmentMeasurementsTestCase(CompanyTestMixin, ModuleTestCase):
self.assertEqual(package.total_weight, 10)
self.assertEqual(package.total_volume, 2)
Shipment.pack([shipment])
Shipment.do([shipment])
self.assertEqual(shipment.internal_weight, 10)
self.assertEqual(shipment.internal_volume, 2)
self.assertEqual(shipment.weight, 10)
self.assertEqual(shipment.volume, 2)
for clause, result in [
([('weight', '=', 10)], [shipment]),
([('weight', '=', 5)], []),
([('weight', 'in', [10, 5])], [shipment]),
([('volume', '=', 2)], [shipment]),
([('volume', '=', 3)], []),
([('volume', 'in', [2, 3])], [shipment]),
]:
msg = 'clause: %s' % clause
self.assertEqual(Shipment.search(clause), result, msg=msg)
del ModuleTestCase