details:   https://code.tryton.org/tryton/commit/4719e38cdad6
branch:    default
user:      Cédric Krier <[email protected]>
date:      Sat Feb 15 22:23:11 2025 +0100
description:
        Support DPD international shipping
diffstat:

 modules/stock_package_shipping_dpd/CHANGELOG                                   
                |    1 +
 modules/stock_package_shipping_dpd/setup.py                                    
                |    1 +
 modules/stock_package_shipping_dpd/stock.py                                    
                |  123 +++++-
 
modules/stock_package_shipping_dpd/tests/scenario_stock_package_shipping_dpd_international.rst
 |  200 ++++++++++
 modules/stock_package_shipping_dpd/tests/test_scenario.py                      
                |    5 +-
 modules/stock_package_shipping_dpd/tryton.cfg                                  
                |    8 +
 6 files changed, 331 insertions(+), 7 deletions(-)

diffs (422 lines):

diff -r 28626f2e2d49 -r 4719e38cdad6 
modules/stock_package_shipping_dpd/CHANGELOG
--- a/modules/stock_package_shipping_dpd/CHANGELOG      Sat Feb 15 19:27:34 
2025 +0100
+++ b/modules/stock_package_shipping_dpd/CHANGELOG      Sat Feb 15 22:23:11 
2025 +0100
@@ -1,3 +1,4 @@
+* Support international shipping
 * Update to Shipment Service 4.4
 * Support international shipping
 
diff -r 28626f2e2d49 -r 4719e38cdad6 modules/stock_package_shipping_dpd/setup.py
--- a/modules/stock_package_shipping_dpd/setup.py       Sat Feb 15 19:27:34 
2025 +0100
+++ b/modules/stock_package_shipping_dpd/setup.py       Sat Feb 15 22:23:11 
2025 +0100
@@ -49,6 +49,7 @@
 
 tests_require = [
     get_require_version('proteus'),
+    get_require_version('trytond_incoterm'),
     get_require_version('trytond_sale'),
     get_require_version('trytond_sale_shipment_cost'),
     ]
diff -r 28626f2e2d49 -r 4719e38cdad6 modules/stock_package_shipping_dpd/stock.py
--- a/modules/stock_package_shipping_dpd/stock.py       Sat Feb 15 19:27:34 
2025 +0100
+++ b/modules/stock_package_shipping_dpd/stock.py       Sat Feb 15 22:23:11 
2025 +0100
@@ -2,6 +2,7 @@
 # this repository contains the full copyright notices and license terms.
 
 import locale
+from decimal import Decimal
 from io import BytesIO
 from itertools import zip_longest
 from math import ceil
@@ -235,7 +236,7 @@
 
         return shipping_party
 
-    def get_parcel(self, package):
+    def get_parcel(self, shipment, package):
         pool = Pool()
         UoM = pool.get('product.uom')
         ModelData = pool.get('ir.model.data')
@@ -269,22 +270,38 @@
         return parcel
 
     def get_shipment_data(self, credential, shipment, packages):
+        pool = Pool()
+        UoM = pool.get('product.uom')
+        ModelData = pool.get('ir.model.data')
+
+        cm3 = UoM(ModelData.get_id('product', 'uom_cubic_centimeter'))
+        kg = UoM(ModelData.get_id('product', 'uom_kilogram'))
+
+        volume = round(UoM.compute_qty(
+                shipment.volume_uom, shipment.volume, cm3, round=False))
+        weight = round(UoM.compute_qty(
+                shipment.weight_uom, shipment.weight, kg, round=False), 2)
         return {
             'generalShipmentData': {
                 'identificationNumber': shipment.number,
                 'sendingDepot': credential.depot,
                 'product': shipment.carrier.dpd_product,
+                'mpsVolume': int(volume),
+                'mpsWeight': int(weight * 100),
                 'sender': self.shipping_party(
                     shipment.company.party,
                     shipment.shipping_warehouse.address),
                 'recipient': self.shipping_party(
                     shipment.shipping_to, shipment.shipping_to_address),
                 },
-            'parcels': [self.get_parcel(p) for p in packages],
-            'productAndServiceData': {
-                'orderType': 'consignment',
-                **self.get_notification(shipment),
-                },
+            'parcels': [self.get_parcel(shipment, p) for p in packages],
+            'productAndServiceData': self.get_product_and_service(shipment),
+            }
+
+    def get_product_and_service(self, shipment):
+        return {
+            'orderType': 'consignment',
+            **self.get_notification(shipment),
             }
 
     def get_notification(self, shipment, usage=None):
@@ -324,3 +341,97 @@
                     },
                 }
         return {}
+
+
+class CreateDPDShipping_Customs(metaclass=PoolMeta):
+    __name__ = 'stock.shipment.create_shipping.dpd'
+
+    def get_product_and_service(self, shipment):
+        pool = Pool()
+        Date = pool.get('ir.date')
+        Currency = pool.get('currency.currency')
+
+        with Transaction().set_context(company=shipment.company.id):
+            today = Date.today()
+
+        product_and_service = super().get_product_and_service(shipment)
+
+        if shipment.customs_international:
+            invoice_date = shipment.effective_date or today
+            currency, = {m.currency for m in shipment.customs_moves}
+
+            amount = sum(
+                Currency.compute(
+                    curr, Decimal(str(v['quantity'])) * price, currency,
+                    round=False)
+                for (product, price, curr, _), v in
+                shipment.customs_products.items())
+
+            international = {
+                'parcelType': 0,
+                'customsAmount': int(
+                    amount.quantize(Decimal('.01')) * Decimal(100)),
+                'customsCurrency': currency.code,
+                'customsTerms': self.get_customs_terms(shipment),
+                'customsPaper': 'A',
+                'customsInvoice': shipment.number[:20],
+                'customsInvoiceDate': invoice_date.strftime('%Y%m%d'),
+                }
+            if customs_agent := shipment.customs_agent:
+                international.update({
+                        'commercialInvoiceConsigneeVatNumber': (
+                            customs_agent.tax_identifier.code)[:20],
+                        'commercialInvoiceConsignee': self.shipping_party(
+                            customs_agent.party,
+                            customs_agent.address),
+                        })
+            if shipment.tax_identifier:
+                international['commercialInvoiceConsignorVatNumber'] = (
+                    shipment.tax_identifier.code[:17])
+            international['commercialInvoiceConsignor'] = self.shipping_party(
+                shipment.company.party, shipment.customs_from_address)
+            international['additionalInvoiceLines'] = [
+                {'customsInvoicePosition': i,
+                    **self.get_international_invoice_line(shipment, *k, **v)}
+                for i, (k, v) in enumerate(
+                    shipment.customs_products.items(), 1)]
+            international['numberOfArticle'] = len(
+                international['additionalInvoiceLines'])
+            product_and_service['international'] = international
+        return product_and_service
+
+    def get_customs_terms(self, shipment):
+        if shipment and shipment.incoterm:
+            if shipment.incoterm.code == 'DAP':
+                return '01'
+            elif shipment.incoterm.code == 'DDP':
+                return '03'
+            elif shipment.incoterm.code == 'EXW':
+                return '05'
+
+    def get_international_invoice_line(
+            self, shipment, product, price, currency, unit, quantity, weight):
+        tariff_code = product.get_tariff_code({
+                'date': shipment.effective_date or shipment.planned_date,
+                'country': (
+                    shipment.customs_to_country.id
+                    if shipment.customs_to_country else None),
+                })
+
+        weight = round(weight, 2)
+
+        value = price * Decimal(str(quantity))
+        if not quantity.is_integer():
+            quantity = 1
+
+        return {
+            'quantityItems': int(quantity),
+            'customsContent': product.name[:200],
+            'customsTarif': tariff_code.code if tariff_code else None,
+            'customsAmountLine': int(
+                value.quantize(Decimal('.01')) * Decimal(100)),
+            'customsOrigin': (
+                product.country_of_origin.code_numeric
+                if product.country_of_origin else None),
+            'customsGrossWeight': int(weight * 100),
+            }
diff -r 28626f2e2d49 -r 4719e38cdad6 
modules/stock_package_shipping_dpd/tests/scenario_stock_package_shipping_dpd_international.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ 
b/modules/stock_package_shipping_dpd/tests/scenario_stock_package_shipping_dpd_international.rst
    Sat Feb 15 22:23:11 2025 +0100
@@ -0,0 +1,200 @@
+======================================================
+Stock Package Shipping with DPD International Scenario
+======================================================
+
+Imports::
+
+    >>> import os
+    >>> from decimal import Decimal
+
+    >>> from proteus import Model
+    >>> from trytond.modules.company.tests.tools import create_company, 
get_company
+    >>> from trytond.tests.tools import activate_modules, assertEqual
+
+Activate modules::
+
+    >>> config = activate_modules(
+    ...     ['stock_package_shipping_dpd', 'stock_shipment_customs', 
'incoterm'],
+    ...     create_company)
+
+    >>> Address = Model.get('party.address')
+    >>> Agent = Model.get('customs.agent')
+    >>> AgentSelection = Model.get('customs.agent.selection')
+    >>> Carrier = Model.get('carrier')
+    >>> Country = Model.get('country.country')
+    >>> DPDCredential = Model.get('carrier.credential.dpd')
+    >>> Incoterm = Model.get('incoterm.incoterm')
+    >>> Location = Model.get('stock.location')
+    >>> Package = Model.get('stock.package')
+    >>> PackageType = Model.get('stock.package.type')
+    >>> Party = Model.get('party.party')
+    >>> ProductTemplate = Model.get('product.template')
+    >>> Shipment = Model.get('stock.shipment.out')
+    >>> TariffCode = Model.get('customs.tariff.code')
+    >>> UoM = Model.get('product.uom')
+
+Get company::
+
+    >>> company = get_company()
+
+Create countries::
+
+    >>> belgium = Country(code='BE', name="Belgium")
+    >>> belgium.save()
+    >>> switzerland = Country(code='CH', name="Switerland")
+    >>> switzerland.save()
+    >>> taiwan = Country(code='TW', code_numeric='158', name="Taiwan")
+    >>> taiwan.save()
+
+Create parties::
+
+    >>> customer = Party(name="Customer")
+    >>> customer_address, = customer.addresses
+    >>> customer_address.street = "Pfistergasse 17"
+    >>> customer_address.postal_code = "6003"
+    >>> customer_address.city = "Lucerna"
+    >>> customer_address.country = switzerland
+    >>> customer_phone = customer.contact_mechanisms.new()
+    >>> customer_phone.type = 'phone'
+    >>> customer_phone.value = "+(41) (041) 410-62-66"
+    >>> customer_email = customer.contact_mechanisms.new()
+    >>> customer_email.type = 'email'
+    >>> customer_email.value = '[email protected]'
+    >>> customer.save()
+
+    >>> agent_party = Party(name="Agent")
+    >>> agent_address, = agent_party.addresses
+    >>> agent_address.street = "Gerechtigkeitsgasse 53"
+    >>> agent_address.postal_code = "3011"
+    >>> agent_address.city = "Berna"
+    >>> agent_address.country = switzerland
+    >>> agent_identifier = agent_party.identifiers.new()
+    >>> agent_identifier.type = 'ch_vat'
+    >>> agent_identifier.code = "CHE-123.456.788 IVA"
+    >>> agent_email = agent_party.contact_mechanisms.new()
+    >>> agent_email.type = 'email'
+    >>> agent_email.value = '[email protected]'
+    >>> agent_phone = agent_party.contact_mechanisms.new()
+    >>> agent_phone.type = 'phone'
+    >>> agent_phone.value = '+(41) (041) 745-55-28'
+    >>> agent_party.save()
+    >>> agent = Agent(party=agent_party)
+    >>> agent.save()
+    >>> AgentSelection(to_country=switzerland, agent=agent).save()
+
+Set the warehouse address::
+
+    >>> warehouse, = Location.find([('type', '=', 'warehouse')])
+    >>> company_address = Address()
+    >>> company_address.party = company.party
+    >>> company_address.street = '2 rue de la Centrale'
+    >>> company_address.postal_code = '4000'
+    >>> company_address.city = 'Sclessin'
+    >>> company_address.country = belgium
+    >>> company_address.save()
+    >>> company_phone = company.party.contact_mechanisms.new()
+    >>> company_phone.type = 'phone'
+    >>> company_phone.value = '+32 4 2522122'
+    >>> company_phone.save()
+    >>> warehouse.address = company_address
+    >>> warehouse.save()
+
+Get some units::
+
+    >>> unit, = UoM.find([('name', '=', "Unit")], limit=1)
+    >>> cm, = UoM.find([('name', '=', "Centimeter")], limit=1)
+    >>> gram, = UoM.find([('name', '=', "Gram")], limit=1)
+
+Create tariff::
+
+    >>> tariff_code = TariffCode(code='17039099')
+    >>> tariff_code.save()
+
+Create product::
+
+    >>> template = ProductTemplate()
+    >>> template.name = 'product'
+    >>> template.default_uom = unit
+    >>> template.type = 'goods'
+    >>> template.weight = 100
+    >>> template.weight_uom = gram
+    >>> template.list_price = Decimal('10.0000')
+    >>> template.country_of_origin = taiwan
+    >>> _ = template.tariff_codes.new(tariff_code=tariff_code)
+    >>> template.save()
+    >>> product, = template.products
+
+Create Package Type::
+
+    >>> box = PackageType(
+    ...     name='Box',
+    ...     length=10, length_uom=cm,
+    ...     height=8, height_uom=cm,
+    ...     width=5, width_uom=cm)
+    >>> box.save()
+
+Create a DPD Carrier and the related credential::
+
+    >>> credential = DPDCredential()
+    >>> credential.company = company
+    >>> credential.user_id = os.getenv('DPD_USER_ID')
+    >>> credential.password = os.getenv('DPD_PASSWORD')
+    >>> credential.server = 'testing'
+    >>> credential.save()
+
+    >>> carrier_product_template = ProductTemplate()
+    >>> carrier_product_template.name = 'DPD Delivery'
+    >>> carrier_product_template.default_uom = unit
+    >>> carrier_product_template.type = 'service'
+    >>> carrier_product_template.list_price = Decimal(20)
+    >>> carrier_product_template.save()
+    >>> carrier_product, = carrier_product_template.products
+
+    >>> dpd = Party(name='DPD')
+    >>> dpd.save()
+
+    >>> carrier = Carrier()
+    >>> carrier.party = dpd
+    >>> carrier.carrier_product = carrier_product
+    >>> carrier.shipping_service = 'dpd'
+    >>> carrier.dpd_product = 'CL'
+    >>> carrier.dpd_output_format = 'PDF'
+    >>> carrier.dpd_paper_format = 'A6'
+    >>> carrier.dpd_notification = 'sms'
+    >>> carrier.save()
+
+Create a shipment::
+
+    >>> shipment = Shipment()
+    >>> shipment.customer = customer
+    >>> shipment.carrier = carrier
+    >>> shipment.incoterm, = Incoterm.find([('code', '=', 'DAP')], limit=1)
+    >>> move = shipment.outgoing_moves.new()
+    >>> move.product = product
+    >>> move.unit = unit
+    >>> move.quantity = 2
+    >>> move.from_location = shipment.warehouse_output
+    >>> move.to_location = shipment.customer_location
+    >>> move.unit_price = Decimal('50.0000')
+    >>> move.currency = company.currency
+    >>> shipment.click('wait')
+    >>> assertEqual(shipment.customs_agent, agent)
+    >>> shipment.click('assign_force')
+    >>> shipment.click('pick')
+    >>> shipment.state
+    'picked'
+
+Create the packs and ship the shipment::
+
+    >>> pack = shipment.packages.new()
+    >>> pack.type = box
+    >>> pack_move, = pack.moves.find([])
+    >>> pack.moves.append(pack_move)
+    >>> shipment.click('pack')
+    >>> shipment.state
+    'packed'
+
+    >>> create_shipping = shipment.click('create_shipping')
+    >>> shipment.reload()
+    >>> bool(shipment.shipping_reference)
+    True
diff -r 28626f2e2d49 -r 4719e38cdad6 
modules/stock_package_shipping_dpd/tests/test_scenario.py
--- a/modules/stock_package_shipping_dpd/tests/test_scenario.py Sat Feb 15 
19:27:34 2025 +0100
+++ b/modules/stock_package_shipping_dpd/tests/test_scenario.py Sat Feb 15 
22:23:11 2025 +0100
@@ -9,5 +9,8 @@
 def load_tests(*args, **kwargs):
     if (not TEST_NETWORK
             or not (os.getenv('DPD_USER_ID') and os.getenv('DPD_PASSWORD'))):
-        kwargs.setdefault('skips', set()).add('scenario_shipping_dpd.rst')
+        kwargs.setdefault('skips', set()).update([
+                'scenario_shipping_dpd.rst',
+                'scenario_stock_package_shipping_dpd_international.rst',
+                ])
     return load_doc_tests(__name__, __file__, *args, **kwargs)
diff -r 28626f2e2d49 -r 4719e38cdad6 
modules/stock_package_shipping_dpd/tryton.cfg
--- a/modules/stock_package_shipping_dpd/tryton.cfg     Sat Feb 15 19:27:34 
2025 +0100
+++ b/modules/stock_package_shipping_dpd/tryton.cfg     Sat Feb 15 22:23:11 
2025 +0100
@@ -9,6 +9,10 @@
     stock_shipment_measurements
     stock_package
     stock_package_shipping
+extras_depend:
+    customs
+    incoterm
+    stock_shipment_customs
 xml:
     carrier.xml
     stock.xml
@@ -23,5 +27,9 @@
     stock.CreateShipping
     stock.CreateDPDShipping
 
+[register incoterm stock_shipment_customs]
+wizard:
+    stock.CreateDPDShipping_Customs
+
 [register_mixin]
 stock.ShippingDPDMixin: 
trytond.modules.stock_package_shipping.stock.ShippingMixin

Reply via email to