diff --git a/CHANGELOG b/CHANGELOG
index 88a4d194a9..dfa0c1c68a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Use a single assign wizard for all shipments
* Add access rules to product quantities by warehouse
* Add moves for product quantities by warehouse
* Enforce filling cost price of move
diff --git a/__init__.py b/__init__.py
index 33a0f3cb7c..c117577177 100644
--- a/__init__.py
+++ b/__init__.py
@@ -30,11 +30,9 @@ def register():
shipment.ShipmentInReturn,
shipment.ShipmentOut,
shipment.ShipmentOutReturn,
- shipment.AssignShipmentOutAssignFailed,
shipment.ShipmentInternal,
+ shipment.AssignPartial,
shipment.Address,
- shipment.AssignShipmentInternalAssignFailed,
- shipment.AssignShipmentInReturnAssignFailed,
period.Period,
period.Cache,
product.Template,
@@ -57,9 +55,7 @@ def register():
res.User,
module='stock', type_='model')
Pool.register(
- shipment.AssignShipmentOut,
- shipment.AssignShipmentInternal,
- shipment.AssignShipmentInReturn,
+ shipment.Assign,
product.OpenProductQuantitiesByWarehouse,
product.RecomputeCostPrice,
product.ModifyCostPrice,
diff --git a/shipment.py b/shipment.py
index 19f148e5b5..66b272b91f 100644
--- a/shipment.py
+++ b/shipment.py
@@ -8,7 +8,7 @@ from functools import partial
from sql import Null
from trytond.i18n import gettext
-from trytond.model import Workflow, ModelView, ModelSQL, fields
+from trytond.model import Workflow, ModelView, ModelSQL, fields, dualmethod
from trytond.model.exceptions import AccessError
from trytond.modules.company import CompanyReport
from trytond.wizard import Wizard, StateTransition, StateView, Button
@@ -28,6 +28,43 @@ class ShipmentMixin:
]
+class ShipmentAssignMixin(ShipmentMixin):
+
+ @classmethod
+ def assign_wizard(cls, shipments):
+ raise NotImplementedError
+
+ @property
+ def assign_moves(self):
+ raise NotImplementedError
+
+ @dualmethod
+ @ModelView.button
+ def assign_try(cls, shipments):
+ raise NotImplementedError
+
+ @dualmethod
+ def assign_reset(cls, shipments):
+ cls.wait(shipments)
+
+ @dualmethod
+ @ModelView.button
+ def assign_force(cls, shipments):
+ cls.assign(shipments)
+
+ @dualmethod
+ def assign_ignore(cls, shipments):
+ pool = Pool()
+ Move = pool.get('stock.move')
+ Move.write([
+ m for s in shipments for m in s.assign_moves
+ if m.assignation_required
+ and m.state in {'staging', 'draft'}], {
+ 'quantity': 0,
+ })
+ cls.assign(shipments)
+
+
class ShipmentIn(ShipmentMixin, Workflow, ModelSQL, ModelView):
"Supplier Shipment"
__name__ = 'stock.shipment.in'
@@ -457,7 +494,7 @@ class ShipmentIn(ShipmentMixin, Workflow, ModelSQL, ModelView):
})
-class ShipmentInReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
+class ShipmentInReturn(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
"Supplier Return Shipment"
__name__ = 'stock.shipment.in.return'
_rec_name = 'number'
@@ -736,7 +773,7 @@ class ShipmentInReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
@set_employee('assigned_by')
def assign(cls, shipments):
Move = Pool().get('stock.move')
- Move.assign([m for s in shipments for m in s.moves])
+ Move.assign([m for s in shipments for m in s.assign_moves])
@classmethod
@ModelView.button
@@ -764,7 +801,11 @@ class ShipmentInReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
def assign_wizard(cls, shipments):
pass
- @classmethod
+ @property
+ def assign_moves(self):
+ return self.moves
+
+ @dualmethod
@ModelView.button
def assign_try(cls, shipments, with_childs=None):
pool = Pool()
@@ -772,7 +813,7 @@ class ShipmentInReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
to_assign = defaultdict(list)
for shipment in shipments:
location_type = shipment.from_location.type
- for move in shipment.moves:
+ for move in shipment.assign_moves:
if move.assignation_required:
to_assign[location_type].append(move)
success = True
@@ -789,13 +830,8 @@ class ShipmentInReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
cls.assign(shipments)
return success
- @classmethod
- @ModelView.button
- def assign_force(cls, shipments):
- cls.assign(shipments)
-
-class ShipmentOut(ShipmentMixin, Workflow, ModelSQL, ModelView):
+class ShipmentOut(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
"Customer Shipment"
__name__ = 'stock.shipment.out'
_rec_name = 'number'
@@ -1134,6 +1170,9 @@ class ShipmentOut(ShipmentMixin, Workflow, ModelSQL, ModelView):
@Workflow.transition('assigned')
@set_employee('assigned_by')
def assign(cls, shipments):
+ pool = Pool()
+ Move = pool.get('stock.move')
+ Move.assign([m for s in shipments for m in s.assign_moves])
cls._sync_inventory_to_outgoing(shipments, quantity=False)
@classmethod
@@ -1347,11 +1386,16 @@ class ShipmentOut(ShipmentMixin, Workflow, ModelSQL, ModelView):
def assign_wizard(cls, shipments):
pass
- @classmethod
+ @property
+ def assign_moves(self):
+ return self.inventory_moves
+
+ @dualmethod
@ModelView.button
def assign_try(cls, shipments):
Move = Pool().get('stock.move')
- to_assign = [m for s in shipments for m in s.inventory_moves
+ to_assign = [
+ m for s in shipments for m in s.assign_moves
if m.assignation_required]
if Move.assign_try(to_assign):
cls.assign(shipments)
@@ -1359,13 +1403,6 @@ class ShipmentOut(ShipmentMixin, Workflow, ModelSQL, ModelView):
else:
return False
- @classmethod
- @ModelView.button
- def assign_force(cls, shipments):
- Move = Pool().get('stock.move')
- Move.assign([m for s in shipments for m in s.inventory_moves])
- cls.assign(shipments)
-
class ShipmentOutReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
"Customer Return Shipment"
@@ -1774,56 +1811,7 @@ class ShipmentOutReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
cls.save(shipments)
-class AssignShipmentOutAssignFailed(ModelView):
- 'Assign Customer Shipment'
- __name__ = 'stock.shipment.out.assign.failed'
- inventory_moves = fields.Many2Many('stock.move', None, None,
- 'Inventory Moves', readonly=True,
- help="The inventory moves that were not assigned.")
-
- @staticmethod
- def default_inventory_moves():
- ShipmentOut = Pool().get('stock.shipment.out')
- shipment_id = Transaction().context.get('active_id')
- if not shipment_id:
- return []
- shipment = ShipmentOut(shipment_id)
- return [x.id for x in shipment.inventory_moves if x.state == 'draft']
-
-
-class AssignShipmentOut(Wizard):
- 'Assign Customer Shipment'
- __name__ = 'stock.shipment.out.assign'
- start = StateTransition()
- failed = StateView('stock.shipment.out.assign.failed',
- 'stock.shipment_out_assign_failed_view_form', [
- Button('Force Assign', 'force', 'tryton-forward',
- states={
- 'invisible': ~Id('stock',
- 'group_stock_force_assignment').in_(
- Eval('context', {}).get('groups', [])),
- }),
- Button('OK', 'end', 'tryton-ok', True),
- ])
- force = StateTransition()
-
- def transition_start(self):
- pool = Pool()
- Shipment = pool.get('stock.shipment.out')
-
- if Shipment.assign_try([Shipment(Transaction().context['active_id'])]):
- return 'end'
- else:
- return 'failed'
-
- def transition_force(self):
- Shipment = Pool().get('stock.shipment.out')
-
- Shipment.assign_force([Shipment(Transaction().context['active_id'])])
- return 'end'
-
-
-class ShipmentInternal(ShipmentMixin, Workflow, ModelSQL, ModelView):
+class ShipmentInternal(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
"Internal Shipment"
__name__ = 'stock.shipment.internal'
_rec_name = 'number'
@@ -2353,7 +2341,9 @@ class ShipmentInternal(ShipmentMixin, Workflow, ModelSQL, ModelView):
@Workflow.transition('assigned')
@set_employee('assigned_by')
def assign(cls, shipments):
- pass
+ pool = Pool()
+ Move = pool.get('stock.move')
+ Move.assign([m for s in shipments for m in s.assign_moves])
@classmethod
@ModelView.button
@@ -2394,11 +2384,16 @@ class ShipmentInternal(ShipmentMixin, Workflow, ModelSQL, ModelView):
def assign_wizard(cls, shipments):
pass
- @classmethod
+ @property
+ def assign_moves(self):
+ return self.outgoing_moves
+
+ @dualmethod
@ModelView.button
def assign_try(cls, shipments):
Move = Pool().get('stock.move')
- to_assign = [m for s in shipments for m in s.outgoing_moves
+ to_assign = [
+ m for s in shipments for m in s.assign_moves
if m.assignation_required]
if not to_assign or Move.assign_try(to_assign):
cls.assign(shipments)
@@ -2406,13 +2401,6 @@ class ShipmentInternal(ShipmentMixin, Workflow, ModelSQL, ModelView):
else:
return False
- @classmethod
- @ModelView.button
- def assign_force(cls, shipments):
- Move = Pool().get('stock.move')
- Move.assign([m for s in shipments for m in s.outgoing_moves])
- cls.assign(shipments)
-
@property
def _move_planned_date(self):
'''
@@ -2457,103 +2445,61 @@ class Address(metaclass=PoolMeta):
help="Check to send deliveries to the address.")
-class AssignShipmentInternalAssignFailed(ModelView):
- 'Assign Shipment Internal'
- __name__ = 'stock.shipment.internal.assign.failed'
- moves = fields.Many2Many('stock.move', None, None, 'Moves',
- readonly=True,
- help="The moves that were not assigned.")
-
- @staticmethod
- def default_moves():
- ShipmentInternal = Pool().get('stock.shipment.internal')
- shipment_id = Transaction().context.get('active_id')
- if not shipment_id:
- return []
- shipment = ShipmentInternal(shipment_id)
- return [x.id for x in shipment.outgoing_moves if x.state == 'draft']
-
-
-class AssignShipmentInternal(Wizard):
- 'Assign Shipment Internal'
- __name__ = 'stock.shipment.internal.assign'
+class Assign(Wizard):
+ "Assign Shipment"
+ __name__ = 'stock.shipment.assign'
start = StateTransition()
- failed = StateView('stock.shipment.internal.assign.failed',
- 'stock.shipment_internal_assign_failed_view_form', [
- Button('Force Assign', 'force', 'tryton-forward',
- states={
- 'invisible': ~Id('stock',
- 'group_stock_force_assignment').in_(
- Eval('context', {}).get('groups', [])),
- }),
- Button('OK', 'end', 'tryton-ok', True),
- ])
- force = StateTransition()
-
- def transition_start(self):
- pool = Pool()
- Shipment = pool.get('stock.shipment.internal')
-
- if Shipment.assign_try([Shipment(Transaction().context['active_id'])]):
- return 'end'
- else:
- return 'failed'
-
- def transition_force(self):
- Shipment = Pool().get('stock.shipment.internal')
-
- Shipment.assign_force([Shipment(Transaction().context['active_id'])])
- return 'end'
-
-
-class AssignShipmentInReturnAssignFailed(ModelView):
- 'Assign Supplier Return Shipment'
- __name__ = 'stock.shipment.in.return.assign.failed'
- moves = fields.Many2Many('stock.move', None, None, 'Moves',
- readonly=True,
- help="The moves that were not assigned.")
-
- @staticmethod
- def default_moves():
- ShipmentInternal = Pool().get('stock.shipment.in.return')
- shipment_id = Transaction().context.get('active_id')
- if not shipment_id:
- return []
- shipment = ShipmentInternal(shipment_id)
- return [x.id for x in shipment.moves if x.state == 'draft']
-
-
-class AssignShipmentInReturn(Wizard):
- 'Assign Supplier Return Shipment'
- __name__ = 'stock.shipment.in.return.assign'
- start = StateTransition()
- failed = StateView('stock.shipment.in.return.assign.failed',
- 'stock.shipment_in_return_assign_failed_view_form', [
- Button('Force Assign', 'force', 'tryton-forward',
+ partial = StateView(
+ 'stock.shipment.assign.partial',
+ 'stock.shipment_assign_partial_view_form', [
+ Button("Cancel", 'cancel', 'tryton-cancel'),
+ Button("Wait", 'end', 'tryton-ok', True),
+ Button("Ignore", 'ignore', 'tryton-forward'),
+ Button("Force", 'force', 'tryton-forward',
states={
'invisible': ~Id('stock',
'group_stock_force_assignment').in_(
Eval('context', {}).get('groups', [])),
}),
- Button('OK', 'end', 'tryton-ok', True),
])
+ cancel = StateTransition()
force = StateTransition()
+ ignore = StateTransition()
def transition_start(self):
- pool = Pool()
- Shipment = pool.get('stock.shipment.in.return')
-
- if Shipment.assign_try([Shipment(Transaction().context['active_id'])]):
+ if self.record.assign_try():
return 'end'
else:
- return 'failed'
+ return 'partial'
+
+ def default_partial(self, fields):
+ values = {}
+ if 'moves' in fields:
+ values['moves'] = [
+ m.id for m in self.record.assign_moves
+ if m.state in {'staging', 'draft'}]
+ return values
+
+ def transition_cancel(self):
+ self.record.assign_reset()
+ return 'end'
def transition_force(self):
- Shipment = Pool().get('stock.shipment.in.return')
-
- Shipment.assign_force([Shipment(Transaction().context['active_id'])])
+ self.record.assign_force()
return 'end'
+ def transition_ignore(self):
+ self.record.assign_ignore()
+ return 'end'
+
+
+class AssignPartial(ModelView):
+ "Assign Shipment"
+ __name__ = 'stock.shipment.assign.partial'
+ moves = fields.Many2Many(
+ 'stock.move', None, None, "Moves", readonly=True,
+ help="The moves that were not assigned.")
+
class DeliveryNote(CompanyReport):
'Delivery Note'
diff --git a/shipment.xml b/shipment.xml
index 92275c3388..70fe33a993 100644
--- a/shipment.xml
+++ b/shipment.xml
@@ -54,12 +54,6 @@ this repository contains the full copyright notices and license terms. -->
-
- Assign Purchase Return Shipment
- stock.shipment.in.return.assign
- stock.shipment.in.return
-
-
stock.shipment.in.return
form
@@ -122,18 +116,6 @@ this repository contains the full copyright notices and license terms. -->
action="act_shipment_in_return_form"
id="menu_shipment_in_return_form"/>
-
- stock.shipment.in.return.assign.failed
- form
- shipment_in_return_assign_failed_form
-
-
-
- Assign Customer Shipment
- stock.shipment.out.assign
- stock.shipment.out
-
-
stock.shipment.out
form
@@ -199,18 +181,6 @@ this repository contains the full copyright notices and license terms. -->
-
- stock.shipment.out.assign.failed
- form
- shipment_out_assign_failed_form
-
-
-
- Assign Shipment Internal
- stock.shipment.internal.assign
- stock.shipment.internal
-
-
stock.shipment.internal
form
@@ -288,12 +258,6 @@ this repository contains the full copyright notices and license terms. -->
action="act_shipment_internal_form"
id="menu_shipment_internal_form"/>
-
- stock.shipment.internal.assign.failed
- form
- shipment_internal_assign_failed_form
-
-
stock.shipment.out.return
form
@@ -1069,5 +1033,28 @@ this repository contains the full copyright notices and license terms. -->
pyson="1"/>
+
+
+ stock.shipment.assign.partial
+ form
+ shipment_assign_partial_form
+
+
+
+ Assign Supplier Return Shipment
+ stock.shipment.assign
+ stock.shipment.in.return
+
+
+ Assign Customer Shipment
+ stock.shipment.assign
+ stock.shipment.out
+
+
+ Assign Internal Shipment
+ stock.shipment.assign
+ stock.shipment.internal
+
+
diff --git a/tests/scenario_stock_shipment_out.rst b/tests/scenario_stock_shipment_out.rst
index a8a137ba7d..c3f8b25a83 100644
--- a/tests/scenario_stock_shipment_out.rst
+++ b/tests/scenario_stock_shipment_out.rst
@@ -132,8 +132,10 @@ Make 1 unit of the product available::
Assign the shipment now::
- >>> shipment_out.click('assign_try')
- False
+ >>> shipment_assign = Wizard('stock.shipment.assign', [shipment_out])
+ >>> len(shipment_assign.form.moves)
+ 1
+ >>> shipment_assign.execute('end')
>>> shipment_out.reload()
>>> len(shipment_out.outgoing_moves)
2
@@ -152,14 +154,12 @@ Assign the shipment now::
>>> len(set(planned_dates))
1
-Delete the draft move, assign and pack shipment::
+Ignore non assigned moves and pack shipment::
- >>> for move in shipment_out.inventory_moves:
- ... if move.state == 'draft':
- ... break
- >>> shipment_out.inventory_moves.remove(move)
- >>> shipment_out.click('assign_try')
- True
+ >>> shipment_assign = Wizard('stock.shipment.assign', [shipment_out])
+ >>> shipment_assign.execute('ignore')
+ >>> sorted([m.quantity for m in shipment_out.inventory_moves])
+ [0.0, 1.0]
>>> shipment_out.assigned_by == employee
True
>>> shipment_out.packed_by
@@ -174,7 +174,7 @@ Delete the draft move, assign and pack shipment::
>>> len(shipment_out.outgoing_moves)
2
>>> len(shipment_out.inventory_moves)
- 1
+ 2
>>> shipment_out.inventory_moves[0].state
'done'
>>> sum([m.quantity for m in shipment_out.inventory_moves]) == \
@@ -199,7 +199,7 @@ Set the state as Done::
>>> len(shipment_out.outgoing_moves)
2
>>> len(shipment_out.inventory_moves)
- 1
+ 2
>>> shipment_out.inventory_moves[0].state
'done'
>>> sum([m.quantity for m in shipment_out.inventory_moves]) == \
diff --git a/view/shipment_assign_partial_form.xml b/view/shipment_assign_partial_form.xml
new file mode 100644
index 0000000000..992fc1e0e8
--- /dev/null
+++ b/view/shipment_assign_partial_form.xml
@@ -0,0 +1,8 @@
+
+
+