details:   https://code.tryton.org/tryton/commit/cf2851caba68
branch:    default
user:      Cédric Krier <[email protected]>
date:      Wed Nov 05 17:18:15 2025 +0100
description:
        Generate a default shipping description for the shipment

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

        Closes #14351
diffstat:

 modules/stock_package_shipping/CHANGELOG            |   1 +
 modules/stock_package_shipping/stock.py             |  33 ++++++++++++++-
 modules/stock_package_shipping/tests/test_module.py |  48 +++++++++++++++++++++
 modules/stock_package_shipping/tryton.cfg           |   2 +
 modules/stock_package_shipping_ups/CHANGELOG        |   1 +
 modules/stock_package_shipping_ups/message.xml      |   4 +-
 modules/stock_package_shipping_ups/stock.py         |  13 +---
 7 files changed, 90 insertions(+), 12 deletions(-)

diffs (196 lines):

diff -r cd3931840db9 -r cf2851caba68 modules/stock_package_shipping/CHANGELOG
--- a/modules/stock_package_shipping/CHANGELOG  Thu Nov 20 17:03:46 2025 +0100
+++ b/modules/stock_package_shipping/CHANGELOG  Wed Nov 05 17:18:15 2025 +0100
@@ -1,3 +1,4 @@
+* Generate a default shipping description based on custom categories
 
 Version 7.6.0 - 2025-04-28
 --------------------------
diff -r cd3931840db9 -r cf2851caba68 modules/stock_package_shipping/stock.py
--- a/modules/stock_package_shipping/stock.py   Thu Nov 20 17:03:46 2025 +0100
+++ b/modules/stock_package_shipping/stock.py   Wed Nov 05 17:18:15 2025 +0100
@@ -96,6 +96,18 @@
         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 @@
     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 @@
                     [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)
diff -r cd3931840db9 -r cf2851caba68 
modules/stock_package_shipping/tests/test_module.py
--- a/modules/stock_package_shipping/tests/test_module.py       Thu Nov 20 
17:03:46 2025 +0100
+++ b/modules/stock_package_shipping/tests/test_module.py       Wed Nov 05 
17:18:15 2025 +0100
@@ -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 @@
     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
diff -r cd3931840db9 -r cf2851caba68 modules/stock_package_shipping/tryton.cfg
--- a/modules/stock_package_shipping/tryton.cfg Thu Nov 20 17:03:46 2025 +0100
+++ b/modules/stock_package_shipping/tryton.cfg Wed Nov 05 17:18:15 2025 +0100
@@ -10,6 +10,8 @@
     stock_shipment_measurements
     stock_shipment_cost
     product_measurements
+extras_depend:
+    customs
 xml:
     carrier.xml
     stock.xml
diff -r cd3931840db9 -r cf2851caba68 
modules/stock_package_shipping_ups/CHANGELOG
--- a/modules/stock_package_shipping_ups/CHANGELOG      Thu Nov 20 17:03:46 
2025 +0100
+++ b/modules/stock_package_shipping_ups/CHANGELOG      Wed Nov 05 17:18:15 
2025 +0100
@@ -1,3 +1,4 @@
+* Use "General Merchandise" as fallback shipping description
 
 Version 7.6.0 - 2025-04-28
 --------------------------
diff -r cd3931840db9 -r cf2851caba68 
modules/stock_package_shipping_ups/message.xml
--- a/modules/stock_package_shipping_ups/message.xml    Thu Nov 20 17:03:46 
2025 +0100
+++ b/modules/stock_package_shipping_ups/message.xml    Wed Nov 05 17:18:15 
2025 +0100
@@ -16,8 +16,8 @@
         <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>
diff -r cd3931840db9 -r cf2851caba68 modules/stock_package_shipping_ups/stock.py
--- a/modules/stock_package_shipping_ups/stock.py       Thu Nov 20 17:03:46 
2025 +0100
+++ b/modules/stock_package_shipping_ups/stock.py       Wed Nov 05 17:18:15 
2025 +0100
@@ -105,14 +105,6 @@
                             '.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 @@
             # 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),

Reply via email to