first commit
This commit is contained in:
336
modules/stock_package_shipping_sendcloud/stock.py
Normal file
336
modules/stock_package_shipping_sendcloud/stock.py
Normal file
@@ -0,0 +1,336 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from decimal import Decimal
|
||||
from itertools import zip_longest
|
||||
from math import ceil
|
||||
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import fields
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.modules.stock_package_shipping.stock import address_name
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.wizard import StateAction, StateTransition, Wizard
|
||||
|
||||
|
||||
class Package(metaclass=PoolMeta):
|
||||
__name__ = 'stock.package'
|
||||
|
||||
sendcloud_shipping_id = fields.Integer("ID", readonly=True)
|
||||
sendcloud_shipping_tracking_url = fields.Char(
|
||||
"Tracking URL", readonly=True)
|
||||
|
||||
def get_shipping_tracking_url(self, name):
|
||||
url = super().get_shipping_tracking_url(name)
|
||||
if (self.shipping_reference
|
||||
and self.shipment
|
||||
and self.shipment.id >= 0
|
||||
and self.shipment.carrier
|
||||
and self.shipment.carrier.shipping_service == 'sendcloud'):
|
||||
url = self.sendcloud_shipping_tracking_url
|
||||
return url
|
||||
|
||||
@classmethod
|
||||
def copy(cls, packages, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
else:
|
||||
default = default.copy()
|
||||
default.setdefault('sendcloud_shipping_id')
|
||||
default.setdefault('sendcloud_shipping_tracking_url')
|
||||
return super().copy(packages, default=default)
|
||||
|
||||
|
||||
class ShippingSendcloudMixin:
|
||||
__slots__ = ()
|
||||
|
||||
def get_sendcloud_credential(self):
|
||||
pool = Pool()
|
||||
SendcloudCredential = pool.get('carrier.credential.sendcloud')
|
||||
|
||||
pattern = self._get_sendcloud_credential_pattern()
|
||||
for credential in SendcloudCredential.search([]):
|
||||
if credential.match(pattern):
|
||||
return credential
|
||||
|
||||
def _get_sendcloud_credential_pattern(self):
|
||||
return {
|
||||
'company': self.company.id,
|
||||
}
|
||||
|
||||
def validate_packing_sendcloud(self):
|
||||
pass
|
||||
|
||||
|
||||
class ShipmentOut(ShippingSendcloudMixin, metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.out'
|
||||
|
||||
|
||||
class ShipmentInReturn(ShippingSendcloudMixin, metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.in.return'
|
||||
|
||||
|
||||
class CreateShipping(metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.create_shipping'
|
||||
|
||||
sendcloud = StateAction(
|
||||
'stock_package_shipping_sendcloud.act_create_shipping_wizard')
|
||||
|
||||
def transition_start(self):
|
||||
next_state = super().transition_start()
|
||||
if self.record.carrier.shipping_service == 'sendcloud':
|
||||
next_state = 'sendcloud'
|
||||
return next_state
|
||||
|
||||
def do_sendcloud(self, action):
|
||||
ctx = Transaction().context
|
||||
return action, {
|
||||
'model': ctx['active_model'],
|
||||
'id': ctx['active_id'],
|
||||
'ids': [ctx['active_id']],
|
||||
}
|
||||
|
||||
|
||||
class CreateShippingSendcloud(Wizard):
|
||||
__name__ = 'stock.shipment.create_shipping.sendcloud'
|
||||
|
||||
start = StateTransition()
|
||||
|
||||
def transition_start(self):
|
||||
pool = Pool()
|
||||
Package = pool.get('stock.package')
|
||||
|
||||
shipment = self.record
|
||||
if shipment.shipping_reference:
|
||||
raise AccessError(
|
||||
gettext('stock_package_shipping_sendcloud'
|
||||
'.msg_shipment_has_shipping_reference_number',
|
||||
shipment=shipment.rec_name))
|
||||
|
||||
credential = shipment.get_sendcloud_credential()
|
||||
carrier = shipment.carrier
|
||||
packages = shipment.root_packages
|
||||
|
||||
parcels = []
|
||||
for package in packages:
|
||||
parcels.append(self.get_parcel(shipment, package, credential))
|
||||
parcels = credential.create_parcels(parcels)
|
||||
|
||||
for package, parcel in zip_longest(packages, parcels):
|
||||
format_ = shipment.carrier.sendcloud_format.split()
|
||||
label_url = parcel['label']
|
||||
for key in format_:
|
||||
try:
|
||||
index = int(key)
|
||||
except ValueError:
|
||||
key += '_printer'
|
||||
label_url = label_url[key]
|
||||
else:
|
||||
label_url = label_url[index]
|
||||
package.sendcloud_shipping_id = parcel['id']
|
||||
package.shipping_label = credential.get_label(label_url)
|
||||
package.shipping_label_mimetype = carrier.shipping_label_mimetype
|
||||
package.shipping_reference = parcel['tracking_number']
|
||||
package.sendcloud_shipping_tracking_url = parcel['tracking_url']
|
||||
if not shipment.shipping_reference:
|
||||
shipment.shipping_reference = (
|
||||
parcel.get('colli_tracking_number')
|
||||
or parcel['tracking_number'])
|
||||
Package.save(packages)
|
||||
shipment.save()
|
||||
|
||||
return 'end'
|
||||
|
||||
def get_parcel(self, shipment, package, credential, usage=None):
|
||||
pool = Pool()
|
||||
UoM = pool.get('product.uom')
|
||||
ModelData = pool.get('ir.model.data')
|
||||
|
||||
cm = UoM(ModelData.get_id('product', 'uom_centimeter'))
|
||||
kg = UoM(ModelData.get_id('product', 'uom_kilogram'))
|
||||
party = shipment.shipping_to
|
||||
address = shipment.shipping_to_address
|
||||
phone = address.contact_mechanism_get(
|
||||
{'phone', 'mobile'}, usage=usage)
|
||||
email = address.contact_mechanism_get('email', usage=usage)
|
||||
street_lines = (address.street or '').splitlines()
|
||||
name = address_name(address, party)
|
||||
company_name = party.full_name if party.full_name != name else None
|
||||
parcel = {
|
||||
'name': name,
|
||||
'company_name': company_name,
|
||||
'address': street_lines[0] if street_lines else '',
|
||||
'address_2': (
|
||||
' '.join(street_lines[1:]) if len(street_lines) > 1 else ''),
|
||||
'house_number': address.numbers,
|
||||
'city': address.city,
|
||||
'postal_code': address.postal_code,
|
||||
'country': address.country.code if address.country else None,
|
||||
'country_state': (
|
||||
address.subdivision.code.split('-', 1)[1]
|
||||
if address.subdivision else None),
|
||||
'telephone': phone.value if phone else None,
|
||||
'email': email.value if email else None,
|
||||
'sender_address': credential.get_sender_address(shipment),
|
||||
'external_reference': '/'.join([shipment.number, package.number]),
|
||||
'quantity': 1,
|
||||
'order_number': shipment.number,
|
||||
'request_label': True,
|
||||
}
|
||||
if address.post_box:
|
||||
parcel['to_post_number'] = address.post_box
|
||||
if package.total_weight is not None:
|
||||
parcel['weight'] = ceil(
|
||||
UoM.compute_qty(
|
||||
package.weight_uom, package.total_weight, kg, round=False)
|
||||
* 100) / 100
|
||||
if (package.length is not None
|
||||
and package.width is not None
|
||||
and package.height is not None):
|
||||
parcel.update(
|
||||
length=ceil(
|
||||
UoM.compute_qty(
|
||||
package.length_uom, package.length, cm, round=False)
|
||||
* 100) / 100,
|
||||
width=ceil(
|
||||
UoM.compute_qty(
|
||||
package.width_uom, package.width, cm, round=False)
|
||||
* 100) / 100,
|
||||
height=ceil(
|
||||
UoM.compute_qty(
|
||||
package.height_uom, package.height, cm, round=False)
|
||||
* 100) / 100)
|
||||
shipping_method = credential.get_shipping_method(
|
||||
shipment, package=package)
|
||||
if shipping_method:
|
||||
parcel['shipment'] = {'id': shipping_method}
|
||||
else:
|
||||
parcel['apply_shipping_rules'] = True
|
||||
return parcel
|
||||
|
||||
|
||||
class CreateShippingSendcloud_Customs(metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.create_shipping.sendcloud'
|
||||
|
||||
def get_parcel(self, shipment, package, credential, usage=None):
|
||||
parcel = super().get_parcel(shipment, package, credential, usage=usage)
|
||||
if shipment.customs_international:
|
||||
parcel['customs_invoice_nr'] = shipment.number
|
||||
parcel['customs_shipment_type'] = self.get_customs_shipment_type(
|
||||
shipment, credential)
|
||||
parcel['export_type'] = self.get_export_type(shipment, credential)
|
||||
if description := shipment.shipping_description_used:
|
||||
parcel['general_notes'] = description[:500]
|
||||
parcel['tax_numbers'] = {
|
||||
'sender': list(self.get_tax_numbers(
|
||||
shipment, shipment.company.party, credential)),
|
||||
'receiver': list(self.get_tax_numbers(
|
||||
shipment, shipment.shipping_to, credential)),
|
||||
}
|
||||
if shipment.customs_agent:
|
||||
parcel['importer_of_record'] = self.get_importer_of_record(
|
||||
shipment, shipment.customs_agent, credential)
|
||||
parcel['tax_numbers']['importer_of_record'] = list(
|
||||
self.get_tax_numbers(
|
||||
shipment, shipment.customs_agent.party, credential))
|
||||
|
||||
parcel_items = []
|
||||
for k, v in shipment.customs_products.items():
|
||||
parcel_items.append(self.get_parcel_item(shipment, *k, **v))
|
||||
parcel['parcel_items'] = parcel_items
|
||||
return parcel
|
||||
|
||||
def get_customs_shipment_type(self, shipment, credential):
|
||||
return {
|
||||
'stock.shipment.out': 2,
|
||||
'stock.shipment.in.return': 4,
|
||||
}.get(shipment.__class__.__name__)
|
||||
|
||||
def get_export_type(self, shipment, credential):
|
||||
if shipment.shipping_to.tax_identifier:
|
||||
return 'commercial_b2b'
|
||||
else:
|
||||
return 'commercial_b2c'
|
||||
|
||||
def get_importer_of_record(
|
||||
self, shipment, customs_agent, credential, usage=None):
|
||||
address = customs_agent.address
|
||||
phone = address.contact_mechanism_get(
|
||||
{'phone', 'mobile'}, usage=usage)
|
||||
email = address.contact_mechanism_get('email', usage=usage)
|
||||
street_lines = (address.street or '').splitlines()
|
||||
return {
|
||||
'name': customs_agent.party.full_name[:75],
|
||||
'address_1': street_lines[0] if street_lines else '',
|
||||
'address_2': street_lines[1] if len(street_lines) > 1 else '',
|
||||
'house_number': None, # TODO
|
||||
'city': address.city,
|
||||
'postal_code': address.postal_code,
|
||||
'country_code': address.country.code if address.country else None,
|
||||
'country_state': (
|
||||
address.subdivision.split('-', 1)[1]
|
||||
if address.subdivision else None),
|
||||
'telephone': phone.value if phone else None,
|
||||
'email': email.value if email else None,
|
||||
}
|
||||
|
||||
def get_tax_numbers(self, shipment, party, credential):
|
||||
for tax_identifier in party.identifiers:
|
||||
if tax_identifier.type == 'br_vat':
|
||||
yield {
|
||||
'name': 'CNP',
|
||||
'country_code': 'BR',
|
||||
'value': tax_identifier.code[:100],
|
||||
}
|
||||
elif tax_identifier.type == 'ru_vat':
|
||||
yield {
|
||||
'name': 'INN',
|
||||
'country_code': 'RU',
|
||||
'value': tax_identifier.code[:100],
|
||||
}
|
||||
elif tax_identifier.type == 'eu_vat':
|
||||
yield {
|
||||
'name': 'VAT',
|
||||
'country_code': tax_identifier.code[:2],
|
||||
'value': tax_identifier.code[2:][:100],
|
||||
}
|
||||
elif tax_identifier.type.endswith('_vat'):
|
||||
yield {
|
||||
'name': 'VAT',
|
||||
'country_code': tax_identifier.type[:2].upper(),
|
||||
'value': tax_identifier.code[:100],
|
||||
}
|
||||
elif tax_identifier.type in {'us_ein', 'us_ssn'}:
|
||||
country, name = tax_identifier.type.upper().split('_')
|
||||
yield {
|
||||
'name': name,
|
||||
'country_code': country,
|
||||
'value': tax_identifier.code[:100],
|
||||
}
|
||||
|
||||
def get_parcel_item(
|
||||
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),
|
||||
})
|
||||
if not quantity.is_integer():
|
||||
value = price * Decimal(str(quantity))
|
||||
quantity = 1
|
||||
else:
|
||||
value = price
|
||||
|
||||
return {
|
||||
'hs_code': tariff_code.code if tariff_code else None,
|
||||
'weight': weight,
|
||||
'quantity': quantity,
|
||||
'description': product.name[:255],
|
||||
'origin_country': (
|
||||
product.country_of_origin.code if product.country_of_origin
|
||||
else None),
|
||||
'value': float(value.quantize(Decimal('.01'))),
|
||||
'product_id': product.code,
|
||||
}
|
||||
Reference in New Issue
Block a user