Support DPD international shipping
This commit is contained in:
1 parent
39a79c70ef
commit
8a772ad678
6 files changed
+331
-7
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Support international shipping
|
||||
* Update to Shipment Service 4.4
|
||||
* Support international shipping
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ requires.append(get_require_version('trytond'))
|
||||
|
||||
tests_require = [
|
||||
get_require_version('proteus'),
|
||||
get_require_version('trytond_incoterm'),
|
||||
get_require_version('trytond_sale'),
|
||||
get_require_version('trytond_sale_shipment_cost'),
|
||||
]
|
||||
|
||||
@@ -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 @@ class CreateDPDShipping(Wizard):
|
||||
|
||||
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 @@ class CreateDPDShipping(Wizard):
|
||||
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 @@ class CreateDPDShipping(Wizard):
|
||||
},
|
||||
}
|
||||
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),
|
||||
}
|
||||
+200
@@ -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 = 'customer@example.com'
|
||||
>>> 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 = 'agent@example.com'
|
||||
>>> 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
|
||||
@@ -9,5 +9,8 @@ from trytond.tests.test_tryton import TEST_NETWORK, load_doc_tests
|
||||
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)
|
||||
@@ -9,6 +9,10 @@ depends:
|
||||
stock_shipment_measurements
|
||||
stock_package
|
||||
stock_package_shipping
|
||||
extras_depend:
|
||||
customs
|
||||
incoterm
|
||||
stock_shipment_customs
|
||||
xml:
|
||||
carrier.xml
|
||||
stock.xml
|
||||
@@ -23,5 +27,9 @@ wizard:
|
||||
stock.CreateShipping
|
||||
stock.CreateDPDShipping
|
||||
|
||||
[register incoterm stock_shipment_customs]
|
||||
wizard:
|
||||
stock.CreateDPDShipping_Customs
|
||||
|
||||
[register_mixin]
|
||||
stock.ShippingDPDMixin: trytond.modules.stock_package_shipping.stock.ShippingMixin
|
||||
Reference in new issue
Block a user