Allow unsplitting moves

Closes #14079
This commit is contained in:
Cédric Krier
2025-08-09 15:39:54 +02:00
parent 9da4595239
commit b7f6a462f1
6 changed files with 147 additions and 1 deletions
+1
View File
@@ -1,3 +1,4 @@
* Allow unsplitting moves
Version 7.6.0 - 2025-04-28
--------------------------
+2 -1
View File
@@ -11,7 +11,8 @@ Move
When the *Stock Split Module* is activated, the `Move <stock:model-stock.move>`
gain a :guilabel:`Split` button to launch the `Split Move
<wizard-stock.move.split>` wizard.
<wizard-stock.move.split>` wizard and another :guilabel:`Unsplit` button to
reverse the operation.
.. seealso::
+49
View File
@@ -18,6 +18,10 @@ class Move(metaclass=PoolMeta):
'readonly': ~Eval('state').in_(['draft', 'assigned']),
'depends': ['state'],
},
'unsplit': {
'readonly': ~Eval('state').in_(['draft', 'assigned']),
'depends': ['state'],
},
})
@classmethod
@@ -73,6 +77,51 @@ class Move(metaclass=PoolMeta):
})
return moves
@classmethod
@ModelView.button
def unsplit(cls, moves, _exclude=None):
pool = Pool()
UoM = pool.get('product.uom')
_exclude = _exclude.copy() if _exclude is not None else set()
_exclude.update({
'id', 'create_uid', 'create_date', 'write_uid', 'write_date',
'quantity', 'unit', 'internal_quantity'})
fields_names = {
name for name, field in cls._fields.items()
if not isinstance(field, (fields.Function, fields.MultiValue))}
fields_names -= _exclude
values = cls.read([m.id for m in moves], fields_names=fields_names)
id2values = {v.pop('id'): v for v in values}
groups = []
for move in moves:
for group in groups:
if id2values[move.id] == id2values[group[0].id]:
group.append(move)
break
else:
groups.append([move])
def unit_precision(unit):
return unit.factor * unit.rounding
to_save, to_clear = [], []
for group in groups:
if len(group) <= 1:
continue
quantity = sum(m.internal_quantity for m in group)
unit = min((m.unit for m in group), key=unit_precision)
move, *others = group
move.quantity = UoM.compute_qty(
move.product.default_uom, quantity, unit)
move.unit = unit
to_save.append(move)
to_clear.extend(others)
cls.write(to_clear, {'quantity': 0})
cls.save(to_save)
class SplitMoveStart(ModelView):
__name__ = 'stock.move.split.start'
+6
View File
@@ -49,6 +49,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="string">Split</field>
</record>
<record model="ir.model.button" id="move_unsplit_button">
<field name="model">stock.move</field>
<field name="name">unsplit</field>
<field name="string">Unsplit</field>
</record>
<record model="ir.ui.view" id="shipment_in_return_view_form">
<field name="model">stock.shipment.in.return</field>
<field name="inherit" ref="stock.shipment_in_return_view_form"/>
+88
View File
@@ -86,5 +86,93 @@ class StockSplitTestCase(CompanyTestMixin, ModuleTestCase):
self.assertEqual([m.state for m in moves],
['assigned', 'assigned'])
@with_transaction()
def test_unsplit(self):
"Test unsplit"
pool = Pool()
Uom = pool.get('product.uom')
Template = pool.get('product.template')
Product = pool.get('product.product')
Location = pool.get('stock.location')
Move = pool.get('stock.move')
meter, = Uom.search([('name', '=', "Meter")])
cm, = Uom.search([('name', '=', "Centimeter")])
template, = Template.create([{
'name': 'Test Split',
'type': 'goods',
'default_uom': meter.id,
}])
product, = Product.create([{
'template': template.id,
}])
input_, = Location.search([('code', '=', 'IN')])
storage, = Location.search([('code', '=', 'STO')])
company = create_company()
with set_company(company):
moves = Move.create([{
'product': product,
'unit': meter,
'quantity': 2,
'from_location': input_.id,
'to_location': storage.id,
'company': company.id,
}, {
'product': product,
'unit': cm,
'quantity': 25,
'from_location': input_.id,
'to_location': storage.id,
'company': company.id,
}])
Move.unsplit(moves)
self.assertEqual(moves[0].quantity, 225)
self.assertEqual(moves[0].unit, cm)
self.assertEqual(moves[1].quantity, 0)
@with_transaction()
def test_unsplit_different(self):
"Test unsplit different"
pool = Pool()
Uom = pool.get('product.uom')
Template = pool.get('product.template')
Product = pool.get('product.product')
Location = pool.get('stock.location')
Move = pool.get('stock.move')
unit, = Uom.search([('name', '=', "Unit")])
template, = Template.create([{
'name': 'Test Split',
'type': 'goods',
'default_uom': unit.id,
}])
product, = Product.create([{
'template': template.id,
}])
input_, = Location.search([('code', '=', 'IN')])
storage, = Location.search([('code', '=', 'STO')])
company = create_company()
with set_company(company):
moves = Move.create([{
'product': product,
'unit': unit,
'quantity': 2,
'from_location': input_.id,
'to_location': storage.id,
'company': company.id,
}, {
'product': product,
'unit': unit,
'quantity': 2,
'from_location': storage.id,
'to_location': input_.id,
'company': company.id,
}])
Move.unsplit(moves)
self.assertEqual(moves[0].quantity, 2)
self.assertEqual(moves[1].quantity, 2)
del ModuleTestCase
+1
View File
@@ -4,5 +4,6 @@ this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/tree" position="inside">
<button name="split_wizard" string="Split"/>
<button name="unsplit" multiple="1"/>
</xpath>
</data>