@@ -1,3 +1,4 @@
|
||||
* Allow unsplitting moves
|
||||
|
||||
Version 7.6.0 - 2025-04-28
|
||||
--------------------------
|
||||
|
||||
@@ -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::
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user