Add pack wizard on shipments and package

Closes #14586
This commit is contained in:
Cédric Krier
2026-03-19 15:20:46 +01:00
parent fe53481953
commit 3e4e9198a3
11 changed files with 318 additions and 29 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add pack wizard
* Add support for Python 3.14
* Remove support for Python 3.9
+153 -2
View File
@@ -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
+51
View File
@@ -31,22 +31,38 @@ this repository contains the full copyright notices and license terms. -->
<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 @@ this repository contains the full copyright notices and license terms. -->
<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>
@@ -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'
+4
View File
@@ -22,3 +22,7 @@ model:
stock.ShipmentOut
stock.ShipmentInReturn
stock.ShipmentInternal
stock.PackagePackMove
wizard:
stock.ShipmentPack
stock.PackagePack
@@ -15,6 +15,7 @@ this repository contains the full copyright notices and license terms. -->
<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"/>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>