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>

Reply via email to