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