changeset ed3cb6d538b7 in modules/stock_assign_manual:default
details:
https://hg.tryton.org/modules/stock_assign_manual?cmd=changeset&node=ed3cb6d538b7
description:
Add wizard to unassign quantity from inventory moves on shipment
issue9769
review306801011
diffstat:
CHANGELOG | 4 +-
__init__.py | 3 +
doc/index.rst | 3 +
stock.py | 162 +++++++++++++++++++++++++++-
stock.xml | 37 ++++++
tests/scenario_stock_assign_manual.rst | 47 ++++++++
view/shipment_assigned_move_form.xml | 11 +
view/shipment_assigned_move_list.xml | 8 +
view/shipment_unassign_manual_show_form.xml | 8 +
9 files changed, 278 insertions(+), 5 deletions(-)
diffs (373 lines):
diff -r ac8f323710ac -r ed3cb6d538b7 CHANGELOG
--- a/CHANGELOG Tue Dec 22 18:11:05 2020 +0100
+++ b/CHANGELOG Mon Apr 12 19:43:05 2021 +0200
@@ -1,2 +1,4 @@
+* Add manual unassign wizard on shipment
+
Version 5.8.0 - 2020-11-02
-* Initial release
\ No newline at end of file
+* Initial release
diff -r ac8f323710ac -r ed3cb6d538b7 __init__.py
--- a/__init__.py Tue Dec 22 18:11:05 2020 +0100
+++ b/__init__.py Mon Apr 12 19:43:05 2021 +0200
@@ -14,10 +14,13 @@
stock.ShipmentInReturn,
stock.ShipmentOut,
stock.ShipmentInternal,
+ stock.ShipmentAssignedMove,
stock.ShipmentAssignManualShow,
+ stock.ShipmentUnassignManualShow,
module='stock_assign_manual', type_='model')
Pool.register(
stock.ShipmentAssignManual,
+ stock.ShipmentUnassignManual,
module='stock_assign_manual', type_='wizard')
Pool.register(
production.Production,
diff -r ac8f323710ac -r ed3cb6d538b7 doc/index.rst
--- a/doc/index.rst Tue Dec 22 18:11:05 2020 +0100
+++ b/doc/index.rst Mon Apr 12 19:43:05 2021 +0200
@@ -3,3 +3,6 @@
The Stock Assign Manual module adds a wizard on shipments and production that
allows you to decide from which precise location to pick products.
+
+Another wizard allows either the whole amount, or a specific quantity, to be
+unassigned of each move.
diff -r ac8f323710ac -r ed3cb6d538b7 stock.py
--- a/stock.py Tue Dec 22 18:11:05 2020 +0100
+++ b/stock.py Mon Apr 12 19:43:05 2021 +0200
@@ -4,7 +4,8 @@
from trytond.exceptions import UserError
from trytond.i18n import gettext
-from trytond.model import Model, ModelStorage, ModelView, fields
+from trytond.model import (
+ Model, ModelStorage, ModelView, ModelSQL, fields, dualmethod)
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
@@ -12,7 +13,41 @@
from trytond.wizard import Wizard, StateTransition, StateView, Button
-class ShipmentInReturn(metaclass=PoolMeta):
+class ShipmentUnassignMixin:
+ '''Mixin to unassign quantity from assigned shipment moves'''
+
+ @dualmethod
+ def unassign(cls, shipments, moves, quantities):
+ '''
+ Unassign the quantity from the corresponding move of the shipments.
+ '''
+ pool = Pool()
+ Move = pool.get('stock.move')
+ to_unassign = []
+ if not all(m.state == 'assigned' for m in moves):
+ raise ValueError("Not assigned move")
+ Move.draft(moves)
+ for move, unassign_quantity in zip(moves, quantities):
+ if not unassign_quantity:
+ continue
+ if unassign_quantity > move.quantity:
+ raise ValueError(
+ "Unassigned quantity greater than move quantity")
+ if unassign_quantity == move.quantity:
+ to_unassign.append(move)
+ else:
+ with Transaction().set_context(_stock_move_split=True):
+ to_unassign.extend(Move.copy(
+ [move],
+ {'quantity': unassign_quantity}))
+ move.quantity -= unassign_quantity
+ Move.save(moves)
+ Move.assign(moves)
+ if to_unassign:
+ cls.wait(shipments, to_unassign)
+
+
+class ShipmentInReturn(ShipmentUnassignMixin, metaclass=PoolMeta):
__name__ = 'stock.shipment.in.return'
@classmethod
@@ -32,7 +67,7 @@
pass
-class ShipmentOut(metaclass=PoolMeta):
+class ShipmentOut(ShipmentUnassignMixin, metaclass=PoolMeta):
__name__ = 'stock.shipment.out'
@classmethod
@@ -55,7 +90,7 @@
pass
-class ShipmentInternal(metaclass=PoolMeta):
+class ShipmentInternal(ShipmentUnassignMixin, metaclass=PoolMeta):
__name__ = 'stock.shipment.internal'
@classmethod
@@ -277,3 +312,122 @@
for field, value in zip(grouping, key[1:]):
if value is not None and '.' not in field:
setattr(self.move, field, value)
+
+
+class ShipmentUnassignManual(Wizard):
+ "Manual Unassign Shipment"
+ __name__ = 'stock.shipment.unassign.manual'
+ start = StateTransition()
+ show = StateView('stock.shipment.unassign.manual.show',
+ 'stock_assign_manual.shipment_unassign_manual_show_view_form', [
+ Button("Cancel", 'end', 'tryton-cancel'),
+ Button("Unassign", 'unassign', 'tryton-ok', default=True),
+ ])
+ unassign = StateTransition()
+
+ def transition_start(self):
+ moves = self.record.assign_moves
+ if any(m.state == 'assigned' for m in moves):
+ return 'show'
+ return 'end'
+
+ def default_show(self, fields):
+ moves = self.record.assign_moves
+ move_ids = [m.id for m in moves if m.state == 'assigned']
+ return {
+ 'assigned_moves': move_ids,
+ }
+
+ def transition_unassign(self):
+ moves = []
+ quantities = []
+ for m in self.show.moves:
+ moves.append(m.move)
+ quantities.append(m.unassigned_quantity)
+ self.record.unassign(moves, quantities)
+ return 'end'
+
+
+class ShipmentAssignedMove(ModelView, ModelSQL):
+ "Shipment Assigned Move"
+ __name__ = 'stock.shipment.assigned.move'
+
+ move = fields.Many2One('stock.move', "Move", required=True)
+ unassigned_quantity = fields.Float(
+ "Unassigned Quantity", digits=(16, Eval('unit_digits', 2)),
+ domain=['OR',
+ ('unassigned_quantity', '=', None),
+ [
+ ('unassigned_quantity', '>=', 0),
+ ('unassigned_quantity', '<=', Eval('move_quantity', 0)),
+ ],
+ ],
+ depends=['unit_digits', 'move_quantity'],
+ help="The quantity to unassign")
+ assigned_quantity = fields.Float(
+ "Assigned Quantity", digits=(16, Eval('unit_digits', 2)),
+ domain=['OR',
+ ('assigned_quantity', '=', None),
+ [
+ ('assigned_quantity', '>=', 0),
+ ('assigned_quantity', '<=', Eval('move_quantity', 0)),
+ ],
+ ],
+ depends=['unit_digits', 'move_quantity'],
+ help="The quantity left assigned")
+ unit = fields.Function(
+ fields.Many2One('product.uom', "Unit"), 'on_change_with_unit')
+ unit_digits = fields.Function(
+ fields.Integer("Unit Digits"), 'on_change_with_unit_digits')
+ move_quantity = fields.Function(
+ fields.Float("Move Quantity"), 'on_change_with_move_quantity')
+
+ @staticmethod
+ def default_unassigned_quantity():
+ return 0.0
+
+ @fields.depends('move', 'unassigned_quantity', 'assigned_quantity')
+ def on_change_move(self, name=None):
+ if self.move:
+ self.assigned_quantity = self.move.quantity
+ self.unassigned_quantity = 0.0
+
+ @fields.depends('assigned_quantity', 'move', 'unassigned_quantity', 'unit')
+ def on_change_unassigned_quantity(self, name=None):
+ if self.move and self.unassigned_quantity:
+ self.assigned_quantity = self.unit.round(
+ self.move.quantity - self.unassigned_quantity)
+
+ @fields.depends('unassigned_quantity', 'move', 'assigned_quantity', 'unit')
+ def on_change_assigned_quantity(self, name=None):
+ if self.move and self.assigned_quantity:
+ self.unassigned_quantity = self.unit.round(
+ self.move.quantity - self.assigned_quantity)
+
+ @fields.depends('move')
+ def on_change_with_unit(self, name=None):
+ if self.move:
+ return self.move.uom.id
+
+ @fields.depends('move')
+ def on_change_with_unit_digits(self, name=None):
+ if self.move:
+ return self.move.uom.digits
+
+ @fields.depends('move')
+ def on_change_with_move_quantity(self, name=None):
+ if self.move:
+ return self.move.quantity
+
+
+class ShipmentUnassignManualShow(ModelView):
+ "Manually Unassign Shipment"
+ __name__ = 'stock.shipment.unassign.manual.show'
+
+ moves = fields.One2Many(
+ 'stock.shipment.assigned.move', None, "Moves",
+ domain=[('move.id', 'in', Eval('assigned_moves'))],
+ depends=['assigned_moves'],
+ help="The moves to unassign.")
+ assigned_moves = fields.Many2Many(
+ 'stock.move', None, None, "Assigned Moves")
diff -r ac8f323710ac -r ed3cb6d538b7 stock.xml
--- a/stock.xml Tue Dec 22 18:11:05 2020 +0100
+++ b/stock.xml Mon Apr 12 19:43:05 2021 +0200
@@ -72,5 +72,42 @@
<field name="type">form</field>
<field name="name">shipment_assign_manual_show_form</field>
</record>
+
+ <record model="ir.action.wizard" id="wizard_shipment_unassign_manual">
+ <field name="name">Manually Unassign Shipment</field>
+ <field name="wiz_name">stock.shipment.unassign.manual</field>
+ </record>
+ <record model="ir.action.keyword"
id="shipment_in_return_unassign_manual_keyword">
+ <field name="keyword">form_action</field>
+ <field name="model">stock.shipment.in.return,-1</field>
+ <field name="action" ref="wizard_shipment_unassign_manual"/>
+ </record>
+ <record model="ir.action.keyword"
id="shipment_out_unassign_manual_keyword">
+ <field name="keyword">form_action</field>
+ <field name="model">stock.shipment.out,-1</field>
+ <field name="action" ref="wizard_shipment_unassign_manual"/>
+ </record>
+ <record model="ir.action.keyword"
id="shipment_internal_unassign_manual_keyword">
+ <field name="keyword">form_action</field>
+ <field name="model">stock.shipment.internal,-1</field>
+ <field name="action" ref="wizard_shipment_unassign_manual"/>
+ </record>
+
+ <record model="ir.ui.view"
id="shipment_unassign_manual_show_view_form">
+ <field name="model">stock.shipment.unassign.manual.show</field>
+ <field name="type">form</field>
+ <field name="name">shipment_unassign_manual_show_form</field>
+ </record>
+
+ <record model="ir.ui.view" id="shipment_assigned_move_view_form">
+ <field name="model">stock.shipment.assigned.move</field>
+ <field name="type">form</field>
+ <field name="name">shipment_assigned_move_form</field>
+ </record>
+ <record model="ir.ui.view" id="shipment_assigned_move_view_list">
+ <field name="model">stock.shipment.assigned.move</field>
+ <field name="type">tree</field>
+ <field name="name">shipment_assigned_move_list</field>
+ </record>
</data>
</tryton>
diff -r ac8f323710ac -r ed3cb6d538b7 tests/scenario_stock_assign_manual.rst
--- a/tests/scenario_stock_assign_manual.rst Tue Dec 22 18:11:05 2020 +0100
+++ b/tests/scenario_stock_assign_manual.rst Mon Apr 12 19:43:05 2021 +0200
@@ -137,3 +137,50 @@
>>> shipment.state
'assigned'
+
+Unassign move::
+
+ >>> AssignedMove = Model.get('stock.shipment.assigned.move')
+ >>> sorted([m.state for m in shipment.inventory_moves])
+ ['assigned', 'assigned', 'assigned']
+ >>> move1, _, _ = shipment.inventory_moves
+ >>> unassign_manual = Wizard('stock.shipment.unassign.manual', [shipment])
+ >>> move_to_unassign = AssignedMove()
+ >>> move_to_unassign.move = StockMove(move1.id)
+ >>> move_to_unassign.unassigned_quantity = 1.0
+ >>> unassign_manual.form.moves.append(move_to_unassign)
+ >>> unassign_manual.execute('unassign')
+ >>> shipment.state
+ 'waiting'
+ >>> sorted([(m.state, m.quantity) for m in shipment.inventory_moves])
+ [('assigned', 1.0), ('assigned', 3.0), ('draft', 1.0)]
+
+Unassign a second move to be merged::
+
+ >>> move2, = [i for i in shipment.inventory_moves if (
+ ... i.quantity == 1.0 and i.state == 'assigned')]
+ >>> unassign_manual = Wizard('stock.shipment.unassign.manual', [shipment])
+ >>> move_to_unassign = AssignedMove()
+ >>> move_to_unassign.move = StockMove(move2.id)
+ >>> move_to_unassign.unassigned_quantity = 1.0
+ >>> unassign_manual.form.moves.append(move_to_unassign)
+ >>> unassign_manual.execute('unassign')
+ >>> shipment.state
+ 'waiting'
+ >>> sorted([(m.state, m.quantity) for m in shipment.inventory_moves])
+ [('assigned', 3.0), ('draft', 2.0)]
+
+Unassign partially third move::
+
+ >>> move3, = [i for i in shipment.inventory_moves
+ ... if i.quantity == 3.0 and i.state == 'assigned']
+ >>> unassign_manual = Wizard('stock.shipment.unassign.manual', [shipment])
+ >>> move_to_unassign = AssignedMove()
+ >>> move_to_unassign.move = StockMove(move3.id)
+ >>> move_to_unassign.unassigned_quantity = 2.0
+ >>> unassign_manual.form.moves.append(move_to_unassign)
+ >>> unassign_manual.execute('unassign')
+ >>> shipment.state
+ 'waiting'
+ >>> sorted([(m.state, m.quantity) for m in shipment.inventory_moves])
+ [('assigned', 1.0), ('draft', 2.0), ('draft', 2.0)]
diff -r ac8f323710ac -r ed3cb6d538b7 view/shipment_assigned_move_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/shipment_assigned_move_form.xml Mon Apr 12 19:43:05 2021 +0200
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+ <label name="move"/>
+ <field name="move" colspan="3"/>
+ <label name="unassigned_quantity"/>
+ <field name="unassigned_quantity" symbol="unit"/>
+ <label name="assigned_quantity"/>
+ <field name="assigned_quantity" symbol="unit"/>
+</form>
diff -r ac8f323710ac -r ed3cb6d538b7 view/shipment_assigned_move_list.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/shipment_assigned_move_list.xml Mon Apr 12 19:43:05 2021 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree editable="1">
+ <field name="move" expand="1"/>
+ <field name="unassigned_quantity" symbol="unit"/>
+ <field name="assigned_quantity" symbol="unit"/>
+</tree>
diff -r ac8f323710ac -r ed3cb6d538b7 view/shipment_unassign_manual_show_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/shipment_unassign_manual_show_form.xml Mon Apr 12 19:43:05
2021 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form col="2">
+ <image name="tryton-question" xexpand="0" xfill="0"/>
+ <label string="Enter the quantities to unassign:" id="fill" yalign="0.0"
xalign="0.0" xexpand="1"/>
+ <field name="moves" product="move" colspan="2"/>
+</form>