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:
@@ -1,3 +1,4 @@
|
||||
* Generate a default shipping description based on custom categories
|
||||
|
||||
Version 7.6.0 - 2025-04-28
|
||||
--------------------------
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user