details: https://code.tryton.org/tryton/commit/757346d254e1
branch: default
user: Cédric Krier <[email protected]>
date: Thu Mar 19 15:20:46 2026 +0100
description:
Add pack wizard on shipments and package
Closes #14586
diffstat:
modules/stock_package/CHANGELOG | 1 +
modules/stock_package/stock.py | 155 ++++++++++++++++-
modules/stock_package/stock.xml | 51 +++++
modules/stock_package/tests/scenario_stock_package.rst | 63 ++++--
modules/stock_package/tryton.cfg | 4 +
modules/stock_package/view/package_form.xml | 1 +
modules/stock_package/view/package_form_pack.xml | 39 ++++
modules/stock_package/view/package_pack_move_form.xml | 10 +
modules/stock_package/view/shipment_in_return_form.xml | 8 +-
modules/stock_package/view/shipment_internal_form.xml | 7 +-
modules/stock_package/view/shipment_out_form.xml | 8 +-
11 files changed, 318 insertions(+), 29 deletions(-)
diffs (573 lines):
diff -r c3d9872466b1 -r 757346d254e1 modules/stock_package/CHANGELOG
--- a/modules/stock_package/CHANGELOG Thu Mar 19 10:38:52 2026 +0100
+++ b/modules/stock_package/CHANGELOG Thu Mar 19 15:20:46 2026 +0100
@@ -1,3 +1,4 @@
+* Add pack wizard
* Add support for Python 3.14
* Remove support for Python 3.9
diff -r c3d9872466b1 -r 757346d254e1 modules/stock_package/stock.py
--- a/modules/stock_package/stock.py Thu Mar 19 10:38:52 2026 +0100
+++ b/modules/stock_package/stock.py Thu Mar 19 15:20:46 2026 +0100
@@ -8,6 +8,8 @@
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 @@
],
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 @@
field.states = {
'readonly': Eval('state') == 'closed',
}
+ cls._buttons.update(
+ fill={
+ 'invisible': Eval('state') != 'open',
+ 'icon': 'tryton-launch',
+ 'depends': ['state'],
+ })
@classmethod
def __register__(cls, module):
@@ -287,6 +300,11 @@
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)
for package in packages:
@@ -394,6 +412,13 @@
])
@classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls._buttons.update(pack_wizard={
+ 'icon': 'tryton-launch',
+ })
+
+ @classmethod
def check_packages(cls, shipments):
for shipment in shipments:
if not shipment.packages:
@@ -409,6 +434,11 @@
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 {}
default.setdefault('packages')
@@ -428,6 +458,9 @@
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 @@
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 @@
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 @@
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 -r c3d9872466b1 -r 757346d254e1 modules/stock_package/stock.xml
--- a/modules/stock_package/stock.xml Thu Mar 19 10:38:52 2026 +0100
+++ b/modules/stock_package/stock.xml Thu Mar 19 15:20:46 2026 +0100
@@ -31,22 +31,38 @@
<record model="ir.ui.view" id="package_view_form">
<field name="model">stock.package</field>
<field name="type">form</field>
+ <field name="priority" eval="10"/>
<field name="name">package_form</field>
</record>
+ <record model="ir.ui.view" id="package_view_form_pack">
+ <field name="model">stock.package</field>
+ <field name="type">form</field>
+ <field name="priority" eval="20"/>
+ <field name="name">package_form_pack</field>
+ </record>
+
<record model="ir.ui.view" id="package_view_tree">
<field name="model">stock.package</field>
<field name="type">tree</field>
<field name="field_childs">children</field>
+ <field name="priority" eval="20"/>
<field name="name">package_tree</field>
</record>
<record model="ir.ui.view" id="package_view_list">
<field name="model">stock.package</field>
<field name="type">tree</field>
+ <field name="priority" eval="10"/>
<field name="name">package_list</field>
</record>
+ <record model="ir.model.button" id="package_fill_button">
+ <field name="model">stock.package</field>
+ <field name="name">fill</field>
+ <field name="string">Fill</field>
+ </record>
+
<record model="ir.rule.group" id="rule_group_package_companies">
<field name="name">User in companies</field>
<field name="model">stock.package</field>
@@ -137,17 +153,52 @@
<field name="name">shipment_out_form</field>
</record>
+ <record model="ir.model.button" id="shipment_out_pack_wizard_button">
+ <field name="model">stock.shipment.out</field>
+ <field name="name">pack_wizard</field>
+ <field name="string">Pack</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"/>
<field name="name">shipment_in_return_form</field>
</record>
+ <record model="ir.model.button"
id="shipment_in_return_pack_wizard_button">
+ <field name="model">stock.shipment.in.return</field>
+ <field name="name">pack_wizard</field>
+ <field name="string">Fill</field>
+ </record>
+
<record model="ir.ui.view" id="shipment_internal_view_form">
<field name="model">stock.shipment.internal</field>
<field name="inherit" ref="stock.shipment_internal_view_form"/>
<field name="name">shipment_internal_form</field>
</record>
+ <record model="ir.model.button"
id="shipment_internal_pack_wizard_button">
+ <field name="model">stock.shipment.internal</field>
+ <field name="name">pack_wizard</field>
+ <field name="string">Add Package</field>
+ </record>
+
+ <record model="ir.action.wizard" id="wizard_shipment_pack">
+ <field name="name">Pack Shipment</field>
+ <field name="wiz_name">stock.shipment.pack</field>
+ </record>
+
+ <record model="ir.action.wizard" id="wizard_package_pack">
+ <field name="name">Pack Package</field>
+ <field name="wiz_name">stock.package.pack</field>
+ <field name="model">stock.package</field>
+ </record>
+
+ <record model="ir.ui.view" id="package_pack_move_view_form">
+ <field name="model">stock.package.pack.move</field>
+ <field name="type">form</field>
+ <field name="name">package_pack_move_form</field>
+ </record>
+
</data>
</tryton>
diff -r c3d9872466b1 -r 757346d254e1
modules/stock_package/tests/scenario_stock_package.rst
--- a/modules/stock_package/tests/scenario_stock_package.rst Thu Mar 19
10:38:52 2026 +0100
+++ b/modules/stock_package/tests/scenario_stock_package.rst Thu Mar 19
15:20:46 2026 +0100
@@ -7,7 +7,7 @@
>>> 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 @@
>>> config = activate_modules('stock_package', create_company)
+ >>> Package = Model.get('stock.package')
+
Get currency::
>>> currency = get_currency()
@@ -66,7 +68,7 @@
>>> 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 @@
>>> 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.reload()
+ >>> package, = shipment_out.root_packages
- >>> shipment_out.save()
+ >>> 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 -r c3d9872466b1 -r 757346d254e1 modules/stock_package/tryton.cfg
--- a/modules/stock_package/tryton.cfg Thu Mar 19 10:38:52 2026 +0100
+++ b/modules/stock_package/tryton.cfg Thu Mar 19 15:20:46 2026 +0100
@@ -22,3 +22,7 @@
stock.ShipmentOut
stock.ShipmentInReturn
stock.ShipmentInternal
+ stock.PackagePackMove
+wizard:
+ stock.ShipmentPack
+ stock.PackagePack
diff -r c3d9872466b1 -r 757346d254e1 modules/stock_package/view/package_form.xml
--- a/modules/stock_package/view/package_form.xml Thu Mar 19 10:38:52
2026 +0100
+++ b/modules/stock_package/view/package_form.xml Thu Mar 19 15:20:46
2026 +0100
@@ -15,6 +15,7 @@
<notebook colspan="4">
<page name="moves">
<field name="moves" colspan="4" widget="many2many"/>
+ <button name="fill" colspan="4"/>
</page>
<page name="children">
<field name="children" colspan="4"/>
diff -r c3d9872466b1 -r 757346d254e1
modules/stock_package/view/package_form_pack.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock_package/view/package_form_pack.xml Thu Mar 19 15:20:46
2026 +0100
@@ -0,0 +1,39 @@
+<?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="type"/>
+ <field name="type" widget="selection"/>
+ <label name="number"/>
+ <field name="number"/>
+ <label name="parent" string="Is contained in:"/>
+ <field name="parent"/>
+ <notebook colspan="4">
+ <page name="children" string="Containing">
+ <field name="children" colspan="4" widget="many2many"
string="Containing"/>
+ </page>
+ <page string="Measurements" col="3" id="measurements">
+ <label name="length"/>
+ <field name="length"/>
+ <field name="length_uom" widget="selection"/>
+
+ <label name="height"/>
+ <field name="height"/>
+ <field name="height_uom" widget="selection"/>
+
+ <label name="width"/>
+ <field name="width"/>
+ <field name="width_uom" widget="selection"/>
+
+ <label name="packaging_volume"/>
+ <field name="packaging_volume"/>
+ <field name="packaging_volume_uom" widget="selection"/>
+
+ <label name="packaging_weight"/>
+ <field name="packaging_weight"/>
+ <field name="packaging_weight_uom"/>
+ </page>
+ </notebook>
+ <field name="company" invisible="1" colspan="4"/>
+ <field name="shipment" invisible="1" colspan="4"/>
+</form>
diff -r c3d9872466b1 -r 757346d254e1
modules/stock_package/view/package_pack_move_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock_package/view/package_pack_move_form.xml Thu Mar 19
15:20:46 2026 +0100
@@ -0,0 +1,10 @@
+<?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">
+ <label name="source"/>
+ <field name="source"/>
+
+ <label name="quantity"/>
+ <field name="quantity"/>
+</form>
diff -r c3d9872466b1 -r 757346d254e1
modules/stock_package/view/shipment_in_return_form.xml
--- a/modules/stock_package/view/shipment_in_return_form.xml Thu Mar 19
10:38:52 2026 +0100
+++ b/modules/stock_package/view/shipment_in_return_form.xml Thu Mar 19
15:20:46 2026 +0100
@@ -2,8 +2,10 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
- <xpath expr="//field[@name='moves']" position="after">
- <field name="root_packages" colspan="4"
- view_ids="stock_package.package_view_tree"/>
+ <xpath expr="//page[@name='moves']" position="after">
+ <page name="root_packages">
+ <field name="root_packages" colspan="4"
view_ids="stock_package.package_view_tree"/>
+ <button name="pack_wizard" colspan="4"/>
+ </page>
</xpath>
</data>
diff -r c3d9872466b1 -r 757346d254e1
modules/stock_package/view/shipment_internal_form.xml
--- a/modules/stock_package/view/shipment_internal_form.xml Thu Mar 19
10:38:52 2026 +0100
+++ b/modules/stock_package/view/shipment_internal_form.xml Thu Mar 19
15:20:46 2026 +0100
@@ -2,7 +2,10 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
- <xpath expr="//page[@name='outgoing_moves']" position="inside">
- <field name="root_packages" colspan="4"
view_ids="stock_package.package_view_tree"/>
+ <xpath expr="//page[@name='outgoing_moves']" position="after">
+ <page name="root_packages">
+ <field name="root_packages" colspan="4"
view_ids="stock_package.package_view_tree"/>
+ <button name="pack_wizard" colspan="4"/>
+ </page>
</xpath>
</data>
diff -r c3d9872466b1 -r 757346d254e1
modules/stock_package/view/shipment_out_form.xml
--- a/modules/stock_package/view/shipment_out_form.xml Thu Mar 19 10:38:52
2026 +0100
+++ b/modules/stock_package/view/shipment_out_form.xml Thu Mar 19 15:20:46
2026 +0100
@@ -2,8 +2,10 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
- <xpath expr="/form/notebook/page[@id='outgoing_moves']" position="inside">
- <field name="root_packages" colspan="4"
- view_ids="stock_package.package_view_tree"/>
+ <xpath expr="//page[@id='outgoing_moves']" position="after">
+ <page name="root_packages">
+ <field name="root_packages" colspan="4"
view_ids="stock_package.package_view_tree"/>
+ <button name="pack_wizard" colspan="4"/>
+ </page>
</xpath>
</data>