From 2e41eeb2e08cdd57ef3b92a1a07e75b448e0565c Mon Sep 17 00:00:00 2001 From: Dan Kobina Date: Fri, 2 Nov 2018 09:15:00 -0400 Subject: [PATCH 1/6] remove difference between quote_options and shipment_options --- purplship/domain/entities/__init__.py | 4 +-- purplship/domain/entities/datatypes.py | 24 ------------- purplship/domain/entities/factories.py | 36 ------------------- purplship/domain/mapper.py | 2 +- purplship/mappers/caps/caps_mapper.py | 2 +- purplship/mappers/dhl/dhl_mapper.py | 2 +- .../mappers/fedex/fedex_mapper/__init__.py | 2 +- .../fedex/fedex_mapper/partials/interface.py | 2 +- .../fedex/fedex_mapper/partials/rate.py | 2 +- purplship/mappers/ups/ups_mapper.py | 2 +- 10 files changed, 9 insertions(+), 69 deletions(-) diff --git a/purplship/domain/entities/__init__.py b/purplship/domain/entities/__init__.py index 7c301f9135..3bd57fa074 100644 --- a/purplship/domain/entities/__init__.py +++ b/purplship/domain/entities/__init__.py @@ -5,9 +5,9 @@ class Quote: """ manage quotes operations """ @staticmethod - def create(**args) -> quote_request: + def create(**args) -> shipment_request: """ Create a quote request payload """ - return quote_request(**quote_request_type(**args)._asdict()) + return shipment_request(**shipment_request_type(**args)._asdict()) class Tracking: """ manage tracking operations """ diff --git a/purplship/domain/entities/datatypes.py b/purplship/domain/entities/datatypes.py index 38a9dac1fe..91d43613ba 100644 --- a/purplship/domain/entities/datatypes.py +++ b/purplship/domain/entities/datatypes.py @@ -60,25 +60,6 @@ class invoice_type(NamedTuple): copies: int = None extra: Dict = {} -class quote_options(NamedTuple): - packages: List[package_type] - insured_amount: float = None - number_of_packages: int = None - packaging_type: str = None - is_document: bool = False - total_weight: float = None - weight_unit: str = "LB" - dimension_unit: str = "IN" - currency: str = None - paid_by: str = None - declared_value: float = None - duty_paid_by: str = None - payment_country_code: str = None - payment_account_number: str = None - shipper_account_number: str = None - services: List[str] = [] - extra: Dict = {} - class shipment_options(NamedTuple): packages: List[package_type] insured_amount: float = None @@ -110,11 +91,6 @@ class shipment_options(NamedTuple): label: label_type = None extra: Dict = {} -class quote_request(NamedTuple): - shipper: party - recipient: party - shipment: quote_options - class shipment_request(NamedTuple): shipper: party recipient: party diff --git a/purplship/domain/entities/factories.py b/purplship/domain/entities/factories.py index 17d220088f..bca60929d3 100644 --- a/purplship/domain/entities/factories.py +++ b/purplship/domain/entities/factories.py @@ -17,31 +17,6 @@ def __new__(cls, no_eei: str = None, aes: str = None, description: str = None, t ) -''' quote options Type definition ''' -class quote_options_type(namedtuple("quote_options_type", "packages insured_amount number_of_packages packaging_type is_document currency total_weight weight_unit dimension_unit paid_by declared_value duty_paid_by payment_country_code payment_account_number shipper_account_number services extra")): - def __new__(cls, packages: List[package_type], insured_amount: float = None, number_of_packages: int = None, packaging_type: str = None, is_document: bool = False, currency: str = None, total_weight: float = None, weight_unit: str = "LB", dimension_unit: str = "IN", paid_by: str = None, declared_value: float = None, duty_paid_by: str = None, payment_country_code: str = None, payment_account_number: str = None, shipper_account_number: str = None, services: List[str] = [], extra: Dict = {}): - return super(cls, quote_options_type).__new__( - cls, - [package_type(**p) for p in packages], - insured_amount, - number_of_packages, - packaging_type, - is_document, - currency, - total_weight, - weight_unit, - dimension_unit, - paid_by, - declared_value, - duty_paid_by, - payment_country_code, - payment_account_number, - shipper_account_number, - services, - extra - ) - - ''' shipment options Type definition ''' class shipment_options_type(namedtuple("shipment_options_type", "packages insured_amount number_of_packages packaging_type is_document currency date total_weight weight_unit dimension_unit paid_by duty_paid_by payment_type payment_country_code duty_payment_account declared_value payment_account_number shipper_account_number billing_account_number services customs invoice references commodities label, extra")): def __new__(cls, packages: List, insured_amount: float = None, number_of_packages: int = None, packaging_type: str = None, is_document: bool = False, currency: str = None, date: str = None, total_weight: float = None, weight_unit: str = "LB", dimension_unit: str = "IN", paid_by: str = None, duty_paid_by: str = None, payment_type: str = None, payment_country_code: str = None, duty_payment_account: str = None, declared_value: float = None, payment_account_number: str = None, shipper_account_number: str = None, billing_account_number: str = None, services: List[str] = [], customs: Dict = None, invoice: dict = None, references: List[str] = [], commodities: List[Dict] = [], label: Dict = None, extra: Dict = {}): @@ -76,17 +51,6 @@ def __new__(cls, packages: List, insured_amount: float = None, number_of_package ) -''' quote request Type definition ''' -class quote_request_type(namedtuple("quote_request_type", "shipper recipient shipment")): - def __new__(cls, shipper: Dict, recipient: Dict, shipment: Dict): - return super(cls, quote_request_type).__new__( - cls, - party(**shipper), - party(**recipient), - quote_options_type(**shipment) - ) - - ''' shipment request Type definition ''' class shipment_request_type(namedtuple("shipment_request_type", "shipper recipient shipment")): def __new__(cls, shipper: Dict, recipient: Dict, shipment: Dict): diff --git a/purplship/domain/mapper.py b/purplship/domain/mapper.py index 5a4c66a680..fc8bcec971 100644 --- a/purplship/domain/mapper.py +++ b/purplship/domain/mapper.py @@ -6,7 +6,7 @@ class Mapper: """ Unitied API to carrier API Mapper (Interface) """ client: Client - def create_quote_request(self, payload: E.quote_request): + def create_quote_request(self, payload: E.shipment_request): """ Create carrier specific quote request xml data from payload """ raise Exception("Not Supported") diff --git a/purplship/mappers/caps/caps_mapper.py b/purplship/mappers/caps/caps_mapper.py index 722d6eca2d..639d0da2b2 100644 --- a/purplship/mappers/caps/caps_mapper.py +++ b/purplship/mappers/caps/caps_mapper.py @@ -23,7 +23,7 @@ def parse_error_response(self, response) -> List[E.Error]: """ Interface functions """ - def create_quote_request(self, payload: E.quote_request) -> Rate.mailing_scenario: + def create_quote_request(self, payload: E.shipment_request) -> Rate.mailing_scenario: package = payload.shipment.packages[0] if len(payload.shipment.services) > 0: diff --git a/purplship/mappers/dhl/dhl_mapper.py b/purplship/mappers/dhl/dhl_mapper.py index cb8b77014f..a993c9f0f8 100644 --- a/purplship/mappers/dhl/dhl_mapper.py +++ b/purplship/mappers/dhl/dhl_mapper.py @@ -39,7 +39,7 @@ def parse_error_response(self, response) -> List[E.Error]: """ Interface functions """ - def create_quote_request(self, payload: E.quote_request) -> Req.DCTRequest: + def create_quote_request(self, payload: E.shipment_request) -> Req.DCTRequest: Request_ = self.init_request() Request_.MetaData = MetaData(SoftwareName="3PV", SoftwareVersion="1.0") diff --git a/purplship/mappers/fedex/fedex_mapper/__init__.py b/purplship/mappers/fedex/fedex_mapper/__init__.py index 10cb940b42..d46318a3a2 100644 --- a/purplship/mappers/fedex/fedex_mapper/__init__.py +++ b/purplship/mappers/fedex/fedex_mapper/__init__.py @@ -24,7 +24,7 @@ class FedexMapper( FedexShipmentMapperPartial ): - def create_quote_request(self, payload: E.quote_request) -> RateRequest: + def create_quote_request(self, payload: E.shipment_request) -> RateRequest: return self.create_rate_request(payload) def create_tracking_request(self, payload: E.tracking_request) -> TrackRequest: diff --git a/purplship/mappers/fedex/fedex_mapper/partials/interface.py b/purplship/mappers/fedex/fedex_mapper/partials/interface.py index df79586b4f..b99c108942 100644 --- a/purplship/mappers/fedex/fedex_mapper/partials/interface.py +++ b/purplship/mappers/fedex/fedex_mapper/partials/interface.py @@ -20,7 +20,7 @@ class FedexCapabilities: """ Requests """ - def create_rate_request(self, payload: E.quote_request) -> RateRequest: + def create_rate_request(self, payload: E.shipment_request) -> RateRequest: pass def create_track_request(self, payload: E.tracking_request) -> TrackRequest: diff --git a/purplship/mappers/fedex/fedex_mapper/partials/rate.py b/purplship/mappers/fedex/fedex_mapper/partials/rate.py index 9ddbce6bab..797622cb03 100644 --- a/purplship/mappers/fedex/fedex_mapper/partials/rate.py +++ b/purplship/mappers/fedex/fedex_mapper/partials/rate.py @@ -34,7 +34,7 @@ def _extract_quote(self, quotes: List[E.QuoteDetails], detailNode: 'XMLElement') ] - def create_rate_request(self, payload: E.quote_request) -> RateRequest: + def create_rate_request(self, payload: E.shipment_request) -> RateRequest: return RateRequest( WebAuthenticationDetail=self.webAuthenticationDetail, ClientDetail=self.clientDetail, diff --git a/purplship/mappers/ups/ups_mapper.py b/purplship/mappers/ups/ups_mapper.py index ad17f9b179..5dfc25feba 100644 --- a/purplship/mappers/ups/ups_mapper.py +++ b/purplship/mappers/ups/ups_mapper.py @@ -23,7 +23,7 @@ def __init__(self, client: UPSClient): - def create_quote_request(self, payload: E.quote_request) -> Rate.FreightRateRequest: + def create_quote_request(self, payload: E.shipment_request) -> Rate.FreightRateRequest: Request_ = Common.RequestType( TransactionReference=Common.TransactionReferenceType( TransactionIdentifier="TransactionIdentifier" From 7389c60075634b4f76c9bbbbfb89291786cdb6a1 Mon Sep 17 00:00:00 2001 From: Dan Kobina Date: Fri, 2 Nov 2018 09:26:03 -0400 Subject: [PATCH 2/6] move account_number into party --- purplship/domain/entities/datatypes.py | 2 +- purplship/domain/entities/factories.py | 5 ++--- purplship/mappers/caps/caps_mapper.py | 4 ++-- purplship/mappers/dhl/dhl_mapper.py | 2 +- purplship/mappers/fedex/fedex_mapper/partials/rate.py | 2 +- purplship/mappers/fedex/fedex_mapper/partials/shipment.py | 2 +- purplship/mappers/ups/ups_mapper.py | 2 +- tests/caps/shipment.py | 6 +++--- tests/dhl/shipment.py | 2 +- 9 files changed, 13 insertions(+), 14 deletions(-) diff --git a/purplship/domain/entities/datatypes.py b/purplship/domain/entities/datatypes.py index 91d43613ba..4056eacb57 100644 --- a/purplship/domain/entities/datatypes.py +++ b/purplship/domain/entities/datatypes.py @@ -5,6 +5,7 @@ class party(NamedTuple): city: str = None type: str = None tax_id: str = None + account_number: str = None person_name: str = None company_name: str = None country_name: str = None @@ -78,7 +79,6 @@ class shipment_options(NamedTuple): duty_payment_account: str = None payment_country_code: str = None payment_account_number: str = None - shipper_account_number: str = None ship_date: str = None customs: customs_type = None diff --git a/purplship/domain/entities/factories.py b/purplship/domain/entities/factories.py index bca60929d3..1812274d47 100644 --- a/purplship/domain/entities/factories.py +++ b/purplship/domain/entities/factories.py @@ -18,8 +18,8 @@ def __new__(cls, no_eei: str = None, aes: str = None, description: str = None, t ''' shipment options Type definition ''' -class shipment_options_type(namedtuple("shipment_options_type", "packages insured_amount number_of_packages packaging_type is_document currency date total_weight weight_unit dimension_unit paid_by duty_paid_by payment_type payment_country_code duty_payment_account declared_value payment_account_number shipper_account_number billing_account_number services customs invoice references commodities label, extra")): - def __new__(cls, packages: List, insured_amount: float = None, number_of_packages: int = None, packaging_type: str = None, is_document: bool = False, currency: str = None, date: str = None, total_weight: float = None, weight_unit: str = "LB", dimension_unit: str = "IN", paid_by: str = None, duty_paid_by: str = None, payment_type: str = None, payment_country_code: str = None, duty_payment_account: str = None, declared_value: float = None, payment_account_number: str = None, shipper_account_number: str = None, billing_account_number: str = None, services: List[str] = [], customs: Dict = None, invoice: dict = None, references: List[str] = [], commodities: List[Dict] = [], label: Dict = None, extra: Dict = {}): +class shipment_options_type(namedtuple("shipment_options_type", "packages insured_amount number_of_packages packaging_type is_document currency date total_weight weight_unit dimension_unit paid_by duty_paid_by payment_type payment_country_code duty_payment_account declared_value payment_account_number billing_account_number services customs invoice references commodities label, extra")): + def __new__(cls, packages: List, insured_amount: float = None, number_of_packages: int = None, packaging_type: str = None, is_document: bool = False, currency: str = None, date: str = None, total_weight: float = None, weight_unit: str = "LB", dimension_unit: str = "IN", paid_by: str = None, duty_paid_by: str = None, payment_type: str = None, payment_country_code: str = None, duty_payment_account: str = None, declared_value: float = None, payment_account_number: str = None, billing_account_number: str = None, services: List[str] = [], customs: Dict = None, invoice: dict = None, references: List[str] = [], commodities: List[Dict] = [], label: Dict = None, extra: Dict = {}): return super(cls, shipment_options_type).__new__( cls, [package_type(**p) for p in packages], @@ -39,7 +39,6 @@ def __new__(cls, packages: List, insured_amount: float = None, number_of_package duty_payment_account, declared_value, payment_account_number, - shipper_account_number, billing_account_number, services, customs_details_type(**customs) if customs else None, diff --git a/purplship/mappers/caps/caps_mapper.py b/purplship/mappers/caps/caps_mapper.py index 639d0da2b2..b0872973e1 100644 --- a/purplship/mappers/caps/caps_mapper.py +++ b/purplship/mappers/caps/caps_mapper.py @@ -40,7 +40,7 @@ def create_quote_request(self, payload: E.shipment_request) -> Rate.mailing_scen )) return Rate.mailing_scenario( - customer_number=payload.shipment.shipper_account_number or payload.shipment.payment_account_number or self.client.customer_number, + customer_number=payload.shipper.account_number or payload.shipment.payment_account_number or self.client.customer_number, contract_id=payload.shipment.extra.get('contract-id'), promo_code=payload.shipment.extra.get('promo-code'), quote_type=payload.shipment.extra.get('quote-type'), @@ -235,7 +235,7 @@ def _initialise_delivery_spec() -> Shipment.DeliverySpecType: ) shipment_ = Shipment.ShipmentType( - customer_request_id=payload.shipment.shipper_account_number or payload.shipment.payment_account_number or self.client.customer_number, + customer_request_id=payload.shipper.account_number or payload.shipment.payment_account_number or self.client.customer_number, quickship_label_requested=payload.shipment.extra.get('quickship-label-requested'), cpc_pickup_indicator=payload.shipment.extra.get('cpc-pickup-indicator'), requested_shipping_point=payload.shipment.extra.get('requested-shipping-point'), diff --git a/purplship/mappers/dhl/dhl_mapper.py b/purplship/mappers/dhl/dhl_mapper.py index a993c9f0f8..776ef1257f 100644 --- a/purplship/mappers/dhl/dhl_mapper.py +++ b/purplship/mappers/dhl/dhl_mapper.py @@ -139,7 +139,7 @@ def create_shipment_request(self, payload: E.shipment_request) ->ShipReq.Shipmen Request_.MetaData = MetaData(SoftwareName="3PV", SoftwareVersion="1.0") Billing_ = ShipReq.Billing( - ShipperAccountNumber=payload.shipment.shipper_account_number or self.client.account_number, + ShipperAccountNumber=payload.shipper.account_number or self.client.account_number, BillingAccountNumber=payload.shipment.payment_account_number, ShippingPaymentType=payload.shipment.paid_by, DutyAccountNumber=payload.shipment.duty_payment_account, diff --git a/purplship/mappers/fedex/fedex_mapper/partials/rate.py b/purplship/mappers/fedex/fedex_mapper/partials/rate.py index 797622cb03..639b7d4754 100644 --- a/purplship/mappers/fedex/fedex_mapper/partials/rate.py +++ b/purplship/mappers/fedex/fedex_mapper/partials/rate.py @@ -58,7 +58,7 @@ def create_rate_request(self, payload: E.shipment_request) -> RateRequest: PreferredCurrency=payload.shipment.currency, ShipmentAuthorizationDetail=None, Shipper=Party( - AccountNumber=payload.shipment.shipper_account_number, + AccountNumber=payload.shipper.account_number, Tins=None, Contact=Contact( ContactId=None, diff --git a/purplship/mappers/fedex/fedex_mapper/partials/shipment.py b/purplship/mappers/fedex/fedex_mapper/partials/shipment.py index c26d5a7e84..75c93faca4 100644 --- a/purplship/mappers/fedex/fedex_mapper/partials/shipment.py +++ b/purplship/mappers/fedex/fedex_mapper/partials/shipment.py @@ -81,7 +81,7 @@ def create_process_shipment_request(self, payload: E.shipment_request) -> Proces PreferredCurrency=payload.shipment.currency, ShipmentAuthorizationDetail=None, Shipper=Party( - AccountNumber=payload.shipment.shipper_account_number, + AccountNumber=payload.shipper.account_number, Tins=None, Contact=Contact( ContactId=None, diff --git a/purplship/mappers/ups/ups_mapper.py b/purplship/mappers/ups/ups_mapper.py index 5dfc25feba..65d1da05f1 100644 --- a/purplship/mappers/ups/ups_mapper.py +++ b/purplship/mappers/ups/ups_mapper.py @@ -65,7 +65,7 @@ def create_quote_request(self, payload: E.shipment_request) -> Rate.FreightRateR Payer=Rate.PayerType( Name=payload.shipment.payment_country_code or payload.shipper.country_code, Address=ShipFrom_.Address, - ShipperNumber=payload.shipment.shipper_account_number or payload.shipment.payment_account_number or self.client.account_number + ShipperNumber=payload.shipper.account_number or payload.shipment.payment_account_number or self.client.account_number ), ShipmentBillingOption=Rate.RateCodeDescriptionType( Code=10 diff --git a/tests/caps/shipment.py b/tests/caps/shipment.py index cf892c0907..612e42267f 100644 --- a/tests/caps/shipment.py +++ b/tests/caps/shipment.py @@ -383,7 +383,8 @@ def test_get_info(self, http_mock): "country_code": "CA", "person_name": "Bob", "phone_number": "1 (450) 823-8432", - "state_code": "QC" + "state_code": "QC", + "account_number": "123456789", }, "recipient": { "company_name": "CGI", @@ -396,7 +397,6 @@ def test_get_info(self, http_mock): }, "shipment": { "packages": [{"height": 9, "length": 6, "width": 12, "weight": 20.0}], - "shipper_account_number": "123456789", "label": {"format": "8.5x11"}, "services": ["DOM.EP"], "extra": { @@ -430,6 +430,7 @@ def test_get_info(self, http_mock): "city": "Ottawa", "postal_code": "K1A0B1", "phone_number": "555-555-5555", + "account_number": "123456789", "state_code": "ON" }, "recipient": { @@ -443,7 +444,6 @@ def test_get_info(self, http_mock): }, "shipment": { "packages": [{"height": 1, "length": 1, "width": 1, "weight": 15.0}], - "shipper_account_number": "123456789", "services": ["DOM.EP"], "extra": { "requested-shipping-point": "J8R1A2", diff --git a/tests/dhl/shipment.py b/tests/dhl/shipment.py index c33c8fedcc..60fbb862ac 100644 --- a/tests/dhl/shipment.py +++ b/tests/dhl/shipment.py @@ -26,6 +26,7 @@ def test_create_shipment_request(self): "email_address": "test@email.com", "state": "Arizona", "state_code": "AZ", + "account_number": "123456789", "extra": { "ShipperID": "123456789", "RegisteredAccount": "123456789", @@ -53,7 +54,6 @@ def test_create_shipment_request(self): shipment = { "packages": [{"id": "1", "height": 3, "length": 10, "width": 3, "weight": 4.0, "packaging_type": "EE"}], "is_document": False, - "shipper_account_number": "123456789", "paid_by": "S", "payment_account_number": "123456789", "duty_paid_by": "S", From 9df699ef19ec26d0395e668241ed71f8959eae01 Mon Sep 17 00:00:00 2001 From: Dan Kobina Date: Fri, 2 Nov 2018 23:04:41 -0400 Subject: [PATCH 3/6] refactor canada post mapper with partials --- purplship/mappers/caps/__init__.py | 6 +- .../mappers/caps/caps_mapper/__init__.py | 39 +++++ .../caps/caps_mapper/partials/__init__.py | 3 + .../caps/caps_mapper/partials/interface.py | 57 +++++++ .../mappers/caps/caps_mapper/partials/rate.py | 84 ++++++++++ .../partials/shipment.py} | 156 ++---------------- .../caps/caps_mapper/partials/track.py | 33 ++++ purplship/mappers/caps/caps_proxy.py | 3 +- 8 files changed, 233 insertions(+), 148 deletions(-) create mode 100644 purplship/mappers/caps/caps_mapper/__init__.py create mode 100644 purplship/mappers/caps/caps_mapper/partials/__init__.py create mode 100644 purplship/mappers/caps/caps_mapper/partials/interface.py create mode 100644 purplship/mappers/caps/caps_mapper/partials/rate.py rename purplship/mappers/caps/{caps_mapper.py => caps_mapper/partials/shipment.py} (69%) create mode 100644 purplship/mappers/caps/caps_mapper/partials/track.py diff --git a/purplship/mappers/caps/__init__.py b/purplship/mappers/caps/__init__.py index d7961f3d66..c8399af7fe 100644 --- a/purplship/mappers/caps/__init__.py +++ b/purplship/mappers/caps/__init__.py @@ -1,3 +1,3 @@ -from .caps_client import CanadaPostClient -from .caps_mapper import CanadaPostMapper -from .caps_proxy import CanadaPostProxy \ No newline at end of file +from purplship.mappers.caps.caps_client import CanadaPostClient +from purplship.mappers.caps.caps_mapper import CanadaPostMapper +from purplship.mappers.caps.caps_proxy import CanadaPostProxy \ No newline at end of file diff --git a/purplship/mappers/caps/caps_mapper/__init__.py b/purplship/mappers/caps/caps_mapper/__init__.py new file mode 100644 index 0000000000..b7c060c4dd --- /dev/null +++ b/purplship/mappers/caps/caps_mapper/__init__.py @@ -0,0 +1,39 @@ +from typing import Tuple, List, Union +from purplship.domain.mapper import Mapper +from purplship.domain import entities as E +from pycaps.rating import mailing_scenario +from pycaps.shipment import ShipmentType +from pycaps.ncshipment import NonContractShipmentType +from .partials import ( + CanadaPostRateMapperPartial, + CanadaPostTrackMapperPartial, + CanadaPostShipmentMapperPartial +) + + +class CanadaPostMapper( + Mapper, + CanadaPostRateMapperPartial, + CanadaPostTrackMapperPartial, + CanadaPostShipmentMapperPartial + ): + + def create_quote_request(self, payload: E.shipment_request) -> mailing_scenario: + return self.create_mailing_scenario(payload) + + def create_tracking_request(self, payload: E.tracking_request) -> List[str]: + return self.create_tracking_pins(payload) + + def create_shipment_request(self, payload: E.shipment_request) -> Union[ShipmentType, NonContractShipmentType]: + return self.create_shipment(payload) + + + + def parse_quote_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + return self.parse_price_quotes(response) + + def parse_tracking_response(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: + return self.parse_tracking_summary(response) + + def parse_shipment_response(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + return self.parse_shipment_info(response) \ No newline at end of file diff --git a/purplship/mappers/caps/caps_mapper/partials/__init__.py b/purplship/mappers/caps/caps_mapper/partials/__init__.py new file mode 100644 index 0000000000..648e9bcbbd --- /dev/null +++ b/purplship/mappers/caps/caps_mapper/partials/__init__.py @@ -0,0 +1,3 @@ +from .rate import CanadaPostMapperPartial as CanadaPostRateMapperPartial +from .track import CanadaPostMapperPartial as CanadaPostTrackMapperPartial +from .shipment import CanadaPostMapperPartial as CanadaPostShipmentMapperPartial \ No newline at end of file diff --git a/purplship/mappers/caps/caps_mapper/partials/interface.py b/purplship/mappers/caps/caps_mapper/partials/interface.py new file mode 100644 index 0000000000..7fd14483bd --- /dev/null +++ b/purplship/mappers/caps/caps_mapper/partials/interface.py @@ -0,0 +1,57 @@ +from typing import Tuple, List, Union +from functools import reduce +from purplship.mappers.caps import CanadaPostClient +from purplship.domain import entities as E +from pycaps.rating import mailing_scenario +from pycaps.shipment import ShipmentType +from pycaps.ncshipment import NonContractShipmentType +from pycaps.messages import messageType + + +class CanadaPostCapabilities: + """ + CanadaPost native service request types + """ + + """ Requests """ + + def create_mailing_scenario(self, payload: E.shipment_request) -> mailing_scenario: + pass + + def create_tracking_pins(self, payload: E.tracking_request) -> List[str]: + pass + + def create_shipment(self, payload: E.shipment_request) -> Union[ShipmentType, NonContractShipmentType]: + pass + + + """ Replys """ + + def parse_price_quotes(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + pass + + def parse_tracking_summary(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: + pass + + def parse_shipment_info(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + pass + + +class CanadaPostMapperBase(CanadaPostCapabilities): + """ + CanadaPost mapper base class + """ + def __init__(self, client: CanadaPostClient): + self.client = client + + def parse_error_response(self, response: 'XMLElement') -> List[E.Error]: + messages = response.xpath('.//*[local-name() = $name]', name="message") + return reduce(self._extract_error, messages, []) + + def _extract_error(self, errors: List[E.Error], messageNode: 'XMLElement') -> List[E.Error]: + message = messageType() + message.build(messageNode) + return errors + [ + E.Error(code=message.code, + message=message.description, carrier=self.client.carrier_name) + ] diff --git a/purplship/mappers/caps/caps_mapper/partials/rate.py b/purplship/mappers/caps/caps_mapper/partials/rate.py new file mode 100644 index 0000000000..2a17f12148 --- /dev/null +++ b/purplship/mappers/caps/caps_mapper/partials/rate.py @@ -0,0 +1,84 @@ +from pycaps.rating import * +from datetime import datetime +from .interface import reduce, Tuple, List, E, CanadaPostMapperBase + + +class CanadaPostMapperPartial(CanadaPostMapperBase): + + def parse_price_quotes(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + price_quotes = response.xpath('.//*[local-name() = $name]', name="price-quote") + quotes = reduce(self._extract_quote, price_quotes, []) + return (quotes, self.parse_error_response(response)) + + def _extract_quote(self, quotes: List[E.QuoteDetails], price_quoteNode: 'XMLElement') -> List[E.QuoteDetails]: + price_quote = price_quoteType() + price_quote.build(price_quoteNode) + discounts = [E.ChargeDetails(name=d.adjustment_name, currency="CAD", amount=float(d.adjustment_cost or 0)) for d in price_quote.price_details.adjustments.adjustment] + return quotes + [ + E.QuoteDetails( + carrier=self.client.carrier_name, + currency="CAD", + delivery_date=str(price_quote.service_standard.expected_delivery_date), + service_name=price_quote.service_name, + service_type=price_quote.service_code, + base_charge=float(price_quote.price_details.base or 0), + total_charge=float(price_quote.price_details.due or 0), + discount=reduce(lambda sum, d: sum + d.amount, discounts, 0), + duties_and_taxes=float(price_quote.price_details.taxes.gst.valueOf_ or 0) + + float(price_quote.price_details.taxes.pst.valueOf_ or 0) + + float(price_quote.price_details.taxes.hst.valueOf_ or 0), + extra_charges=list(map(lambda a: E.ChargeDetails( + name=a.adjustment_name, currency="CAD", amount=float(a.adjustment_cost or 0)), price_quote.price_details.adjustments.adjustment) + ) + ) + ] + + + def create_mailing_scenario(self, payload: E.shipment_request) -> mailing_scenario: + package = payload.shipment.packages[0] + + if len(payload.shipment.services) > 0: + services = servicesType() + for code in payload.shipment.services: + services.add_service_code(code) + + if 'options' in payload.shipment.extra: + options = optionsType() + for option in payload.shipment.extra.get('options'): + options.add_option(optionType( + option_amount=option.get('option-amount'), + option_code=option.get('option-code') + )) + + return mailing_scenario( + customer_number=payload.shipper.account_number or payload.shipment.payment_account_number or self.client.customer_number, + contract_id=payload.shipment.extra.get('contract-id'), + promo_code=payload.shipment.extra.get('promo-code'), + quote_type=payload.shipment.extra.get('quote-type'), + expected_mailing_date=payload.shipment.extra.get('expected-mailing-date'), + options=options if ('options' in payload.shipment.extra) else None, + parcel_characteristics=parcel_characteristicsType( + weight=payload.shipment.total_weight or package.weight, + dimensions=dimensionsType( + length=package.length, + width=package.width, + height=package.height + ), + unpackaged=payload.shipment.extra.get('unpackaged'), + mailing_tube=payload.shipment.extra.get('mailing-tube'), + oversized=payload.shipment.extra.get('oversized') + ), + services=services if (len(payload.shipment.services) > 0) else None, + origin_postal_code=payload.shipper.postal_code, + destination=destinationType( + domestic=domesticType( + postal_code=payload.recipient.postal_code + ) if (payload.recipient.country_code == 'CA') else None, + united_states=united_statesType( + zip_code=payload.recipient.postal_code + ) if (payload.recipient.country_code == 'US') else None, + international=internationalType( + country_code=payload.shipment.country_code + ) if (payload.recipient.country_code not in ['US', 'CA']) else None + ) + ) diff --git a/purplship/mappers/caps/caps_mapper.py b/purplship/mappers/caps/caps_mapper/partials/shipment.py similarity index 69% rename from purplship/mappers/caps/caps_mapper.py rename to purplship/mappers/caps/caps_mapper/partials/shipment.py index b0872973e1..cd2bde7a82 100644 --- a/purplship/mappers/caps/caps_mapper.py +++ b/purplship/mappers/caps/caps_mapper/partials/shipment.py @@ -1,152 +1,22 @@ -from typing import List, Tuple, Union -from functools import reduce -import time -from purplship.domain import entities as E -from purplship.domain.mapper import Mapper +from pycaps import shipment as Shipment, ncshipment as NCShipment +from base64 import b64encode +from datetime import datetime +from .interface import reduce, Tuple, List, Union, E, CanadaPostMapperBase -from purplship.mappers.caps.caps_client import CanadaPostClient -from pycaps import rating as Rate, track as Track, messages as Msg -from pycaps import shipment as Shipment -from pycaps import ncshipment as NCShipment +class CanadaPostMapperPartial(CanadaPostMapperBase): + def parse_shipment_info(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + shipment = self._extract_shipment(response) if len(response.xpath('.//*[local-name() = $name]', name="shipment-id")) > 0 else None + return (shipment, self.parse_error_response(response)) -class CanadaPostMapper(Mapper): - def __init__(self, client: CanadaPostClient): - self.client = client - - """ Shared functions """ - - def parse_error_response(self, response) -> List[E.Error]: - messages = response.xpath('.//*[local-name() = $name]', name="message") - return reduce(self._extract_error, messages, []) - - """ Interface functions """ - - def create_quote_request(self, payload: E.shipment_request) -> Rate.mailing_scenario: - package = payload.shipment.packages[0] - - if len(payload.shipment.services) > 0: - services = Rate.servicesType() - for code in payload.shipment.services: - services.add_service_code(code) - - if 'options' in payload.shipment.extra: - options = Rate.optionsType() - for option in payload.shipment.extra.get('options'): - options.add_option(Rate.optionType( - option_amount=option.get('option-amount'), - option_code=option.get('option-code') - )) - - return Rate.mailing_scenario( - customer_number=payload.shipper.account_number or payload.shipment.payment_account_number or self.client.customer_number, - contract_id=payload.shipment.extra.get('contract-id'), - promo_code=payload.shipment.extra.get('promo-code'), - quote_type=payload.shipment.extra.get('quote-type'), - expected_mailing_date=payload.shipment.extra.get('expected-mailing-date'), - options=options if ('options' in payload.shipment.extra) else None, - parcel_characteristics=Rate.parcel_characteristicsType( - weight=payload.shipment.total_weight or package.weight, - dimensions=Rate.dimensionsType( - length=package.length, - width=package.width, - height=package.height - ), - unpackaged=payload.shipment.extra.get('unpackaged'), - mailing_tube=payload.shipment.extra.get('mailing-tube'), - oversized=payload.shipment.extra.get('oversized') - ), - services=services if (len(payload.shipment.services) > 0) else None, - origin_postal_code=payload.shipper.postal_code, - destination=Rate.destinationType( - domestic=Rate.domesticType( - postal_code=payload.recipient.postal_code - ) if (payload.recipient.country_code == 'CA') else None, - united_states=Rate.united_statesType( - zip_code=payload.recipient.postal_code - ) if (payload.recipient.country_code == 'US') else None, - international=Rate.internationalType( - country_code=payload.shipment.country_code - ) if (payload.recipient.country_code not in ['US', 'CA']) else None - ) - ) - - def create_tracking_request(self, payload: E.tracking_request) -> List[str]: - return payload.tracking_numbers - - def create_shipment_request(self, payload: E.shipment_request) -> Union[Shipment.ShipmentType, NCShipment.NonContractShipmentType]: + def create_shipment(self, payload: E.shipment_request) -> Union[Shipment.ShipmentType, NCShipment.NonContractShipmentType]: is_non_contract = payload.shipment.extra.get('settlement-info') is None shipment = self._create_ncshipment(payload) if is_non_contract else self._create_shipment(payload) return shipment - """ Mapper Interface parsing methods """ - - def parse_quote_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: - price_quotes = response.xpath('.//*[local-name() = $name]', name="price-quote") - quotes = reduce(self._extract_quote, price_quotes, []) - return (quotes, self.parse_error_response(response)) - - def parse_tracking_response(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: - pin_summaries = response.xpath('.//*[local-name() = $name]', name="pin-summary") - trackings = reduce(self._extract_tracking, pin_summaries, []) - return (trackings, self.parse_error_response(response)) - - def parse_shipment_response(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: - shipment = self._extract_shipment(response) if len(response.xpath('.//*[local-name() = $name]', name="shipment-id")) > 0 else None - return (shipment, self.parse_error_response(response)) - - """ Helpers functions """ - - def _extract_error(self, errors: List[E.Error], messageNode: 'XMLElement') -> List[E.Error]: - message = Msg.messageType() - message.build(messageNode) - return errors + [ - E.Error(code=message.code, - message=message.description, carrier=self.client.carrier_name) - ] - - def _extract_quote(self, quotes: List[E.QuoteDetails], price_quoteNode: 'XMLElement') -> List[E.QuoteDetails]: - price_quote = Rate.price_quoteType() - price_quote.build(price_quoteNode) - discounts = [E.ChargeDetails(name=d.adjustment_name, currency="CAD", amount=float(d.adjustment_cost or 0)) for d in price_quote.price_details.adjustments.adjustment] - return quotes + [ - E.QuoteDetails( - carrier=self.client.carrier_name, - currency="CAD", - delivery_date=str(price_quote.service_standard.expected_delivery_date), - service_name=price_quote.service_name, - service_type=price_quote.service_code, - base_charge=float(price_quote.price_details.base or 0), - total_charge=float(price_quote.price_details.due or 0), - discount=reduce(lambda sum, d: sum + d.amount, discounts, 0), - duties_and_taxes=float(price_quote.price_details.taxes.gst.valueOf_ or 0) + - float(price_quote.price_details.taxes.pst.valueOf_ or 0) + - float(price_quote.price_details.taxes.hst.valueOf_ or 0), - extra_charges=list(map(lambda a: E.ChargeDetails( - name=a.adjustment_name, currency="CAD", amount=float(a.adjustment_cost or 0)), price_quote.price_details.adjustments.adjustment) - ) - ) - ] - - def _extract_tracking(self, trackings: List[E.TrackingDetails], pin_summaryNode: 'XMLElement') -> List[E.TrackingDetails]: - pin_summary = Track.pin_summary() - pin_summary.build(pin_summaryNode) - return trackings + [ - E.TrackingDetails( - carrier=self.client.carrier_name, - tracking_number=pin_summary.pin, - shipment_date=str(pin_summary.mailed_on_date), - events=[E.TrackingEvent( - date=str(pin_summary.event_date_time), - signatory=pin_summary.signatory_name, - code=pin_summary.event_type, - location=pin_summary.event_location, - description=pin_summary.event_description - )] - ) - ] + """ Private functions """ def _extract_shipment(self, response: 'XMLElement') -> E.ShipmentDetails: is_non_contract = len(response.xpath('.//*[local-name() = $name]', name="non-contract-shipment-info")) > 0 @@ -205,9 +75,6 @@ def _extract_shipment(self, response: 'XMLElement') -> E.ShipmentDetails: ) ) - - """ Private functions """ - def _create_shipment(self, payload: E.shipment_request) -> Shipment.ShipmentType: def _initialise_delivery_spec() -> Shipment.DeliverySpecType: """ @@ -425,4 +292,5 @@ def _has_any(dictionary: dict, keys: List[str]) -> bool: """ Return True if at least one key of the list is contained by the dictionary """ - return any([k for k in keys if k in dictionary]) \ No newline at end of file + return any([k for k in keys if k in dictionary]) + diff --git a/purplship/mappers/caps/caps_mapper/partials/track.py b/purplship/mappers/caps/caps_mapper/partials/track.py new file mode 100644 index 0000000000..3c324ae7a8 --- /dev/null +++ b/purplship/mappers/caps/caps_mapper/partials/track.py @@ -0,0 +1,33 @@ +from pycaps.track import * +from .interface import reduce, Tuple, List, E, CanadaPostMapperBase + + +class CanadaPostMapperPartial(CanadaPostMapperBase): + + def parse_tracking_summary(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: + pin_summaries = response.xpath('.//*[local-name() = $name]', name="pin-summary") + trackings = reduce(self._extract_tracking, pin_summaries, []) + return (trackings, self.parse_error_response(response)) + + def _extract_tracking(self, trackings: List[E.TrackingDetails], pin_summaryNode: 'XMLElement') -> List[E.TrackingDetails]: + pin_summary_ = pin_summary() + pin_summary_.build(pin_summaryNode) + return trackings + [ + E.TrackingDetails( + carrier=self.client.carrier_name, + tracking_number=pin_summary_.pin, + shipment_date=str(pin_summary_.mailed_on_date), + events=[E.TrackingEvent( + date=str(pin_summary_.event_date_time), + signatory=pin_summary_.signatory_name, + code=pin_summary_.event_type, + location=pin_summary_.event_location, + description=pin_summary_.event_description + )] + ) + ] + + def create_tracking_pins(self, payload: E.tracking_request) -> List[str]: + return payload.tracking_numbers + + \ No newline at end of file diff --git a/purplship/mappers/caps/caps_proxy.py b/purplship/mappers/caps/caps_proxy.py index 6e9a728f22..f01c3daa2a 100644 --- a/purplship/mappers/caps/caps_proxy.py +++ b/purplship/mappers/caps/caps_proxy.py @@ -1,7 +1,8 @@ from io import StringIO from typing import List, Union from gds_helpers import export, to_xml, request as http, exec_parrallel, bundle_xml -from purplship.mappers.caps.caps_mapper import CanadaPostMapper, CanadaPostClient +from purplship.mappers.caps.caps_mapper import CanadaPostMapper +from purplship.mappers.caps.caps_client import CanadaPostClient from pycaps import rating as Rate, pickuprequest as Pick from purplship.domain.proxy import Proxy from base64 import b64encode From f0a8f7234230d80af27a677e9aac1d43682b8cfd Mon Sep 17 00:00:00 2001 From: Dan Kobina Date: Sat, 3 Nov 2018 00:18:02 -0400 Subject: [PATCH 4/6] refactor DHL mapper with partials --- purplship/mappers/dhl/dhl_mapper.py | 532 ------------------ purplship/mappers/dhl/dhl_mapper/__init__.py | 62 ++ .../dhl/dhl_mapper/partials/__init__.py | 4 + .../dhl/dhl_mapper/partials/interface.py | 92 +++ .../mappers/dhl/dhl_mapper/partials/pickup.py | 151 +++++ .../mappers/dhl/dhl_mapper/partials/rate.py | 142 +++++ .../dhl/dhl_mapper/partials/shipment.py | 199 +++++++ .../mappers/dhl/dhl_mapper/partials/track.py | 46 ++ purplship/mappers/dhl/dhl_proxy.py | 3 +- 9 files changed, 698 insertions(+), 533 deletions(-) delete mode 100644 purplship/mappers/dhl/dhl_mapper.py create mode 100644 purplship/mappers/dhl/dhl_mapper/__init__.py create mode 100644 purplship/mappers/dhl/dhl_mapper/partials/__init__.py create mode 100644 purplship/mappers/dhl/dhl_mapper/partials/interface.py create mode 100644 purplship/mappers/dhl/dhl_mapper/partials/pickup.py create mode 100644 purplship/mappers/dhl/dhl_mapper/partials/rate.py create mode 100644 purplship/mappers/dhl/dhl_mapper/partials/shipment.py create mode 100644 purplship/mappers/dhl/dhl_mapper/partials/track.py diff --git a/purplship/mappers/dhl/dhl_mapper.py b/purplship/mappers/dhl/dhl_mapper.py deleted file mode 100644 index 776ef1257f..0000000000 --- a/purplship/mappers/dhl/dhl_mapper.py +++ /dev/null @@ -1,532 +0,0 @@ -from typing import List, Tuple, TypeVar, Union -from functools import reduce -import time -from base64 import b64decode -from purplship.domain import entities as E -from purplship.domain.mapper import Mapper -from purplship.mappers.dhl.dhl_client import DHLClient -from pydhl import DCT_req_global as Req, DCT_Response_global as Res, tracking_request_known as Track, tracking_response as TrackRes -from pydhl.datatypes_global_v61 import ServiceHeader, MetaData, Request -from pydhl import ship_val_global_req_61 as ShipReq -from gds_helpers import jsonify_xml, jsonify -from lxml import etree -from pydhl.book_pickup_global_req_20 import BookPURequest -from pydhl.modify_pickup_global_req_20 import ModifyPURequest -from pydhl.cancel_pickup_global_req_20 import CancelPURequest -from pydhl.book_pickup_global_res_20 import BookPUResponse -from pydhl.modify_pickup_global_res_20 import ModifyPUResponse -from pydhl import pickupdatatypes_global_20 as PickpuDataTypes - -class DHLMapper(Mapper): - def __init__(self, client: DHLClient): - self.client = client - - """ Shared functions """ - - def init_request(self) -> Request: - ServiceHeader_ = ServiceHeader( - MessageReference="1234567890123456789012345678901", - MessageTime=time.strftime('%Y-%m-%dT%H:%M:%S'), - SiteID=self.client.site_id, - Password=self.client.password - ) - return Request(ServiceHeader=ServiceHeader_) - - def parse_error_response(self, response) -> List[E.Error]: - conditions = response.xpath( - './/*[local-name() = $name]', name="Condition") - return reduce(self._extract_error, conditions, []) - - """ Interface functions """ - - def create_quote_request(self, payload: E.shipment_request) -> Req.DCTRequest: - Request_ = self.init_request() - Request_.MetaData = MetaData(SoftwareName="3PV", SoftwareVersion="1.0") - - From_ = Req.DCTFrom( - CountryCode=payload.shipper.country_code, - Postalcode=payload.shipper.postal_code, - City=payload.shipper.city, - Suburb=payload.shipper.state_code - ) - - To_ = Req.DCTTo( - CountryCode=payload.recipient.country_code, - Postalcode=payload.recipient.postal_code, - City=payload.recipient.city, - Suburb=payload.recipient.state_code - ) - - Pieces = Req.PiecesType() - default_packaging_type = "FLY" if payload.shipment.is_document else "BOX" - for index, piece in enumerate(payload.shipment.packages): - Pieces.add_Piece(Req.PieceType( - PieceID=piece.id or str(index), - PackageTypeCode=piece.packaging_type or default_packaging_type, - Height=piece.height, Width=piece.width, - Weight=piece.weight, Depth=piece.length - )) - - payment_country_code = "CA" if not payload.shipment.payment_country_code else payload.shipment.payment_country_code - - BkgDetails_ = Req.BkgDetailsType( - PaymentCountryCode=payment_country_code, - NetworkTypeCode=payload.shipment.extra.get('NetworkTypeCode') or "AL", - WeightUnit=payload.shipment.weight_unit or "LB", - DimensionUnit=payload.shipment.dimension_unit or "IN", - ReadyTime=time.strftime("PT%HH%MM"), - Date=time.strftime("%Y-%m-%d"), - IsDutiable="N" if payload.shipment.is_document else "Y", - Pieces=Pieces, - NumberOfPieces=payload.shipment.number_of_packages, - ShipmentWeight=payload.shipment.total_weight, - Volume=payload.shipment.extra.get('Volume'), - PaymentAccountNumber=payload.shipment.payment_account_number or self.client.account_number, - InsuredCurrency=payload.shipment.currency or "USD", - InsuredValue=payload.shipment.insured_amount, - PaymentType=payload.shipment.extra.get('PaymentType'), - AcctPickupCloseTime=payload.shipment.extra.get('AcctPickupCloseTime'), - ) - - product_code = "P" if payload.shipment.is_document else "D" - BkgDetails_.add_QtdShp(Req.QtdShpType( - GlobalProductCode=product_code, - LocalProductCode=product_code - )) - - if payload.shipment.insured_amount is not None: - BkgDetails_.QtdShp[0].add_QtdShpExChrg( - Req.QtdShpExChrgType(SpecialServiceType="II") - ) - - if not payload.shipment.is_document: - BkgDetails_.QtdShp[0].add_QtdShpExChrg( - Req.QtdShpExChrgType(SpecialServiceType="DD") - ) - - GetQuote = Req.GetQuoteType( - Request=Request_, - From=From_, - To=To_, - BkgDetails=BkgDetails_, - Dutiable=Req.Dutiable( - DeclaredValue=payload.shipment.declared_value, - DeclaredCurrency=payload.shipment.currency, - ScheduleB=payload.shipment.extra.get('ScheduleB'), - ExportLicense=payload.shipment.extra.get('ExportLicense'), - ShipperEIN=payload.shipment.extra.get('ShipperEIN'), - ShipperIDType=payload.shipment.extra.get('ShipperIDType'), - ConsigneeIDType=payload.shipment.extra.get('ConsigneeIDType'), - ImportLicense=payload.shipment.extra.get('ImportLicense'), - ConsigneeEIN=payload.shipment.extra.get('ConsigneeEIN'), - TermsOfTrade=payload.shipment.extra.get('TermsOfTrade'), - CommerceLicensed=payload.shipment.extra.get('CommerceLicensed'), - Filing=(lambda filing: - Req.Filing( - FilingType=filing.get('FilingType'), - FTSR=filing.get('FTSR'), - ITN=filing.get('ITN'), - AES4EIN=filing.get('AES4EIN') - ) - )(payload.shipment.extra.get('Filing')) if 'Filing' in payload.shipment.extra else None - ) if payload.shipment.declared_value is not None else None - ) - - return Req.DCTRequest(schemaVersion="1.0", GetQuote=GetQuote) - - def create_shipment_request(self, payload: E.shipment_request) ->ShipReq.ShipmentRequest: - Request_ = self.init_request() - Request_.MetaData = MetaData(SoftwareName="3PV", SoftwareVersion="1.0") - - Billing_ = ShipReq.Billing( - ShipperAccountNumber=payload.shipper.account_number or self.client.account_number, - BillingAccountNumber=payload.shipment.payment_account_number, - ShippingPaymentType=payload.shipment.paid_by, - DutyAccountNumber=payload.shipment.duty_payment_account, - DutyPaymentType=payload.shipment.duty_paid_by - ) - - Consignee_ = ShipReq.Consignee( - CompanyName=payload.recipient.company_name, - PostalCode=payload.recipient.postal_code, - CountryCode=payload.recipient.country_code, - City=payload.recipient.city, - CountryName=payload.recipient.country_name, - Division=payload.recipient.state, - DivisionCode=payload.recipient.state_code - ) - - if any([payload.recipient.person_name, payload.recipient.email_address]): - Consignee_.Contact = ShipReq.Contact( - PersonName=payload.recipient.person_name, - PhoneNumber=payload.recipient.phone_number, - Email=payload.recipient.email_address, - FaxNumber=payload.recipient.extra.get('FaxNumber'), - Telex=payload.recipient.extra.get('Telex'), - PhoneExtension=payload.recipient.extra.get('PhoneExtension'), - MobilePhoneNumber=payload.recipient.extra.get('MobilePhoneNumber') - ) - - [Consignee_.add_AddressLine(line) - for line in payload.recipient.address_lines] - - Shipper_ = ShipReq.Shipper( - ShipperID=payload.shipment.extra.get('ShipperID') or Billing_.ShipperAccountNumber, - RegisteredAccount=payload.shipment.extra.get('ShipperID') or Billing_.ShipperAccountNumber, - CompanyName=payload.shipper.company_name, - PostalCode=payload.shipper.postal_code, - CountryCode=payload.shipper.country_code, - City=payload.shipper.city, - CountryName=payload.shipper.country_name, - Division=payload.shipper.state, - DivisionCode=payload.shipper.state_code - ) - - if any([payload.shipper.person_name, payload.shipper.email_address]): - Shipper_.Contact = ShipReq.Contact( - PersonName=payload.shipper.person_name, - PhoneNumber=payload.shipper.phone_number, - Email=payload.shipper.email_address, - FaxNumber=payload.shipper.extra.get('FaxNumber'), - Telex=payload.shipper.extra.get('Telex'), - PhoneExtension=payload.shipper.extra.get('PhoneExtension'), - MobilePhoneNumber=payload.shipper.extra.get('MobilePhoneNumber') - ) - - [Shipper_.add_AddressLine(line) - for line in payload.shipper.address_lines] - - Pieces_ = ShipReq.Pieces() - for p in payload.shipment.packages: - Pieces_.add_Piece(ShipReq.Piece( - PieceID=p.id, - PackageType=p.packaging_type, - Weight=p.weight, - DimWeight=p.extra.get('DimWeight'), - Height=p.height, - Width=p.width, - Depth=p.length, - PieceContents=p.description - )) - - """ - Get PackageType from extra when implementing multi carrier, - Get weight from total_weight if specified otherwise calculated from packages weight sum - """ - ShipmentDetails_ = ShipReq.ShipmentDetails( - NumberOfPieces=len(payload.shipment.packages), - Pieces=Pieces_, - Weight=payload.shipment.total_weight or sum([p.weight for p in payload.shipment.packages]), - CurrencyCode=payload.shipment.currency or "USD", - WeightUnit=(payload.shipment.weight_unit or "LB")[0], - DimensionUnit=(payload.shipment.dimension_unit or "IN")[0], - Date=payload.shipment.date or time.strftime('%Y-%m-%d'), - PackageType=payload.shipment.packaging_type or payload.shipment.extra.get('PackageType'), - IsDutiable= "N" if payload.shipment.is_document else "Y", - InsuredAmount=payload.shipment.insured_amount, - DoorTo=payload.shipment.extra.get('DoorTo'), - GlobalProductCode=payload.shipment.extra.get('GlobalProductCode'), - LocalProductCode=payload.shipment.extra.get('LocalProductCode'), - Contents=payload.shipment.extra.get('Contents') or "..." - ) - - ShipmentRequest_ = ShipReq.ShipmentRequest( - schemaVersion="6.1", - Request=Request_, - RegionCode=payload.shipment.extra.get('RegionCode') or "AM", - RequestedPickupTime=payload.shipment.extra.get('RequestedPickupTime') or "Y", - LanguageCode=payload.shipment.extra.get('LanguageCode') or "en", - PiecesEnabled=payload.shipment.extra.get('PiecesEnabled') or "Y", - NewShipper=payload.shipment.extra.get('NewShipper'), - Billing=Billing_, - Consignee=Consignee_, - Shipper=Shipper_, - ShipmentDetails=ShipmentDetails_, - EProcShip=payload.shipment.extra.get('EProcShip') - ) - - if payload.shipment.label is not None: - DocImages_ = ShipReq.DocImages() - Image_ = None if 'Image' not in payload.shipment.label.extra else b64decode( - payload.shipment.label.extra.get('Image') + '=' * (-len(payload.shipment.label.extra.get('Image')) % 4) - ) - DocImages_.add_DocImage(ShipReq.DocImage( - Type=payload.shipment.label.type, - ImageFormat=payload.shipment.label.format, - Image=Image_ - )) - ShipmentRequest_.DocImages = DocImages_ - - if ShipmentDetails_.IsDutiable == "Y": - ShipmentRequest_.Dutiable = ShipReq.Dutiable( - DeclaredCurrency=ShipmentDetails_.CurrencyCode, - DeclaredValue=payload.shipment.declared_value, - TermsOfTrade=payload.shipment.customs.terms_of_trade, - ScheduleB=payload.shipment.customs.extra.get('ScheduleB'), - ExportLicense=payload.shipment.customs.extra.get('ExportLicense'), - ShipperEIN=payload.shipment.customs.extra.get('ShipperEIN'), - ShipperIDType=payload.shipment.customs.extra.get('ShipperIDType'), - ImportLicense=payload.shipment.customs.extra.get('ImportLicense'), - ConsigneeEIN=payload.shipment.customs.extra.get('ConsigneeEIN') - ) - - [ShipmentRequest_.add_SpecialService( - ShipReq.SpecialService(SpecialServiceType=service) - ) for service in payload.shipment.services] - - [ShipmentRequest_.add_Commodity( - ShipReq.Commodity(CommodityCode=c.code, CommodityName=c.description) - ) for c in payload.shipment.commodities] - - [ShipmentRequest_.add_Reference( - ShipReq.Reference(ReferenceID=r) - ) for r in payload.shipment.references] - - return ShipmentRequest_ - - def create_tracking_request(self, payload: E.tracking_request) -> Track.KnownTrackingRequest: - known_request = Track.KnownTrackingRequest( - Request=self.init_request(), - LanguageCode=payload.language_code or "en", - LevelOfDetails=payload.level_of_details or "ALL_CHECK_POINTS" - ) - for tn in payload.tracking_numbers: - known_request.add_AWBNumber(tn) - return known_request - - def create_pickup_request(self, payload: E.pickup_request) -> BookPURequest: - Requestor_, Place_, PickupContact_, Pickup_ = self._create_pickup_request(payload) - - return BookPURequest( - Request=self.init_request(), - schemaVersion="1.0", - RegionCode=payload.extra.get('RegionCode') or "AM", - Requestor=Requestor_, - Place=Place_, - PickupContact=PickupContact_, - Pickup=Pickup_ - ) - - def modify_pickup_request(self, payload: E.pickup_request) -> ModifyPURequest: - Requestor_, Place_, PickupContact_, Pickup_ = self._create_pickup_request(payload) - - return ModifyPURequest( - Request=self.init_request(), - schemaVersion="1.0", - RegionCode=payload.extra.get('RegionCode') or "AM", - ConfirmationNumber=payload.confirmation_number, - Requestor=Requestor_, - Place=Place_, - PickupContact=PickupContact_, - Pickup=Pickup_, - OriginSvcArea=payload.extra.get('OriginSvcArea') - ) - - def create_pickup_cancellation_request(self, payload: E.pickup_cancellation_request) -> CancelPURequest: - return CancelPURequest( - Request=self.init_request(), - schemaVersion="2.0", - RegionCode=payload.extra.get('RegionCode') or "AM", - ConfirmationNumber=payload.confirmation_number, - RequestorName=payload.person_name, - CountryCode=payload.country_code, - Reason=payload.extra.get('Reason') or "006", - PickupDate=payload.pickup_date, - CancelTime=time.strftime('%H:%M') - ) - - def parse_quote_response(self, response) -> Tuple[List[E.QuoteDetails], List[E.Error]]: - qtdshp_list = response.xpath( - './/*[local-name() = $name]', name="QtdShp") - quotes = reduce(self._extract_quote, qtdshp_list, []) - return (quotes, self.parse_error_response(response)) - - def parse_tracking_response(self, response) -> Tuple[List[E.TrackingDetails], List[E.Error]]: - awbinfos = response.xpath('.//*[local-name() = $name]', name="AWBInfo") - trackings = reduce(self._extract_tracking, awbinfos, []) - return (trackings, self.parse_error_response(response)) - - def parse_shipment_response(self, response) -> Tuple[E.ShipmentDetails, List[E.Error]]: - return (self._extract_shipment(response), self.parse_error_response(response)) - - def parse_pickup_response(self, response) -> Tuple[E.PickupDetails, List[E.Error]]: - ConfirmationNumbers = response.xpath('.//*[local-name() = $name]', name="ConfirmationNumber") - success = len(ConfirmationNumbers) > 0 - if success: - pickup = BookPUResponse() if 'BookPUResponse' in response.tag else ModifyPUResponse() - pickup.build(response) - return ( - self._extract_pickup(pickup) if success else None, - self.parse_error_response(response) if not success else [] - ) - - def parse_pickup_cancellation_response(self, response) -> Tuple[dict, List[E.Error]]: - ConfirmationNumbers = response.xpath('.//*[local-name() = $name]', name="ConfirmationNumber") - success = len(ConfirmationNumbers) > 0 - if success: - cancellation = dict( - confirmation_number=response.xpath('.//*[local-name() = $name]', name="ConfirmationNumber")[0].text - ) - return ( - cancellation if success else None, - self.parse_error_response(response) if not success else [] - ) - - """ Helpers functions """ - - def _extract_error(self, errors: List[E.Error], conditionNode) -> List[E.Error]: - condition = Res.ConditionType() - condition.build(conditionNode) - return errors + [ - E.Error(code=condition.ConditionCode, - message=condition.ConditionData, carrier=self.client.carrier_name) - ] - - def _extract_quote(self, quotes: List[E.QuoteDetails], qtdshpNode) -> List[E.QuoteDetails]: - qtdshp = Res.QtdShpType() - qtdshp.build(qtdshpNode) - ExtraCharges = list(map(lambda s: E.ChargeDetails( - name=s.LocalServiceTypeName, amount=float(s.ChargeValue or 0)), qtdshp.QtdShpExChrg)) - Discount_ = reduce( - lambda d, ec: d + ec.value if "Discount" in ec.name else d, ExtraCharges, 0) - DutiesAndTaxes_ = reduce( - lambda d, ec: d + ec.value if "TAXES PAID" in ec.name else d, ExtraCharges, 0) - return quotes + [ - E.QuoteDetails( - carrier=self.client.carrier_name, - currency=qtdshp.CurrencyCode, - delivery_date=str(qtdshp.DeliveryDate[0].DlvyDateTime), - pickup_date=str(qtdshp.PickupDate), - pickup_time=str(qtdshp.PickupCutoffTime), - service_name=qtdshp.LocalProductName, - service_type=qtdshp.NetworkTypeCode, - base_charge=float(qtdshp.WeightCharge or 0), - total_charge=float(qtdshp.ShippingCharge or 0), - duties_and_taxes=DutiesAndTaxes_, - discount=Discount_, - extra_charges=list(map(lambda s: E.ChargeDetails( - name=s.LocalServiceTypeName, amount=float(s.ChargeValue or 0)), qtdshp.QtdShpExChrg)) - ) - ] - - def _extract_tracking(self, trackings: List[E.TrackingDetails], awbInfoNode) -> List[E.TrackingDetails]: - awbInfo = TrackRes.AWBInfo() - awbInfo.build(awbInfoNode) - if awbInfo.ShipmentInfo == None: - return trackings - return trackings + [ - E.TrackingDetails( - carrier=self.client.carrier_name, - tracking_number=awbInfo.AWBNumber, - shipment_date=str(awbInfo.ShipmentInfo.ShipmentDate), - events=list(map(lambda e: E.TrackingEvent( - date=str(e.Date), - time=str(e.Time), - signatory=e.Signatory, - code=e.ServiceEvent.EventCode, - location=e.ServiceArea.Description, - description=e.ServiceEvent.Description - ), awbInfo.ShipmentInfo.ShipmentEvent)) - ) - ] - - def _extract_shipment(self, shipmentResponseNode) -> E.ShipmentDetails: - """ - Shipment extraction is implemented using lxml queries instead of generated ShipmentResponse type - because the type construction fail during validation out of our control - """ - get_value = lambda query: query[0].text if len(query) > 0 else None - get = lambda key: get_value(shipmentResponseNode.xpath("//%s" % key)) - tracking_number = get("AirwayBillNumber") - if tracking_number == None: - return None - plates = [p.text for p in shipmentResponseNode.xpath("//LicensePlateBarCode")] - barcodes = [child.text for child in shipmentResponseNode.xpath("//Barcodes")[0].getchildren()] - documents = reduce(lambda r,i: (r + [i] if i else r), [get("AWBBarCode")] + plates + barcodes, []) - reference = E.ReferenceDetails(value=get("ReferenceID"), type=get("ReferenceType")) if len(shipmentResponseNode.xpath("//Reference")) > 0 else None - currency_ = get("CurrencyCode") - return E.ShipmentDetails( - carrier=self.client.carrier_name, - tracking_numbers=[tracking_number], - shipment_date= get("ShipmentDate"), - services=( - [get("ProductShortName")] + - [service.text for service in shipmentResponseNode.xpath("//SpecialServiceDesc")] + - [service.text for service in shipmentResponseNode.xpath("//InternalServiceCode")] - ), - charges=[ - E.ChargeDetails(name="PackageCharge", amount=float(get("PackageCharge")), currency=currency_) - ], - documents=documents, - reference=reference, - total_charge= E.ChargeDetails(name="Shipment charge", amount=get("ShippingCharge"), currency=currency_) - ) - - def _extract_pickup(self, pickup: Union[BookPUResponse, ModifyPURequest]) -> E.PickupDetails: - pickup_charge = None if pickup.PickupCharge is None else E.ChargeDetails( - name="Pickup Charge", amount=pickup.PickupCharge, currency=pickup.CurrencyCode - ) - ref_times = ( - ([] if pickup.ReadyByTime is None else [E.TimeDetails(name="ReadyByTime", value=pickup.ReadyByTime)]) + - ([] if pickup.CallInTime is None else [E.TimeDetails(name="CallInTime", value=pickup.CallInTime)]) - ) - return E.PickupDetails( - carrier=self.client.carrier_name, - confirmation_number=pickup.ConfirmationNumber, - pickup_date=pickup.NextPickupDate, - pickup_charge=pickup_charge, - ref_times=ref_times - ) - - - """ Private functions """ - - def _create_pickup_request(self, payload: E.pickup_request) -> Tuple[ - PickpuDataTypes.Requestor, - PickpuDataTypes.Place, - PickpuDataTypes.Contact, - PickpuDataTypes.Pickup - ]: - RequestorContact_ = None if "RequestorContact" not in payload.extra else PickpuDataTypes.RequestorContact( - PersonName=payload.extra.get("RequestorContact").get("PersonName"), - Phone=payload.extra.get("RequestorContact").get("Phone"), - PhoneExtension=payload.extra.get("RequestorContact").get("PhoneExtension") - ) - - Requestor_ = PickpuDataTypes.Requestor( - AccountNumber=payload.account_number, - AccountType=payload.extra.get("AccountType") or "D", - RequestorContact=RequestorContact_, - CompanyName=payload.extra.get("CompanyName") - ) - - Place_ = PickpuDataTypes.Place( - City=payload.city, - StateCode=payload.state_code, - PostalCode=payload.postal_code, - CompanyName=payload.company_name, - CountryCode=payload.country_code, - PackageLocation=payload.package_location or "...", - LocationType="B" if payload.is_business else "R", - Address1=payload.address_lines[0] if len(payload.address_lines) > 0 else None, - Address2=payload.address_lines[1] if len(payload.address_lines) > 1 else None - ) - - PickupContact_ = PickpuDataTypes.Contact( - PersonName=payload.person_name, - Phone=payload.phone_number - ) - - weight_ = PickpuDataTypes.WeightSeg(Weight=payload.weight, WeightUnit=payload.weight_unit) if any([payload.weight, payload.weight_unit]) else None - - Pickup_ = PickpuDataTypes.Pickup( - Pieces=payload.pieces, - PickupDate=payload.date, - ReadyByTime=payload.ready_time, - CloseTime=payload.closing_time, - SpecialInstructions=payload.instruction, - RemotePickupFlag=payload.extra.get("RemotePickupFlag"), - weight=weight_ - ) - - return Requestor_, Place_, PickupContact_, Pickup_ \ No newline at end of file diff --git a/purplship/mappers/dhl/dhl_mapper/__init__.py b/purplship/mappers/dhl/dhl_mapper/__init__.py new file mode 100644 index 0000000000..47f2c1efbd --- /dev/null +++ b/purplship/mappers/dhl/dhl_mapper/__init__.py @@ -0,0 +1,62 @@ +from typing import Tuple, List, Union +from purplship.domain.mapper import Mapper +from purplship.domain import entities as E +from pydhl import ( + DCT_req_global as Req, + ship_val_global_req_61 as ShipReq, + DCT_Response_global as Res, + tracking_request_known as Track, + tracking_response as TrackRes, + book_pickup_global_req_20 as BookPUReq, + modify_pickup_global_req_20 as ModifPUReq, + cancel_pickup_global_req_20 as CancelPUReq +) +from .partials import ( + DHLQuoteMapperPartial, + DHLTrackMapperPartial, + DHLShipmentMapperPartial, + DHLPickupMapperPartial +) + + +class DHLMapper( + Mapper, + DHLQuoteMapperPartial, + DHLTrackMapperPartial, + DHLShipmentMapperPartial, + DHLPickupMapperPartial + ): + + def create_quote_request(self, payload: E.shipment_request) -> Req.DCTRequest: + return self.create_dct_request(payload) + + def create_tracking_request(self, payload: E.tracking_request) -> Track.KnownTrackingRequest: + return self.create_dhltracking_request(payload) + + def create_shipment_request(self, payload: E.shipment_request) -> ShipReq.ShipmentRequest: + return self.create_dhlshipment_request(payload) + + def create_pickup_request(self, payload: E.pickup_request) -> BookPUReq.BookPURequest: + return self.create_book_purequest(payload) + + def modify_pickup_request(self, payload: E.pickup_request) -> ModifPUReq.ModifyPURequest: + return self.create_modify_purequest(payload) + + def create_pickup_cancellation_request(self, payload: E.pickup_cancellation_request) -> CancelPUReq.CancelPURequest: + return self.create_cancel_purequest(payload) + + + def parse_quote_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + return self.parse_dct_response(response) + + def parse_tracking_response(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: + return self.parse_dhltracking_response(response) + + def parse_shipment_response(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + return self.parse_dhlshipment_respone(response) + + def parse_pickup_response(self, response) -> Tuple[E.PickupDetails, List[E.Error]]: + return self.parse_book_puresponse(response) + + def parse_pickup_cancellation_response(self, response) -> Tuple[dict, List[E.Error]]: + return self.parse_cancel_puresponse(response) \ No newline at end of file diff --git a/purplship/mappers/dhl/dhl_mapper/partials/__init__.py b/purplship/mappers/dhl/dhl_mapper/partials/__init__.py new file mode 100644 index 0000000000..3f5027774c --- /dev/null +++ b/purplship/mappers/dhl/dhl_mapper/partials/__init__.py @@ -0,0 +1,4 @@ +from .rate import DHLMapperPartial as DHLQuoteMapperPartial +from .track import DHLMapperPartial as DHLTrackMapperPartial +from .shipment import DHLMapperPartial as DHLShipmentMapperPartial +from .pickup import DHLMapperPartial as DHLPickupMapperPartial \ No newline at end of file diff --git a/purplship/mappers/dhl/dhl_mapper/partials/interface.py b/purplship/mappers/dhl/dhl_mapper/partials/interface.py new file mode 100644 index 0000000000..6a715dd979 --- /dev/null +++ b/purplship/mappers/dhl/dhl_mapper/partials/interface.py @@ -0,0 +1,92 @@ +import time +from typing import Tuple, List, Union +from functools import reduce +from purplship.mappers.dhl import DHLClient +from purplship.domain import entities as E +from pydhl.datatypes_global_v61 import ServiceHeader, MetaData, Request +from pydhl import ( + DCT_req_global as Req, + ship_val_global_req_61 as ShipReq, + DCT_Response_global as Res, + tracking_request_known as Track, + tracking_response as TrackRes, + book_pickup_global_req_20 as BookPUReq, + modify_pickup_global_req_20 as ModifPUReq, + cancel_pickup_global_req_20 as CancelPUReq, + book_pickup_global_res_20 as BookPURes, + modify_pickup_global_res_20 as ModifPURes, + pickupdatatypes_global_20 as PickpuDataTypes +) + + +class DHLCapabilities: + """ + DHL native service request types + """ + + """ Requests """ + + def create_dct_request(self, payload: E.shipment_request) -> Req.DCTRequest: + pass + + def create_dhltracking_request(self, payload: E.tracking_request) -> Track.KnownTrackingRequest: + pass + + def create_dhlshipment_request(self, payload: E.shipment_request) -> ShipReq.ShipmentRequest: + pass + + def create_book_purequest(self, payload: E.pickup_request) -> BookPUReq.BookPURequest: + pass + + def create_modify_purequest(self, payload: E.pickup_request) -> ModifPUReq.ModifyPURequest: + pass + + def create_cancel_purequest(self, payload: E.pickup_cancellation_request) -> CancelPUReq.CancelPURequest: + pass + + """ Response """ + + def parse_dct_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + pass + + def parse_dhltracking_response(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: + pass + + def parse_dhlshipment_respone(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + pass + + def parse_book_puresponse(self, response: 'XMLElement') -> Tuple[E.PickupDetails, List[E.Error]]: + pass + + def parse_cancel_puresponse(self, response: 'XMLElement') -> Tuple[dict, List[E.Error]]: + pass + + +class DHLMapperBase(DHLCapabilities): + """ + DHL mapper base class + """ + def __init__(self, client: DHLClient): + self.client = client + + def init_request(self) -> Request: + ServiceHeader_ = ServiceHeader( + MessageReference="1234567890123456789012345678901", + MessageTime=time.strftime('%Y-%m-%dT%H:%M:%S'), + SiteID=self.client.site_id, + Password=self.client.password + ) + return Request(ServiceHeader=ServiceHeader_) + + def parse_error_response(self, response) -> List[E.Error]: + conditions = response.xpath( + './/*[local-name() = $name]', name="Condition") + return reduce(self._extract_error, conditions, []) + + def _extract_error(self, errors: List[E.Error], conditionNode: 'XMLElement') -> List[E.Error]: + condition = Res.ConditionType() + condition.build(conditionNode) + return errors + [ + E.Error(code=condition.ConditionCode, + message=condition.ConditionData, carrier=self.client.carrier_name) + ] diff --git a/purplship/mappers/dhl/dhl_mapper/partials/pickup.py b/purplship/mappers/dhl/dhl_mapper/partials/pickup.py new file mode 100644 index 0000000000..42a3877528 --- /dev/null +++ b/purplship/mappers/dhl/dhl_mapper/partials/pickup.py @@ -0,0 +1,151 @@ +import time +from pydhl.book_pickup_global_req_20 import BookPURequest +from pydhl.modify_pickup_global_req_20 import ModifyPURequest +from pydhl.cancel_pickup_global_req_20 import CancelPURequest +from pydhl.book_pickup_global_res_20 import BookPUResponse +from pydhl.modify_pickup_global_res_20 import ModifyPUResponse +from pydhl import pickupdatatypes_global_20 as PickpuDataTypes + +from .interface import reduce, Union, Tuple, List, E, DHLMapperBase + + +class DHLMapperPartial(DHLMapperBase): + + + def parse_book_puresponse(self, response) -> Tuple[E.PickupDetails, List[E.Error]]: + ConfirmationNumbers = response.xpath('.//*[local-name() = $name]', name="ConfirmationNumber") + success = len(ConfirmationNumbers) > 0 + if success: + pickup = BookPUResponse() if 'BookPUResponse' in response.tag else ModifyPUResponse() + pickup.build(response) + return ( + self._extract_pickup(pickup) if success else None, + self.parse_error_response(response) if not success else [] + ) + + def parse_cancel_puresponse(self, response) -> Tuple[dict, List[E.Error]]: + ConfirmationNumbers = response.xpath('.//*[local-name() = $name]', name="ConfirmationNumber") + success = len(ConfirmationNumbers) > 0 + if success: + cancellation = dict( + confirmation_number=response.xpath('.//*[local-name() = $name]', name="ConfirmationNumber")[0].text + ) + return ( + cancellation if success else None, + self.parse_error_response(response) if not success else [] + ) + + + + def create_tracking_pins(self, payload: E.tracking_request) -> List[str]: + return payload.tracking_numbers + + def create_book_purequest(self, payload: E.pickup_request) -> BookPURequest: + Requestor_, Place_, PickupContact_, Pickup_ = self._create_pickup_request(payload) + + return BookPURequest( + Request=self.init_request(), + schemaVersion="1.0", + RegionCode=payload.extra.get('RegionCode') or "AM", + Requestor=Requestor_, + Place=Place_, + PickupContact=PickupContact_, + Pickup=Pickup_ + ) + + def create_modify_purequest(self, payload: E.pickup_request) -> ModifyPURequest: + Requestor_, Place_, PickupContact_, Pickup_ = self._create_pickup_request(payload) + + return ModifyPURequest( + Request=self.init_request(), + schemaVersion="1.0", + RegionCode=payload.extra.get('RegionCode') or "AM", + ConfirmationNumber=payload.confirmation_number, + Requestor=Requestor_, + Place=Place_, + PickupContact=PickupContact_, + Pickup=Pickup_, + OriginSvcArea=payload.extra.get('OriginSvcArea') + ) + + def create_cancel_purequest(self, payload: E.pickup_cancellation_request) -> CancelPURequest: + return CancelPURequest( + Request=self.init_request(), + schemaVersion="2.0", + RegionCode=payload.extra.get('RegionCode') or "AM", + ConfirmationNumber=payload.confirmation_number, + RequestorName=payload.person_name, + CountryCode=payload.country_code, + Reason=payload.extra.get('Reason') or "006", + PickupDate=payload.pickup_date, + CancelTime=time.strftime('%H:%M') + ) + + """ Private functions """ + + def _extract_pickup(self, pickup: Union[BookPUResponse, ModifyPURequest]) -> E.PickupDetails: + pickup_charge = None if pickup.PickupCharge is None else E.ChargeDetails( + name="Pickup Charge", amount=pickup.PickupCharge, currency=pickup.CurrencyCode + ) + ref_times = ( + ([] if pickup.ReadyByTime is None else [E.TimeDetails(name="ReadyByTime", value=pickup.ReadyByTime)]) + + ([] if pickup.CallInTime is None else [E.TimeDetails(name="CallInTime", value=pickup.CallInTime)]) + ) + return E.PickupDetails( + carrier=self.client.carrier_name, + confirmation_number=pickup.ConfirmationNumber, + pickup_date=pickup.NextPickupDate, + pickup_charge=pickup_charge, + ref_times=ref_times + ) + + def _create_pickup_request(self, payload: E.pickup_request) -> Tuple[ + PickpuDataTypes.Requestor, + PickpuDataTypes.Place, + PickpuDataTypes.Contact, + PickpuDataTypes.Pickup + ]: + RequestorContact_ = None if "RequestorContact" not in payload.extra else PickpuDataTypes.RequestorContact( + PersonName=payload.extra.get("RequestorContact").get("PersonName"), + Phone=payload.extra.get("RequestorContact").get("Phone"), + PhoneExtension=payload.extra.get("RequestorContact").get("PhoneExtension") + ) + + Requestor_ = PickpuDataTypes.Requestor( + AccountNumber=payload.account_number, + AccountType=payload.extra.get("AccountType") or "D", + RequestorContact=RequestorContact_, + CompanyName=payload.extra.get("CompanyName") + ) + + Place_ = PickpuDataTypes.Place( + City=payload.city, + StateCode=payload.state_code, + PostalCode=payload.postal_code, + CompanyName=payload.company_name, + CountryCode=payload.country_code, + PackageLocation=payload.package_location or "...", + LocationType="B" if payload.is_business else "R", + Address1=payload.address_lines[0] if len(payload.address_lines) > 0 else None, + Address2=payload.address_lines[1] if len(payload.address_lines) > 1 else None + ) + + PickupContact_ = PickpuDataTypes.Contact( + PersonName=payload.person_name, + Phone=payload.phone_number + ) + + weight_ = PickpuDataTypes.WeightSeg(Weight=payload.weight, WeightUnit=payload.weight_unit) if any([payload.weight, payload.weight_unit]) else None + + Pickup_ = PickpuDataTypes.Pickup( + Pieces=payload.pieces, + PickupDate=payload.date, + ReadyByTime=payload.ready_time, + CloseTime=payload.closing_time, + SpecialInstructions=payload.instruction, + RemotePickupFlag=payload.extra.get("RemotePickupFlag"), + weight=weight_ + ) + + return Requestor_, Place_, PickupContact_, Pickup_ + diff --git a/purplship/mappers/dhl/dhl_mapper/partials/rate.py b/purplship/mappers/dhl/dhl_mapper/partials/rate.py new file mode 100644 index 0000000000..ff82c570f9 --- /dev/null +++ b/purplship/mappers/dhl/dhl_mapper/partials/rate.py @@ -0,0 +1,142 @@ +import time +from pydhl.datatypes_global_v61 import MetaData +from pydhl import ( + DCT_req_global as Req, + DCT_Response_global as Res +) +from .interface import ( + reduce, Tuple, List, E, + DHLMapperBase +) + + +class DHLMapperPartial(DHLMapperBase): + + def parse_dct_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + qtdshp_list = response.xpath( + './/*[local-name() = $name]', name="QtdShp") + quotes = reduce(self._extract_quote, qtdshp_list, []) + return (quotes, self.parse_error_response(response)) + + def _extract_quote(self, quotes: List[E.QuoteDetails], qtdshpNode: 'XMLElement') -> List[E.QuoteDetails]: + qtdshp = Res.QtdShpType() + qtdshp.build(qtdshpNode) + ExtraCharges = list(map(lambda s: E.ChargeDetails( + name=s.LocalServiceTypeName, amount=float(s.ChargeValue or 0)), qtdshp.QtdShpExChrg)) + Discount_ = reduce( + lambda d, ec: d + ec.value if "Discount" in ec.name else d, ExtraCharges, 0) + DutiesAndTaxes_ = reduce( + lambda d, ec: d + ec.value if "TAXES PAID" in ec.name else d, ExtraCharges, 0) + return quotes + [ + E.QuoteDetails( + carrier=self.client.carrier_name, + currency=qtdshp.CurrencyCode, + delivery_date=str(qtdshp.DeliveryDate[0].DlvyDateTime), + pickup_date=str(qtdshp.PickupDate), + pickup_time=str(qtdshp.PickupCutoffTime), + service_name=qtdshp.LocalProductName, + service_type=qtdshp.NetworkTypeCode, + base_charge=float(qtdshp.WeightCharge or 0), + total_charge=float(qtdshp.ShippingCharge or 0), + duties_and_taxes=DutiesAndTaxes_, + discount=Discount_, + extra_charges=list(map(lambda s: E.ChargeDetails( + name=s.LocalServiceTypeName, amount=float(s.ChargeValue or 0)), qtdshp.QtdShpExChrg)) + ) + ] + + def create_dct_request(self, payload: E.shipment_request) -> Req.DCTRequest: + Request_ = self.init_request() + Request_.MetaData = MetaData(SoftwareName="3PV", SoftwareVersion="1.0") + + From_ = Req.DCTFrom( + CountryCode=payload.shipper.country_code, + Postalcode=payload.shipper.postal_code, + City=payload.shipper.city, + Suburb=payload.shipper.state_code + ) + + To_ = Req.DCTTo( + CountryCode=payload.recipient.country_code, + Postalcode=payload.recipient.postal_code, + City=payload.recipient.city, + Suburb=payload.recipient.state_code + ) + + Pieces = Req.PiecesType() + default_packaging_type = "FLY" if payload.shipment.is_document else "BOX" + for index, piece in enumerate(payload.shipment.packages): + Pieces.add_Piece(Req.PieceType( + PieceID=piece.id or str(index), + PackageTypeCode=piece.packaging_type or default_packaging_type, + Height=piece.height, Width=piece.width, + Weight=piece.weight, Depth=piece.length + )) + + payment_country_code = "CA" if not payload.shipment.payment_country_code else payload.shipment.payment_country_code + + BkgDetails_ = Req.BkgDetailsType( + PaymentCountryCode=payment_country_code, + NetworkTypeCode=payload.shipment.extra.get('NetworkTypeCode') or "AL", + WeightUnit=payload.shipment.weight_unit or "LB", + DimensionUnit=payload.shipment.dimension_unit or "IN", + ReadyTime=time.strftime("PT%HH%MM"), + Date=time.strftime("%Y-%m-%d"), + IsDutiable="N" if payload.shipment.is_document else "Y", + Pieces=Pieces, + NumberOfPieces=payload.shipment.number_of_packages, + ShipmentWeight=payload.shipment.total_weight, + Volume=payload.shipment.extra.get('Volume'), + PaymentAccountNumber=payload.shipment.payment_account_number or self.client.account_number, + InsuredCurrency=payload.shipment.currency or "USD", + InsuredValue=payload.shipment.insured_amount, + PaymentType=payload.shipment.extra.get('PaymentType'), + AcctPickupCloseTime=payload.shipment.extra.get('AcctPickupCloseTime'), + ) + + product_code = "P" if payload.shipment.is_document else "D" + BkgDetails_.add_QtdShp(Req.QtdShpType( + GlobalProductCode=product_code, + LocalProductCode=product_code + )) + + if payload.shipment.insured_amount is not None: + BkgDetails_.QtdShp[0].add_QtdShpExChrg( + Req.QtdShpExChrgType(SpecialServiceType="II") + ) + + if not payload.shipment.is_document: + BkgDetails_.QtdShp[0].add_QtdShpExChrg( + Req.QtdShpExChrgType(SpecialServiceType="DD") + ) + + GetQuote = Req.GetQuoteType( + Request=Request_, + From=From_, + To=To_, + BkgDetails=BkgDetails_, + Dutiable=Req.Dutiable( + DeclaredValue=payload.shipment.declared_value, + DeclaredCurrency=payload.shipment.currency, + ScheduleB=payload.shipment.extra.get('ScheduleB'), + ExportLicense=payload.shipment.extra.get('ExportLicense'), + ShipperEIN=payload.shipment.extra.get('ShipperEIN'), + ShipperIDType=payload.shipment.extra.get('ShipperIDType'), + ConsigneeIDType=payload.shipment.extra.get('ConsigneeIDType'), + ImportLicense=payload.shipment.extra.get('ImportLicense'), + ConsigneeEIN=payload.shipment.extra.get('ConsigneeEIN'), + TermsOfTrade=payload.shipment.extra.get('TermsOfTrade'), + CommerceLicensed=payload.shipment.extra.get('CommerceLicensed'), + Filing=(lambda filing: + Req.Filing( + FilingType=filing.get('FilingType'), + FTSR=filing.get('FTSR'), + ITN=filing.get('ITN'), + AES4EIN=filing.get('AES4EIN') + ) + )(payload.shipment.extra.get('Filing')) if 'Filing' in payload.shipment.extra else None + ) if payload.shipment.declared_value is not None else None + ) + + return Req.DCTRequest(schemaVersion="1.0", GetQuote=GetQuote) + diff --git a/purplship/mappers/dhl/dhl_mapper/partials/shipment.py b/purplship/mappers/dhl/dhl_mapper/partials/shipment.py new file mode 100644 index 0000000000..be8b78cc41 --- /dev/null +++ b/purplship/mappers/dhl/dhl_mapper/partials/shipment.py @@ -0,0 +1,199 @@ +import time +from base64 import b64decode +from pydhl.datatypes_global_v61 import MetaData +from pydhl import ( + ship_val_global_req_61 as ShipReq, + ship_val_global_res_61 as ShipRes +) +from .interface import ( + reduce, Tuple, List, Union, E, DHLMapperBase +) + + +class DHLMapperPartial(DHLMapperBase): + + def parse_dhlshipment_respone(self, response) -> Tuple[E.ShipmentDetails, List[E.Error]]: + return (self._extract_shipment(response), self.parse_error_response(response)) + + def _extract_shipment(self, shipmentResponseNode) -> E.ShipmentDetails: + """ + Shipment extraction is implemented using lxml queries instead of generated ShipmentResponse type + because the type construction fail during validation out of our control + """ + get_value = lambda query: query[0].text if len(query) > 0 else None + get = lambda key: get_value(shipmentResponseNode.xpath("//%s" % key)) + tracking_number = get("AirwayBillNumber") + if tracking_number == None: + return None + plates = [p.text for p in shipmentResponseNode.xpath("//LicensePlateBarCode")] + barcodes = [child.text for child in shipmentResponseNode.xpath("//Barcodes")[0].getchildren()] + documents = reduce(lambda r,i: (r + [i] if i else r), [get("AWBBarCode")] + plates + barcodes, []) + reference = E.ReferenceDetails(value=get("ReferenceID"), type=get("ReferenceType")) if len(shipmentResponseNode.xpath("//Reference")) > 0 else None + currency_ = get("CurrencyCode") + return E.ShipmentDetails( + carrier=self.client.carrier_name, + tracking_numbers=[tracking_number], + shipment_date= get("ShipmentDate"), + services=( + [get("ProductShortName")] + + [service.text for service in shipmentResponseNode.xpath("//SpecialServiceDesc")] + + [service.text for service in shipmentResponseNode.xpath("//InternalServiceCode")] + ), + charges=[ + E.ChargeDetails(name="PackageCharge", amount=float(get("PackageCharge")), currency=currency_) + ], + documents=documents, + reference=reference, + total_charge= E.ChargeDetails(name="Shipment charge", amount=get("ShippingCharge"), currency=currency_) + ) + + def create_dhlshipment_request(self, payload: E.shipment_request) -> ShipReq.ShipmentRequest: + Request_ = self.init_request() + Request_.MetaData = MetaData(SoftwareName="3PV", SoftwareVersion="1.0") + + Billing_ = ShipReq.Billing( + ShipperAccountNumber=payload.shipper.account_number or self.client.account_number, + BillingAccountNumber=payload.shipment.payment_account_number, + ShippingPaymentType=payload.shipment.paid_by, + DutyAccountNumber=payload.shipment.duty_payment_account, + DutyPaymentType=payload.shipment.duty_paid_by + ) + + Consignee_ = ShipReq.Consignee( + CompanyName=payload.recipient.company_name, + PostalCode=payload.recipient.postal_code, + CountryCode=payload.recipient.country_code, + City=payload.recipient.city, + CountryName=payload.recipient.country_name, + Division=payload.recipient.state, + DivisionCode=payload.recipient.state_code + ) + + if any([payload.recipient.person_name, payload.recipient.email_address]): + Consignee_.Contact = ShipReq.Contact( + PersonName=payload.recipient.person_name, + PhoneNumber=payload.recipient.phone_number, + Email=payload.recipient.email_address, + FaxNumber=payload.recipient.extra.get('FaxNumber'), + Telex=payload.recipient.extra.get('Telex'), + PhoneExtension=payload.recipient.extra.get('PhoneExtension'), + MobilePhoneNumber=payload.recipient.extra.get('MobilePhoneNumber') + ) + + [Consignee_.add_AddressLine(line) + for line in payload.recipient.address_lines] + + Shipper_ = ShipReq.Shipper( + ShipperID=payload.shipment.extra.get('ShipperID') or Billing_.ShipperAccountNumber, + RegisteredAccount=payload.shipment.extra.get('ShipperID') or Billing_.ShipperAccountNumber, + CompanyName=payload.shipper.company_name, + PostalCode=payload.shipper.postal_code, + CountryCode=payload.shipper.country_code, + City=payload.shipper.city, + CountryName=payload.shipper.country_name, + Division=payload.shipper.state, + DivisionCode=payload.shipper.state_code + ) + + if any([payload.shipper.person_name, payload.shipper.email_address]): + Shipper_.Contact = ShipReq.Contact( + PersonName=payload.shipper.person_name, + PhoneNumber=payload.shipper.phone_number, + Email=payload.shipper.email_address, + FaxNumber=payload.shipper.extra.get('FaxNumber'), + Telex=payload.shipper.extra.get('Telex'), + PhoneExtension=payload.shipper.extra.get('PhoneExtension'), + MobilePhoneNumber=payload.shipper.extra.get('MobilePhoneNumber') + ) + + [Shipper_.add_AddressLine(line) + for line in payload.shipper.address_lines] + + Pieces_ = ShipReq.Pieces() + for p in payload.shipment.packages: + Pieces_.add_Piece(ShipReq.Piece( + PieceID=p.id, + PackageType=p.packaging_type, + Weight=p.weight, + DimWeight=p.extra.get('DimWeight'), + Height=p.height, + Width=p.width, + Depth=p.length, + PieceContents=p.description + )) + + """ + Get PackageType from extra when implementing multi carrier, + Get weight from total_weight if specified otherwise calculated from packages weight sum + """ + ShipmentDetails_ = ShipReq.ShipmentDetails( + NumberOfPieces=len(payload.shipment.packages), + Pieces=Pieces_, + Weight=payload.shipment.total_weight or sum([p.weight for p in payload.shipment.packages]), + CurrencyCode=payload.shipment.currency or "USD", + WeightUnit=(payload.shipment.weight_unit or "LB")[0], + DimensionUnit=(payload.shipment.dimension_unit or "IN")[0], + Date=payload.shipment.date or time.strftime('%Y-%m-%d'), + PackageType=payload.shipment.packaging_type or payload.shipment.extra.get('PackageType'), + IsDutiable= "N" if payload.shipment.is_document else "Y", + InsuredAmount=payload.shipment.insured_amount, + DoorTo=payload.shipment.extra.get('DoorTo'), + GlobalProductCode=payload.shipment.extra.get('GlobalProductCode'), + LocalProductCode=payload.shipment.extra.get('LocalProductCode'), + Contents=payload.shipment.extra.get('Contents') or "..." + ) + + ShipmentRequest_ = ShipReq.ShipmentRequest( + schemaVersion="6.1", + Request=Request_, + RegionCode=payload.shipment.extra.get('RegionCode') or "AM", + RequestedPickupTime=payload.shipment.extra.get('RequestedPickupTime') or "Y", + LanguageCode=payload.shipment.extra.get('LanguageCode') or "en", + PiecesEnabled=payload.shipment.extra.get('PiecesEnabled') or "Y", + NewShipper=payload.shipment.extra.get('NewShipper'), + Billing=Billing_, + Consignee=Consignee_, + Shipper=Shipper_, + ShipmentDetails=ShipmentDetails_, + EProcShip=payload.shipment.extra.get('EProcShip') + ) + + if payload.shipment.label is not None: + DocImages_ = ShipReq.DocImages() + Image_ = None if 'Image' not in payload.shipment.label.extra else b64decode( + payload.shipment.label.extra.get('Image') + '=' * (-len(payload.shipment.label.extra.get('Image')) % 4) + ) + DocImages_.add_DocImage(ShipReq.DocImage( + Type=payload.shipment.label.type, + ImageFormat=payload.shipment.label.format, + Image=Image_ + )) + ShipmentRequest_.DocImages = DocImages_ + + if ShipmentDetails_.IsDutiable == "Y": + ShipmentRequest_.Dutiable = ShipReq.Dutiable( + DeclaredCurrency=ShipmentDetails_.CurrencyCode, + DeclaredValue=payload.shipment.declared_value, + TermsOfTrade=payload.shipment.customs.terms_of_trade, + ScheduleB=payload.shipment.customs.extra.get('ScheduleB'), + ExportLicense=payload.shipment.customs.extra.get('ExportLicense'), + ShipperEIN=payload.shipment.customs.extra.get('ShipperEIN'), + ShipperIDType=payload.shipment.customs.extra.get('ShipperIDType'), + ImportLicense=payload.shipment.customs.extra.get('ImportLicense'), + ConsigneeEIN=payload.shipment.customs.extra.get('ConsigneeEIN') + ) + + [ShipmentRequest_.add_SpecialService( + ShipReq.SpecialService(SpecialServiceType=service) + ) for service in payload.shipment.services] + + [ShipmentRequest_.add_Commodity( + ShipReq.Commodity(CommodityCode=c.code, CommodityName=c.description) + ) for c in payload.shipment.commodities] + + [ShipmentRequest_.add_Reference( + ShipReq.Reference(ReferenceID=r) + ) for r in payload.shipment.references] + + return ShipmentRequest_ + diff --git a/purplship/mappers/dhl/dhl_mapper/partials/track.py b/purplship/mappers/dhl/dhl_mapper/partials/track.py new file mode 100644 index 0000000000..ab5f7b1f8d --- /dev/null +++ b/purplship/mappers/dhl/dhl_mapper/partials/track.py @@ -0,0 +1,46 @@ +from pydhl import ( + tracking_request_known as Track, + tracking_response as TrackRes +) +from .interface import reduce, Tuple, List, E, DHLMapperBase + + +class DHLMapperPartial(DHLMapperBase): + + def parse_dhltracking_response(self, response) -> Tuple[List[E.TrackingDetails], List[E.Error]]: + awbinfos = response.xpath('.//*[local-name() = $name]', name="AWBInfo") + trackings = reduce(self._extract_tracking, awbinfos, []) + return (trackings, self.parse_error_response(response)) + + def _extract_tracking(self, trackings: List[E.TrackingDetails], awbInfoNode: 'XMLElement') -> List[E.TrackingDetails]: + awbInfo = TrackRes.AWBInfo() + awbInfo.build(awbInfoNode) + if awbInfo.ShipmentInfo == None: + return trackings + return trackings + [ + E.TrackingDetails( + carrier=self.client.carrier_name, + tracking_number=awbInfo.AWBNumber, + shipment_date=str(awbInfo.ShipmentInfo.ShipmentDate), + events=list(map(lambda e: E.TrackingEvent( + date=str(e.Date), + time=str(e.Time), + signatory=e.Signatory, + code=e.ServiceEvent.EventCode, + location=e.ServiceArea.Description, + description=e.ServiceEvent.Description + ), awbInfo.ShipmentInfo.ShipmentEvent)) + ) + ] + + def create_dhltracking_request(self, payload: E.tracking_request) -> Track.KnownTrackingRequest: + known_request = Track.KnownTrackingRequest( + Request=self.init_request(), + LanguageCode=payload.language_code or "en", + LevelOfDetails=payload.level_of_details or "ALL_CHECK_POINTS" + ) + for tn in payload.tracking_numbers: + known_request.add_AWBNumber(tn) + return known_request + + \ No newline at end of file diff --git a/purplship/mappers/dhl/dhl_proxy.py b/purplship/mappers/dhl/dhl_proxy.py index 8bb17ca801..7fd865a0a4 100644 --- a/purplship/mappers/dhl/dhl_proxy.py +++ b/purplship/mappers/dhl/dhl_proxy.py @@ -1,6 +1,7 @@ from io import StringIO from gds_helpers import export, to_xml, request as http -from purplship.mappers.dhl.dhl_mapper import DHLMapper, DHLClient +from purplship.mappers.dhl.dhl_mapper import DHLMapper +from purplship.mappers.dhl.dhl_client import DHLClient from purplship.domain.proxy import Proxy from pydhl.DCT_req_global import DCTRequest from pydhl.tracking_request_known import KnownTrackingRequest From 6f81d6b199d479d592655acdc6a671749f22baf0 Mon Sep 17 00:00:00 2001 From: Dan Kobina Date: Sat, 3 Nov 2018 01:06:52 -0400 Subject: [PATCH 5/6] refactor UPS mapper with partials --- purplship/mappers/ups/ups_mapper.py | 292 ------------------ purplship/mappers/ups/ups_mapper/__init__.py | 47 +++ .../ups/ups_mapper/partials/__init__.py | 3 + .../ups/ups_mapper/partials/interface.py | 68 ++++ .../mappers/ups/ups_mapper/partials/rate.py | 131 ++++++++ .../ups/ups_mapper/partials/shipment.py | 76 +++++ .../mappers/ups/ups_mapper/partials/track.py | 53 ++++ 7 files changed, 378 insertions(+), 292 deletions(-) delete mode 100644 purplship/mappers/ups/ups_mapper.py create mode 100644 purplship/mappers/ups/ups_mapper/__init__.py create mode 100644 purplship/mappers/ups/ups_mapper/partials/__init__.py create mode 100644 purplship/mappers/ups/ups_mapper/partials/interface.py create mode 100644 purplship/mappers/ups/ups_mapper/partials/rate.py create mode 100644 purplship/mappers/ups/ups_mapper/partials/shipment.py create mode 100644 purplship/mappers/ups/ups_mapper/partials/track.py diff --git a/purplship/mappers/ups/ups_mapper.py b/purplship/mappers/ups/ups_mapper.py deleted file mode 100644 index 65d1da05f1..0000000000 --- a/purplship/mappers/ups/ups_mapper.py +++ /dev/null @@ -1,292 +0,0 @@ -import time -from typing import List, Tuple -from functools import reduce -from purplship.mappers.ups.ups_client import UPSClient -from purplship.domain.mapper import Mapper -from purplship.domain import entities as E -from pyups import freight_rate as Rate, package_track as Track, UPSSecurity as Security, common as Common, error as Err -from pyups import freight_ship as FShip, package_ship as PShip - -class UPSMapper(Mapper): - def __init__(self, client: UPSClient): - self.client = client - - self.Security = Security.UPSSecurity( - UsernameToken=Security.UsernameTokenType( - Username=self.client.username, - Password=self.client.password - ), - ServiceAccessToken=Security.ServiceAccessTokenType( - AccessLicenseNumber=self.client.access_license_number - ) - ) - - - - def create_quote_request(self, payload: E.shipment_request) -> Rate.FreightRateRequest: - Request_ = Common.RequestType( - TransactionReference=Common.TransactionReferenceType( - TransactionIdentifier="TransactionIdentifier" - ) - ) - - Request_.add_RequestOption(1) - - ShipFrom_ = Rate.ShipFromType( - Name=payload.shipper.company_name, - Address=Rate.AddressType( - City=payload.shipper.city, - PostalCode=payload.shipper.postal_code, - CountryCode=payload.shipper.country_code - ), - AttentionName=payload.shipper.person_name, - ) - - for line in payload.shipper.address_lines: - ShipFrom_.Address.add_AddressLine(line) - - if len(payload.shipper.address_lines) == 0: - ShipFrom_.Address.add_AddressLine("...") - - ShipTo_ = Rate.ShipToType( - Name=payload.recipient.company_name, - Address=Rate.AddressType( - City=payload.recipient.city, - PostalCode=payload.recipient.postal_code, - CountryCode=payload.recipient.country_code - ), - AttentionName=payload.recipient.person_name, - ) - - for line in payload.recipient.address_lines: - ShipTo_.Address.add_AddressLine(line) - - PaymentInformation_ = Rate.PaymentInformationType( - Payer=Rate.PayerType( - Name=payload.shipment.payment_country_code or payload.shipper.country_code, - Address=ShipFrom_.Address, - ShipperNumber=payload.shipper.account_number or payload.shipment.payment_account_number or self.client.account_number - ), - ShipmentBillingOption=Rate.RateCodeDescriptionType( - Code=10 - ) - ) - - FreightRateRequest_ = Rate.FreightRateRequest( - Request=Request_, - ShipFrom=ShipFrom_, - ShipTo=ShipTo_, - PaymentInformation=PaymentInformation_, - Service=Rate.RateCodeDescriptionType(Code=309, Description="UPS Ground Freight"), - HandlingUnitOne=Rate.HandlingUnitType( - Quantity=1, Type=Rate.RateCodeDescriptionType(Code="SKD") - ), - ShipmentServiceOptions=Rate.ShipmentServiceOptionsType( - PickupOptions=Rate.PickupOptionsType(WeekendPickupIndicator="") - ), - DensityEligibleIndicator="", - AdjustedWeightIndicator="", - HandlingUnitWeight=Rate.HandlingUnitWeightType( - Value=1, - UnitOfMeasurement=Rate.UnitOfMeasurementType(Code="LB") - ), - PickupRequest=Rate.PickupRequestType(PickupDate=time.strftime('%Y%m%d')), - GFPOptions=Rate.OnCallInformationType(), - TimeInTransitIndicator="" - ) - - for c in payload.shipment.packages: - FreightRateRequest_.add_Commodity( - Rate.CommodityType( - Description=c.description or "...", - Weight=Rate.WeightType( - UnitOfMeasurement=Rate.UnitOfMeasurementType(Code="LBS"), - Value=c.weight - ), - Dimensions=Rate.DimensionsType( - UnitOfMeasurement=Rate.UnitOfMeasurementType(Code="IN"), - Width=c.width, - Height=c.height, - Length=c.length - ), - NumberOfPieces=len(payload.shipment.packages), - PackagingType=Rate.RateCodeDescriptionType(Code="BAG", Description="BAG"), - FreightClass=50 - ) - ) - - return FreightRateRequest_ - - - def create_tracking_request(self, payload: E.tracking_request) -> List[Track.TrackRequest]: - Request_ = Common.RequestType( - TransactionReference=Common.TransactionReferenceType( - TransactionIdentifier="TransactionIdentifier" - ) - ) - - Request_.add_RequestOption(1) - - TrackRequests_ = [Track.TrackRequest( - Request=Request_, - InquiryNumber=number - ) for number in payload.tracking_numbers] - - return TrackRequests_ - - - def parse_error_response(self, response) -> List[E.Error]: - notifications = response.xpath('.//*[local-name() = $name]', name="PrimaryErrorCode") - return reduce(self._extract_error, notifications, []) - - - def parse_quote_response(self, response) -> Tuple[List[E.QuoteDetails], List[E.Error]]: - rate_replys = response.xpath('.//*[local-name() = $name]', name="FreightRateResponse") - quotes = reduce(self._extract_quote, rate_replys, []) - return (quotes, self.parse_error_response(response)) - - - def parse_tracking_response(self, response) -> Tuple[List[E.QuoteDetails], List[E.Error]]: - track_details = response.xpath('.//*[local-name() = $name]', name="Shipment") - trackings = reduce(self._extract_tracking, track_details, []) - return (trackings, self.parse_error_response(response)) - - - def parse_shipment_response(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: - details = response.xpath('.//*[local-name() = $name]', name="FreightShipResponse") + response.xpath('.//*[local-name() = $name]', name="ShipmentResponse") - shipment = self._extract_shipment(details[0]) if len(details) > 0 else None - return (shipment, self.parse_error_response(response)) - - - - - def _extract_error(self, errors: List[E.Error], errorNode: 'XMLElement') -> List[E.Error]: - error = Err.CodeType() - error.build(errorNode) - return errors + [ - E.Error(code=error.Code, message=error.Description, carrier=self.client.carrier_name) - ] - - - def _extract_quote(self, quotes: List[E.QuoteDetails], detailNode: 'XMLElement') -> List[E.QuoteDetails]: - detail = Rate.FreightRateResponse() - detail.build(detailNode) - - total_charge = [r for r in detail.Rate if r.Type.Code == 'AFTR_DSCNT'][0] - Discounts_ = [E.ChargeDetails(name=r.Type.Code, currency=r.Factor.UnitOfMeasurement.Code, amount=float(r.Factor.Value)) for r in detail.Rate if r.Type.Code == 'DSCNT'] - Surcharges_ = [E.ChargeDetails(name=r.Type.Code, currency=r.Factor.UnitOfMeasurement.Code, amount=float(r.Factor.Value)) for r in detail.Rate if r.Type.Code not in ['DSCNT', 'AFTR_DSCNT', 'DSCNT_RATE', 'LND_GROSS']] - extra_charges = Discounts_ + Surcharges_ - return quotes + [ - E.QuoteDetails( - carrier=self.client.carrier_name, - currency=detail.TotalShipmentCharge.CurrencyCode, - service_name=detail.Service.Description, - service_type=detail.Service.Code, - base_charge=float(detail.TotalShipmentCharge.MonetaryValue), - total_charge=float(total_charge.Factor.Value or 0), - duties_and_taxes=reduce(lambda r, c: r + c.amount, Surcharges_, 0), - discount=reduce(lambda r, c: r + c.amount, Discounts_, 0), - extra_charges=extra_charges - ) - ] - - - def _extract_tracking(self, trackings: List[E.TrackingDetails], shipmentNode: 'XMLElement') -> List[E.TrackingDetails]: - trackDetail = Track.ShipmentType() - trackDetail.build(shipmentNode) - activityNodes = shipmentNode.xpath('.//*[local-name() = $name]', name="Activity") - def buildActivity(node): - activity = Track.ActivityType() - activity.build(node) - return activity - activities = map(buildActivity, activityNodes) - return trackings + [ - E.TrackingDetails( - carrier=self.client.carrier_name, - tracking_number=trackDetail.InquiryNumber.Value, - events=list(map(lambda a: E.TrackingEvent( - date=str(a.Date), - time=str(a.Time), - code=a.Status.Code if a.Status else None, - location=a.ActivityLocation.Address.City if a.ActivityLocation and a.ActivityLocation.Address else None, - description=a.Status.Description if a.Status else None - ), activities)) - ) - ] - - - def _extract_shipment(self, shipmentNode: 'XMLElement') -> E.ShipmentDetails: - is_freight = 'FreightShipResponse' in shipmentNode.tag - - return self._extract_freight_shipment(shipmentNode) if is_freight else self._extract_package_shipment(shipmentNode) - - - - - def _extract_freight_shipment(self, shipmentNode: 'XMLElement') -> E.ShipmentDetails: - shipmentResponse = FShip.FreightShipResponse() - shipmentResponse.build(shipmentNode) - shipment = shipmentResponse.ShipmentResults - - return E.ShipmentDetails( - carrier=self.client.carrier_name, - tracking_numbers=[shipment.ShipmentNumber], - total_charge=E.ChargeDetails( - name="Shipment charge", - amount=shipment.TotalShipmentCharge.MonetaryValue, - currency=shipment.TotalShipmentCharge.CurrencyCode - ), - charges=[ - E.ChargeDetails( - name=rate.Type.Code, - amount=rate.Factor.Value, - currency=rate.Factor.UnitOfMeasurement.Code - ) for rate in shipment.Rate - ], - # shipment_date=, - services=[shipment.Service.Code], - documents=[image.GraphicImage for image in (shipment.Documents or [])], - reference=E.ReferenceDetails( - value=shipmentResponse.Response.TransactionReference.CustomerContext, - type="CustomerContext" - ) - ) - - def _extract_package_shipment(self, shipmentNode: 'XMLElement') -> E.ShipmentDetails: - shipmentResponse = PShip.ShipmentResponse() - shipmentResponse.build(shipmentNode) - shipment = shipmentResponse.ShipmentResults - - if not shipment.NegotiatedRateCharges: - total_charge = shipment.ShipmentCharges.TotalChargesWithTaxes or shipment.ShipmentCharges.TotalCharges - else: - total_charge = shipment.NegotiatedRateCharges.TotalChargesWithTaxes or shipment.NegotiatedRateCharges.TotalCharge - - return E.ShipmentDetails( - carrier=self.client.carrier_name, - tracking_numbers=[pkg.TrackingNumber for pkg in shipment.PackageResults], - total_charge=E.ChargeDetails( - name="Shipment charge", - amount=total_charge.MonetaryValue, - currency=total_charge.CurrencyCode - ), - charges=[ - E.ChargeDetails( - name=charge.Code, - amount=charge.MonetaryValue, - currency=charge.CurrencyCode - ) for charge in [ - shipment.ShipmentCharges.TransportationCharges, - shipment.ShipmentCharges.ServiceOptionsCharges, - shipment.ShipmentCharges.BaseServiceCharge - ] if charge is not None - ], - documents=[ - pkg.ShippingLabel.GraphicImage for pkg in (shipment.PackageResults or []) - ], - reference=E.ReferenceDetails( - value=shipmentResponse.Response.TransactionReference.CustomerContext, - type="CustomerContext" - ) - ) - diff --git a/purplship/mappers/ups/ups_mapper/__init__.py b/purplship/mappers/ups/ups_mapper/__init__.py new file mode 100644 index 0000000000..14cd1f8955 --- /dev/null +++ b/purplship/mappers/ups/ups_mapper/__init__.py @@ -0,0 +1,47 @@ +from typing import Tuple, List, Union +from purplship.domain.mapper import Mapper +from purplship.domain import entities as E +from pyups import ( + freight_rate as Rate, + package_track as Track, + UPSSecurity as Security, + error as Err +) +from .partials import ( + UPSRateMapperPartial, + UPSTrackMapperPartial, + UPSShipmentMapperPartial +) + + +class UPSMapper( + Mapper, + UPSRateMapperPartial, + UPSTrackMapperPartial, + UPSShipmentMapperPartial + ): + + def create_quote_request(self, payload: E.shipment_request) -> Rate.FreightRateRequest: + return self.create_freight_rate_request(payload) + + def create_tracking_request(self, payload: E.tracking_request) -> List[Track.TrackRequest]: + return self.create_track_request(payload) + + + + def parse_quote_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + return self.parse_freight_rate_response(response) + + def parse_tracking_response(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: + return self.parse_track_response(response) + + def parse_shipment_response(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + details = response.xpath('.//*[local-name() = $name]', name="FreightShipResponse") + response.xpath('.//*[local-name() = $name]', name="ShipmentResponse") + if len(details) > 0: + shipmentNode = details[0] + is_freight = 'FreightShipResponse' in shipmentNode.tag + shipment = self.parse_freight_shipment_response(shipmentNode) if is_freight else self.parse_package_shipment_response(shipmentNode) + return ( + shipment if len(details) > 0 else None, + self.parse_error_response(response) + ) diff --git a/purplship/mappers/ups/ups_mapper/partials/__init__.py b/purplship/mappers/ups/ups_mapper/partials/__init__.py new file mode 100644 index 0000000000..ee3c32f330 --- /dev/null +++ b/purplship/mappers/ups/ups_mapper/partials/__init__.py @@ -0,0 +1,3 @@ +from .rate import UPSMapperPartial as UPSRateMapperPartial +from .track import UPSMapperPartial as UPSTrackMapperPartial +from .shipment import UPSMapperPartial as UPSShipmentMapperPartial \ No newline at end of file diff --git a/purplship/mappers/ups/ups_mapper/partials/interface.py b/purplship/mappers/ups/ups_mapper/partials/interface.py new file mode 100644 index 0000000000..1bb2ccd968 --- /dev/null +++ b/purplship/mappers/ups/ups_mapper/partials/interface.py @@ -0,0 +1,68 @@ +from typing import Tuple, List, Union +from functools import reduce +from purplship.mappers.ups import UPSClient +from purplship.domain import entities as E +from pyups import ( + freight_rate as Rate, + package_track as Track, + UPSSecurity as Security, + error as Err +) + + +class UPSCapabilities: + """ + UPS native service request types + """ + + """ Requests """ + + def create_freight_rate_request(self, payload: E.shipment_request) -> Rate.FreightRateRequest: + pass + + def create_track_request(self, payload: E.tracking_request) -> List[Track.TrackRequest]: + pass + + + """ Replys """ + + def parse_freight_rate_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + pass + + def parse_track_response(self, response: 'XMLElement') -> Tuple[List[E.TrackingDetails], List[E.Error]]: + pass + + def parse_freight_shipment_response(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + pass + + def parse_package_shipment_response(self, response: 'XMLElement') -> Tuple[E.ShipmentDetails, List[E.Error]]: + pass + + +class UPSMapperBase(UPSCapabilities): + """ + UPS mapper base class + """ + def __init__(self, client: UPSClient): + self.client = client + + self.Security = Security.UPSSecurity( + UsernameToken=Security.UsernameTokenType( + Username=self.client.username, + Password=self.client.password + ), + ServiceAccessToken=Security.ServiceAccessTokenType( + AccessLicenseNumber=self.client.access_license_number + ) + ) + + def parse_error_response(self, response) -> List[E.Error]: + notifications = response.xpath('.//*[local-name() = $name]', name="PrimaryErrorCode") + return reduce(self._extract_error, notifications, []) + + def _extract_error(self, errors: List[E.Error], errorNode: 'XMLElement') -> List[E.Error]: + error = Err.CodeType() + error.build(errorNode) + return errors + [ + E.Error(code=error.Code, message=error.Description, carrier=self.client.carrier_name) + ] \ No newline at end of file diff --git a/purplship/mappers/ups/ups_mapper/partials/rate.py b/purplship/mappers/ups/ups_mapper/partials/rate.py new file mode 100644 index 0000000000..4bb88f0391 --- /dev/null +++ b/purplship/mappers/ups/ups_mapper/partials/rate.py @@ -0,0 +1,131 @@ +import time +from pyups import ( + freight_rate as Rate, + common as Common +) +from .interface import reduce, Tuple, List, E, UPSMapperBase + + +class UPSMapperPartial(UPSMapperBase): + + def parse_freight_rate_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + rate_replys = response.xpath('.//*[local-name() = $name]', name="FreightRateResponse") + quotes = reduce(self._extract_quote, rate_replys, []) + return (quotes, self.parse_error_response(response)) + + def _extract_quote(self, quotes: List[E.QuoteDetails], detailNode: 'XMLElement') -> List[E.QuoteDetails]: + detail = Rate.FreightRateResponse() + detail.build(detailNode) + + total_charge = [r for r in detail.Rate if r.Type.Code == 'AFTR_DSCNT'][0] + Discounts_ = [E.ChargeDetails(name=r.Type.Code, currency=r.Factor.UnitOfMeasurement.Code, amount=float(r.Factor.Value)) for r in detail.Rate if r.Type.Code == 'DSCNT'] + Surcharges_ = [E.ChargeDetails(name=r.Type.Code, currency=r.Factor.UnitOfMeasurement.Code, amount=float(r.Factor.Value)) for r in detail.Rate if r.Type.Code not in ['DSCNT', 'AFTR_DSCNT', 'DSCNT_RATE', 'LND_GROSS']] + extra_charges = Discounts_ + Surcharges_ + return quotes + [ + E.QuoteDetails( + carrier=self.client.carrier_name, + currency=detail.TotalShipmentCharge.CurrencyCode, + service_name=detail.Service.Description, + service_type=detail.Service.Code, + base_charge=float(detail.TotalShipmentCharge.MonetaryValue), + total_charge=float(total_charge.Factor.Value or 0), + duties_and_taxes=reduce(lambda r, c: r + c.amount, Surcharges_, 0), + discount=reduce(lambda r, c: r + c.amount, Discounts_, 0), + extra_charges=extra_charges + ) + ] + + def create_freight_rate_request(self, payload: E.shipment_request) -> Rate.FreightRateRequest: + Request_ = Common.RequestType( + TransactionReference=Common.TransactionReferenceType( + TransactionIdentifier="TransactionIdentifier" + ) + ) + + Request_.add_RequestOption(1) + + ShipFrom_ = Rate.ShipFromType( + Name=payload.shipper.company_name, + Address=Rate.AddressType( + City=payload.shipper.city, + PostalCode=payload.shipper.postal_code, + CountryCode=payload.shipper.country_code + ), + AttentionName=payload.shipper.person_name, + ) + + for line in payload.shipper.address_lines: + ShipFrom_.Address.add_AddressLine(line) + + if len(payload.shipper.address_lines) == 0: + ShipFrom_.Address.add_AddressLine("...") + + ShipTo_ = Rate.ShipToType( + Name=payload.recipient.company_name, + Address=Rate.AddressType( + City=payload.recipient.city, + PostalCode=payload.recipient.postal_code, + CountryCode=payload.recipient.country_code + ), + AttentionName=payload.recipient.person_name, + ) + + for line in payload.recipient.address_lines: + ShipTo_.Address.add_AddressLine(line) + + PaymentInformation_ = Rate.PaymentInformationType( + Payer=Rate.PayerType( + Name=payload.shipment.payment_country_code or payload.shipper.country_code, + Address=ShipFrom_.Address, + ShipperNumber=payload.shipper.account_number or payload.shipment.payment_account_number or self.client.account_number + ), + ShipmentBillingOption=Rate.RateCodeDescriptionType( + Code=10 + ) + ) + + FreightRateRequest_ = Rate.FreightRateRequest( + Request=Request_, + ShipFrom=ShipFrom_, + ShipTo=ShipTo_, + PaymentInformation=PaymentInformation_, + Service=Rate.RateCodeDescriptionType(Code=309, Description="UPS Ground Freight"), + HandlingUnitOne=Rate.HandlingUnitType( + Quantity=1, Type=Rate.RateCodeDescriptionType(Code="SKD") + ), + ShipmentServiceOptions=Rate.ShipmentServiceOptionsType( + PickupOptions=Rate.PickupOptionsType(WeekendPickupIndicator="") + ), + DensityEligibleIndicator="", + AdjustedWeightIndicator="", + HandlingUnitWeight=Rate.HandlingUnitWeightType( + Value=1, + UnitOfMeasurement=Rate.UnitOfMeasurementType(Code="LB") + ), + PickupRequest=Rate.PickupRequestType(PickupDate=time.strftime('%Y%m%d')), + GFPOptions=Rate.OnCallInformationType(), + TimeInTransitIndicator="" + ) + + for c in payload.shipment.packages: + FreightRateRequest_.add_Commodity( + Rate.CommodityType( + Description=c.description or "...", + Weight=Rate.WeightType( + UnitOfMeasurement=Rate.UnitOfMeasurementType(Code="LBS"), + Value=c.weight + ), + Dimensions=Rate.DimensionsType( + UnitOfMeasurement=Rate.UnitOfMeasurementType(Code="IN"), + Width=c.width, + Height=c.height, + Length=c.length + ), + NumberOfPieces=len(payload.shipment.packages), + PackagingType=Rate.RateCodeDescriptionType(Code="BAG", Description="BAG"), + FreightClass=50 + ) + ) + + return FreightRateRequest_ + diff --git a/purplship/mappers/ups/ups_mapper/partials/shipment.py b/purplship/mappers/ups/ups_mapper/partials/shipment.py new file mode 100644 index 0000000000..54f3e0e6c6 --- /dev/null +++ b/purplship/mappers/ups/ups_mapper/partials/shipment.py @@ -0,0 +1,76 @@ +from pyups import ( + freight_ship as FShip, + package_ship as PShip +) +from .interface import reduce, Tuple, List, Union, E, UPSMapperBase + + +class UPSMapperPartial(UPSMapperBase): + + def parse_freight_shipment_response(self, shipmentNode: 'XMLElement') -> E.ShipmentDetails: + shipmentResponse = FShip.FreightShipResponse() + shipmentResponse.build(shipmentNode) + shipment = shipmentResponse.ShipmentResults + + return E.ShipmentDetails( + carrier=self.client.carrier_name, + tracking_numbers=[shipment.ShipmentNumber], + total_charge=E.ChargeDetails( + name="Shipment charge", + amount=shipment.TotalShipmentCharge.MonetaryValue, + currency=shipment.TotalShipmentCharge.CurrencyCode + ), + charges=[ + E.ChargeDetails( + name=rate.Type.Code, + amount=rate.Factor.Value, + currency=rate.Factor.UnitOfMeasurement.Code + ) for rate in shipment.Rate + ], + # shipment_date=, + services=[shipment.Service.Code], + documents=[image.GraphicImage for image in (shipment.Documents or [])], + reference=E.ReferenceDetails( + value=shipmentResponse.Response.TransactionReference.CustomerContext, + type="CustomerContext" + ) + ) + + def parse_package_shipment_response(self, shipmentNode: 'XMLElement') -> E.ShipmentDetails: + shipmentResponse = PShip.ShipmentResponse() + shipmentResponse.build(shipmentNode) + shipment = shipmentResponse.ShipmentResults + + if not shipment.NegotiatedRateCharges: + total_charge = shipment.ShipmentCharges.TotalChargesWithTaxes or shipment.ShipmentCharges.TotalCharges + else: + total_charge = shipment.NegotiatedRateCharges.TotalChargesWithTaxes or shipment.NegotiatedRateCharges.TotalCharge + + return E.ShipmentDetails( + carrier=self.client.carrier_name, + tracking_numbers=[pkg.TrackingNumber for pkg in shipment.PackageResults], + total_charge=E.ChargeDetails( + name="Shipment charge", + amount=total_charge.MonetaryValue, + currency=total_charge.CurrencyCode + ), + charges=[ + E.ChargeDetails( + name=charge.Code, + amount=charge.MonetaryValue, + currency=charge.CurrencyCode + ) for charge in [ + shipment.ShipmentCharges.TransportationCharges, + shipment.ShipmentCharges.ServiceOptionsCharges, + shipment.ShipmentCharges.BaseServiceCharge + ] if charge is not None + ], + documents=[ + pkg.ShippingLabel.GraphicImage for pkg in (shipment.PackageResults or []) + ], + reference=E.ReferenceDetails( + value=shipmentResponse.Response.TransactionReference.CustomerContext, + type="CustomerContext" + ) + ) + diff --git a/purplship/mappers/ups/ups_mapper/partials/track.py b/purplship/mappers/ups/ups_mapper/partials/track.py new file mode 100644 index 0000000000..61151107f1 --- /dev/null +++ b/purplship/mappers/ups/ups_mapper/partials/track.py @@ -0,0 +1,53 @@ +from pyups import ( + package_track as Track, + common as Common +) +from .interface import reduce, Tuple, List, E, UPSMapperBase + + +class UPSMapperPartial(UPSMapperBase): + + def parse_track_response(self, response: 'XMLElement') -> Tuple[List[E.QuoteDetails], List[E.Error]]: + track_details = response.xpath('.//*[local-name() = $name]', name="Shipment") + trackings = reduce(self._extract_tracking, track_details, []) + return (trackings, self.parse_error_response(response)) + + def _extract_tracking(self, trackings: List[E.TrackingDetails], shipmentNode: 'XMLElement') -> List[E.TrackingDetails]: + trackDetail = Track.ShipmentType() + trackDetail.build(shipmentNode) + activityNodes = shipmentNode.xpath('.//*[local-name() = $name]', name="Activity") + def buildActivity(node): + activity = Track.ActivityType() + activity.build(node) + return activity + activities = map(buildActivity, activityNodes) + return trackings + [ + E.TrackingDetails( + carrier=self.client.carrier_name, + tracking_number=trackDetail.InquiryNumber.Value, + events=list(map(lambda a: E.TrackingEvent( + date=str(a.Date), + time=str(a.Time), + code=a.Status.Code if a.Status else None, + location=a.ActivityLocation.Address.City if a.ActivityLocation and a.ActivityLocation.Address else None, + description=a.Status.Description if a.Status else None + ), activities)) + ) + ] + + def create_track_request(self, payload: E.shipment_request) -> List[Track.TrackRequest]: + Request_ = Common.RequestType( + TransactionReference=Common.TransactionReferenceType( + TransactionIdentifier="TransactionIdentifier" + ) + ) + + Request_.add_RequestOption(1) + + TrackRequests_ = [Track.TrackRequest( + Request=Request_, + InquiryNumber=number + ) for number in payload.tracking_numbers] + + return TrackRequests_ + From 19a4452ba9c4cbe650bd0ff48993dc7b0173305e Mon Sep 17 00:00:00 2001 From: Dan Kobina Date: Sat, 3 Nov 2018 01:13:44 -0400 Subject: [PATCH 6/6] update version for release candidate 4 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2074628eff..060346b346 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ long_description = fh.read() setup(name='purplship', - version='1.0-rc3', - description='Shipping carriers integration with python', + version='1.0-rc4', + description='Multi-carrier shipping API integration with python', long_description=long_description, long_description_content_type="text/markdown", url='https://github.com/PurplShip/purplship',