diff --git a/modules/stock_package/CHANGELOG b/modules/stock_package/CHANGELOG
index 310bebb5e2..046c66f1b3 100644
--- a/modules/stock_package/CHANGELOG
+++ b/modules/stock_package/CHANGELOG
@@ -1,3 +1,4 @@
+* Add pack wizard
* Add support for Python 3.14
* Remove support for Python 3.9
diff --git a/modules/stock_package/stock.py b/modules/stock_package/stock.py
index 1d2e062d12..6bed33e208 100644
--- a/modules/stock_package/stock.py
+++ b/modules/stock_package/stock.py
@@ -8,6 +8,8 @@ from trytond.model import (
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.transaction import Transaction
+from trytond.wizard import (
+ Button, StateAction, StateTransition, StateView, Wizard)
from .exceptions import PackageError, PackageValidationError
@@ -200,16 +202,21 @@ class Package(tree(), MeasurementsMixin, ModelSQL, ModelView):
],
states={
'readonly': Eval('state') == 'closed',
- })
+ },
+ help="The package that contains this package.")
children = fields.One2Many(
'stock.package', 'parent', 'Children',
domain=[
('company', '=', Eval('company', -1)),
('shipment', '=', Eval('shipment')),
],
+ add_remove=[
+ ('parent', '=', None),
+ ],
states={
'readonly': Eval('state') == 'closed',
- })
+ },
+ help="The packages contained in this package.")
state = fields.Function(fields.Selection([
('open', "Open"),
('closed', "Closed"),
@@ -229,6 +236,12 @@ class Package(tree(), MeasurementsMixin, ModelSQL, ModelView):
field.states = {
'readonly': Eval('state') == 'closed',
}
+ cls._buttons.update(
+ fill={
+ 'invisible': Eval('state') != 'open',
+ 'icon': 'tryton-launch',
+ 'depends': ['state'],
+ })
@classmethod
def __register__(cls, module):
@@ -286,6 +299,11 @@ class Package(tree(), MeasurementsMixin, ModelSQL, ModelView):
if isinstance(getattr(MeasurementsMixin, name), fields.Field):
setattr(self, name, getattr(self.type, name))
+ @classmethod
+ @ModelView.button_action('stock_package.wizard_package_pack')
+ def fill(cls, packages):
+ pass
+
@classmethod
def validate(cls, packages):
super().validate(packages)
@@ -393,6 +411,13 @@ class PackageMixin(object):
('parent', '=', None),
])
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls._buttons.update(pack_wizard={
+ 'icon': 'tryton-launch',
+ })
+
@classmethod
def check_packages(cls, shipments):
for shipment in shipments:
@@ -408,6 +433,11 @@ class PackageMixin(object):
def packages_moves(self):
raise NotImplementedError
+ @classmethod
+ @ModelView.button_action('stock_package.wizard_shipment_pack')
+ def pack_wizard(cls, shipments):
+ pass
+
@classmethod
def copy(cls, shipments, default=None):
default = default.copy() if default is not None else {}
@@ -428,6 +458,9 @@ class ShipmentOut(PackageMixin, object, metaclass=PoolMeta):
Eval('state') != 'picked')
for field in [cls.packages, cls.root_packages]:
field.states['readonly'] = packages_readonly
+ cls._buttons['pack_wizard']['invisible'] = packages_readonly
+ cls._buttons['pack_wizard']['depends'] = [
+ 'warehouse_storage', 'warehouse_output', 'state']
@classmethod
@ModelView.button
@@ -482,6 +515,8 @@ class ShipmentInReturn(PackageMixin, object, metaclass=PoolMeta):
packages_readonly = ~Eval('state').in_(['waiting', 'assigned'])
for field in [cls.packages, cls.root_packages]:
field.states['readonly'] = packages_readonly
+ cls._buttons['pack_wizard']['invisible'] = packages_readonly
+ cls._buttons['pack_wizard']['depends'] = ['state']
@classmethod
@ModelView.button
@@ -508,6 +543,8 @@ class ShipmentInternal(PackageMixin, object, metaclass=PoolMeta):
for field in [cls.packages, cls.root_packages]:
field.states['readonly'] = packages_readonly
field.states['invisible'] = packages_invisible
+ cls._buttons['pack_wizard']['invisible'] = packages_invisible
+ cls._buttons['pack_wizard']['depends'] = ['state', 'transit_location']
@classmethod
@ModelView.button
@@ -521,3 +558,117 @@ class ShipmentInternal(PackageMixin, object, metaclass=PoolMeta):
return [
m for m in self.outgoing_moves
if m.state != 'cancelled' and m.quantity]
+
+
+class ShipmentPack(Wizard):
+ __name__ = 'stock.shipment.pack'
+ start_state = 'package'
+ package = StateView(
+ 'stock.package',
+ 'stock_package.package_view_form_pack', [
+ Button("End", 'end', 'tryton-cancel'),
+ Button("Add Package", 'add_package', 'tryton-ok'),
+ Button(
+ "Add Package and Fill", 'add_fill_package', 'tryton-add',
+ default=True),
+ ])
+ add_package = StateTransition()
+ add_fill_package = StateAction('stock_package.wizard_package_pack')
+
+ def default_package(self, fields):
+ return {
+ 'company': self.record.company,
+ 'shipment': self.record,
+ }
+
+ def transition_add_package(self):
+ self.package.save()
+ return 'package'
+
+ def do_add_fill_package(self, action):
+ self.package.save()
+ return action, {
+ 'id': self.package.id,
+ 'ids': [self.package.id],
+ 'model': self.package.__name__,
+ }
+
+ def transition_add_fill_package(self):
+ return 'package'
+
+
+class PackagePack(Wizard):
+ __name__ = 'stock.package.pack'
+ start_state = 'next_'
+ next_ = StateTransition()
+ move = StateView(
+ 'stock.package.pack.move',
+ 'stock_package.package_pack_move_view_form', [
+ Button("End", 'end', 'tryton-ok'),
+ Button("Add", 'add_move', 'tryton-forward', default=True),
+ ])
+ add_move = StateTransition()
+
+ def transition_next_(self):
+ if any(not m.package for m in self.record.allowed_moves):
+ return 'move'
+ else:
+ return 'end'
+
+ def default_move(self, fields):
+ return {
+ 'allowed_moves': [
+ m for m in self.record.allowed_moves if not m.package],
+ }
+
+ def transition_add_move(self):
+ pool = Pool()
+ Move = pool.get('stock.move')
+ move = self.move.source
+ if self.move.quantity and self.move.quantity != move.quantity:
+ with Transaction().set_context(_stock_move_split=True):
+ Move.copy([move], {
+ 'quantity': move.quantity - self.move.quantity,
+ })
+ move.quantity = self.move.quantity
+ move.package = self.record
+ move.save()
+ return 'next_'
+
+
+class PackagePackMove(ModelView):
+ __name__ = 'stock.package.pack.move'
+
+ source = fields.Many2One(
+ 'stock.move', "Source", required=True,
+ domain=[
+ ('id', 'in', Eval('allowed_moves', [])),
+ ('package', '=', None),
+ ])
+ quantity = fields.Float(
+ "Quantity", digits='unit',
+ domain=['OR',
+ ('quantity', '=', None),
+ [
+ ('quantity', '>', 0),
+ ('quantity', '<=', Eval('move_quantity', 0)),
+ ],
+ ],
+ help="The quantity to pack from the move.\n"
+ "Leave empty for the full quantity of the move.")
+ unit = fields.Function(
+ fields.Many2One('product.uom', "Unit"),
+ 'on_change_with_unit')
+ move_quantity = fields.Function(
+ fields.Float("Move Quantity"),
+ 'on_change_with_move_quantity')
+ allowed_moves = fields.Many2Many(
+ 'stock.move', None, None, "Allowed Moves", readonly=True)
+
+ @fields.depends('source')
+ def on_change_with_unit(self, name=None):
+ return self.source.unit if self.source else None
+
+ @fields.depends('source')
+ def on_change_with_move_quantity(self, name=None):
+ return self.source.quantity if self.source else None
diff --git a/modules/stock_package/stock.xml b/modules/stock_package/stock.xml
index b243e1c076..4ee7633f5c 100644
--- a/modules/stock_package/stock.xml
+++ b/modules/stock_package/stock.xml
@@ -31,22 +31,38 @@ this repository contains the full copyright notices and license terms. -->
stock.package
form
+
package_form
+
+ stock.package
+ form
+
+ package_form_pack
+
+
stock.package
tree
children
+
package_tree
stock.package
tree
+
package_list
+
+ stock.package
+ fill
+ Fill
+
+
User in companies
stock.package
@@ -137,17 +153,52 @@ this repository contains the full copyright notices and license terms. -->
shipment_out_form
+
+ stock.shipment.out
+ pack_wizard
+ Pack
+
+
stock.shipment.in.return
shipment_in_return_form
+
+ stock.shipment.in.return
+ pack_wizard
+ Fill
+
+
stock.shipment.internal
shipment_internal_form
+
+ stock.shipment.internal
+ pack_wizard
+ Add Package
+
+
+
+ Pack Shipment
+ stock.shipment.pack
+
+
+
+ Pack Package
+ stock.package.pack
+ stock.package
+
+
+
+ stock.package.pack.move
+ form
+ package_pack_move_form
+
+
diff --git a/modules/stock_package/tests/scenario_stock_package.rst b/modules/stock_package/tests/scenario_stock_package.rst
index 39775425c3..8d8aa3e383 100644
--- a/modules/stock_package/tests/scenario_stock_package.rst
+++ b/modules/stock_package/tests/scenario_stock_package.rst
@@ -7,7 +7,7 @@ Imports::
>>> import datetime as dt
>>> from decimal import Decimal
- >>> from proteus import Model
+ >>> from proteus import Model, Wizard
>>> from trytond.modules.company.tests.tools import create_company
>>> from trytond.modules.currency.tests.tools import get_currency
>>> from trytond.tests.tools import activate_modules
@@ -18,6 +18,8 @@ Activate modules::
>>> config = activate_modules('stock_package', create_company)
+ >>> Package = Model.get('stock.package')
+
Get currency::
>>> currency = get_currency()
@@ -66,7 +68,7 @@ Add two shipment lines of same product::
>>> for move in shipment_out.outgoing_moves:
... move.product = product
... move.unit = unit
- ... move.quantity = 1
+ ... move.quantity = 2
... move.from_location = output_loc
... move.to_location = customer_loc
... move.unit_price = Decimal('1')
@@ -91,37 +93,60 @@ Package products::
>>> box.packaging_volume
>>> box.packaging_volume_uom, = ProductUom.find([('name', '=', "Cubic meter")])
>>> box.save()
- >>> package1 = shipment_out.packages.new(type=box)
- >>> package1.length
- 80.0
- >>> package1.height = 50
- >>> package1.packaging_volume
- 0.4
- >>> package_child = package1.children.new(shipment=shipment_out, type=box)
- >>> package_child.height = 100
- >>> moves = package_child.moves.find()
+
+ >>> shipment_pack = Wizard('stock.shipment.pack', [shipment_out])
+
+ >>> shipment_pack.form.type = box
+ >>> shipment_pack.form.height = 100
+ >>> shipment_pack.execute('add_fill_package')
+
+ >>> package_pack, = shipment_pack.actions
+ >>> moves = package_pack.form.allowed_moves
>>> len(moves)
2
- >>> package_child.moves.append(moves[0])
+ >>> package_pack.form.source = moves[0]
+ >>> package_pack.execute('add_move')
+ >>> package_pack.execute('end')
- >>> shipment_out.save()
+ >>> shipment_out.reload()
+ >>> package, = shipment_out.root_packages
+
+ >>> shipment_pack.form.type = box
+ >>> shipment_pack.form.children.append(Package(package.id))
+ >>> shipment_pack.form.length
+ 80.0
+ >>> shipment_pack.form.height = 50
+ >>> shipment_pack.form.packaging_volume
+ 0.4
+
+ >>> shipment_pack.execute('add_package')
Traceback (most recent call last):
...
PackageValidationError: ...
- >>> package1.height = 120
- >>> package1.packaging_volume
+ >>> shipment_pack.form.height = 120
+ >>> shipment_pack.form.packaging_volume
0.96
+ >>> shipment_pack.execute('add_package')
+ >>> shipment_out.reload()
>>> shipment_out.click('pack')
Traceback (most recent call last):
...
PackageError: ...
>>> package2 = shipment_out.packages.new(type=box)
- >>> moves = package2.moves.find()
- >>> len(moves)
- 1
- >>> package2.moves.append(moves[0])
+ >>> shipment_pack.form.type = box
+ >>> shipment_pack.execute('add_fill_package')
+ >>> package_pack, = shipment_pack.actions
+ >>> package_pack.form.source, = package_pack.form.allowed_moves
+ >>> package_pack.form.quantity = 1
+ >>> package_pack.execute('add_move')
+ >>> package_pack.form.source, = package_pack.form.allowed_moves
+ >>> package_pack.execute('add_move')
+ >>> package_pack.execute('end')
+ >>> shipment_pack.execute('end')
>>> shipment_out.click('pack')
+ >>> shipment_out.state
+ 'packed'
diff --git a/modules/stock_package/tryton.cfg b/modules/stock_package/tryton.cfg
index f601e4d717..1ea88eaa39 100644
--- a/modules/stock_package/tryton.cfg
+++ b/modules/stock_package/tryton.cfg
@@ -22,3 +22,7 @@ model:
stock.ShipmentOut
stock.ShipmentInReturn
stock.ShipmentInternal
+ stock.PackagePackMove
+wizard:
+ stock.ShipmentPack
+ stock.PackagePack
diff --git a/modules/stock_package/view/package_form.xml b/modules/stock_package/view/package_form.xml
index d2627bfc27..27109a098c 100644
--- a/modules/stock_package/view/package_form.xml
+++ b/modules/stock_package/view/package_form.xml
@@ -15,6 +15,7 @@ this repository contains the full copyright notices and license terms. -->
+
diff --git a/modules/stock_package/view/package_form_pack.xml b/modules/stock_package/view/package_form_pack.xml
new file mode 100644
index 0000000000..f1016d1f0f
--- /dev/null
+++ b/modules/stock_package/view/package_form_pack.xml
@@ -0,0 +1,39 @@
+
+
+
diff --git a/modules/stock_package/view/package_pack_move_form.xml b/modules/stock_package/view/package_pack_move_form.xml
new file mode 100644
index 0000000000..8f43afcf83
--- /dev/null
+++ b/modules/stock_package/view/package_pack_move_form.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/modules/stock_package/view/shipment_in_return_form.xml b/modules/stock_package/view/shipment_in_return_form.xml
index 20332f6659..3606d9965a 100644
--- a/modules/stock_package/view/shipment_in_return_form.xml
+++ b/modules/stock_package/view/shipment_in_return_form.xml
@@ -2,8 +2,10 @@
-
-
+
+
+
+
+
diff --git a/modules/stock_package/view/shipment_internal_form.xml b/modules/stock_package/view/shipment_internal_form.xml
index e142538003..abfef92a25 100644
--- a/modules/stock_package/view/shipment_internal_form.xml
+++ b/modules/stock_package/view/shipment_internal_form.xml
@@ -2,7 +2,10 @@
-
-
+
+
+
+
+
diff --git a/modules/stock_package/view/shipment_out_form.xml b/modules/stock_package/view/shipment_out_form.xml
index 8b2d43a96c..b75081723a 100644
--- a/modules/stock_package/view/shipment_out_form.xml
+++ b/modules/stock_package/view/shipment_out_form.xml
@@ -2,8 +2,10 @@
-
-
+
+
+
+
+