Generate a default shipping description for the shipment

Also for UPS, it fallbacks to "General Merchandise" if the shipping description
is empty.

Closes #14351
This commit is contained in:
Cédric Krier
2025-11-05 17:18:15 +01:00
parent da78b11fca
commit 4cb10fa620
7 changed files with 90 additions and 12 deletions
+1
View File
@@ -1,3 +1,4 @@
* Generate a default shipping description based on custom categories
Version 7.6.0 - 2025-04-28
--------------------------
+32 -1
View File
@@ -96,6 +96,18 @@ class Package(metaclass=PoolMeta):
pass
def lowest_common_root(paths):
min_length = min((len(p) for p in paths), default=0)
common = None
for i in range(min_length):
level_values = {p[i] for p in paths}
if len(level_values) == 1:
common = level_values.pop()
else:
break
return common
class ShippingMixin:
__slots__ = ()
@@ -107,7 +119,8 @@ class ShippingMixin:
shipping_description = fields.Char('Shipping Description',
states={
'readonly': Eval('state').in_(['done', 'packed'])
})
},
help="Leave empty to use the generated description.")
has_shipping_service = fields.Function(
fields.Boolean("Has Shipping Service"),
'on_change_with_has_shipping_service')
@@ -148,6 +161,24 @@ class ShippingMixin:
[table.shipping_reference],
[table.reference]))
@property
def shipping_description_used(self):
pool = Pool()
Product = pool.get('product.product')
description = self.shipping_description
if not description and hasattr(Product, 'customs_category'):
def parents(category):
if not category:
return
yield from parents(category.parent)
yield category
products = {
m.product for m in self.moves if m.product.customs_category}
paths = [list(parents(p.customs_category)) for p in products]
if category := lowest_common_root(paths):
description = category.name
return description
@fields.depends('carrier')
def on_change_with_has_shipping_service(self, name=None):
return bool(self.carrier and self.carrier.shipping_service)
@@ -1,6 +1,9 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import unittest
from trytond.modules.stock_package_shipping.stock import lowest_common_root
from trytond.tests.test_tryton import ModuleTestCase
@@ -9,4 +12,49 @@ class StockPackageShippingTestCase(ModuleTestCase):
module = 'stock_package_shipping'
class testLowestCommonRoot(unittest.TestCase):
def test_simple_common_root(self):
"Test simple common root"
paths = [
["Apparel", "Men", "Shirts"],
["Apparel", "Men", "Pants"]
]
self.assertEqual(lowest_common_root(paths), "Men")
def test_root_only_common(self):
"Test no common"
paths = [
["Root", "Apparel", "Men", "Shirts"],
["Root", "Electronics", "Mobile"]
]
self.assertEqual(lowest_common_root(paths), "Root")
def test_identical_paths(self):
"Test identical paths"
paths = [
["Apparel", "Men", "Shirts"],
["Apparel", "Men", "Shirts"]
]
self.assertEqual(lowest_common_root(paths), "Shirts")
def test_single_path(self):
"Test single path"
paths = [["Apparel", "Men", "Shirts"]]
self.assertEqual(lowest_common_root(paths), "Shirts")
def test_empty_paths_list(self):
"Test empty paths list"
paths = []
self.assertIsNone(lowest_common_root(paths))
def test_no_common_root(self):
"Test no common root"
paths = [
["Apparel", "Men"],
["Electronics", "Mobile"]
]
self.assertIsNone(lowest_common_root(paths))
del ModuleTestCase
@@ -10,6 +10,8 @@ depends:
stock_shipment_measurements
stock_shipment_cost
product_measurements
extras_depend:
customs
xml:
carrier.xml
stock.xml
@@ -1,3 +1,4 @@
* Use "General Merchandise" as fallback shipping description
Version 7.6.0 - 2025-04-28
--------------------------
@@ -16,8 +16,8 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_phone_required">
<field name="text">To validate shipment "%(shipment)s" you must add phone number for address "%(address)s".</field>
</record>
<record model="ir.message" id="msg_shipping_description_required">
<field name="text">To validate shipment "%(shipment)s" you must fill its shipping description.</field>
<record model="ir.message" id="msg_general_merchandise">
<field name="text">General Merchandise</field>
</record>
<record model="ir.message" id="msg_shipment_has_reference_number">
<field name="text">You cannot create shipping for shipment "%(shipment)s" because it has already a reference number.</field>
+4 -9
View File
@@ -105,14 +105,6 @@ class ShippingUPSMixin:
'.msg_phone_required',
shipment=self.rec_name,
address=address.rec_name))
if not self.shipping_description:
if (any(p.type.ups_code != '01' for p in self.root_packages)
and self.carrier.ups_service_type != '11'):
# TODO Should also test if a country is not in the EU
raise PackingValidationError(
gettext('stock_package_shipping_ups'
'.msg_shipping_description_required',
shipment=self.rec_name))
class CreateShipping(metaclass=PoolMeta):
@@ -375,11 +367,14 @@ class CreateShippingUPS(Wizard):
# despite what UPS documentation says
for pkg in packages:
pkg['ShipmentServiceOptions'] = options
description = (
shipment.shipping_description_used
or gettext('stock_package_shipping_ups.msg_general_merchandise'))
return {
'ShipmentRequest': {
'Request': self.get_request_container(shipment),
'Shipment': {
'Description': (shipment.shipping_description or '')[:50],
'Description': description[:50],
'Shipper': shipper,
'ShipTo': self.get_shipping_party(
shipment.shipping_to, shipment.shipping_to_address),