diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..efb50c5
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,14 @@
+language: python
+python:
+- "2.7"
+script:
+- echo "1"
+deploy:
+ provider: pypi
+ user: hparfr
+ password:
+ secure: yyroV8BdvIaQZSbDf/6zKXmu0uL4u5SdyxCPLTJU+qyTlhtS/IJAJZK+W00zu6rOimHUFSgkhi0s21kuck9m4gYuDZnl9ghSuTCkmlra1l4pY565XkIutu1ArGBViQb5A6EkWf8oX9JPXY2fnYr4R/7PMQbDEyY/Ft9yG5zLsug5DtmUJgYU3M9r3lweN8Jv9TFRXjBEFwhbtgpe+/0mWHAMyDQmZE/+CocSf2F7LtILPaqIGvVVXJg3mItV1vCzGi0FbHolIWRnSpLCSun/G2ldQcNUrtKnZY+2vPFfnOGLOwHn8iKqY7Rxid7cI8vYFE65tWkJZ+g6gKI8Rk0JG0IBE1hUl86Smy3+g84Q5SfVDiCXRFQwpAx77SPsfrUD7uYzFQAf+n43inF1X/r26hdZnhr0FjWVUVvtJIVeUGOtO1Dp1NFIpLmCvXHDT8nV6qva7+yIMU2+6HPI2Sy41xMpDAKPJWCCq7rdKd4t0sPnpj2PFdDQ1nY8ZuQV1iQfuqv82Vz89fFCW5S5OHoN01EC6zThAjfRFkAefPtGP7QCZnaAGMcsLhDDG9J/S5Np4sx33hrFuvGtrv/P6zQCPOS7iBn3UA0xEzZORUEImVKL5eFJtQdS7Firl2HYYvPkv7SsDHUvONjk0Drv3Gsk4AtB7Wxx26bXgoqLDrdt0Pk=
+ distributions: sdist bdist_wheel
+ on:
+ tags: true
+ branch: master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57a4ddd..0afb9be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,15 +1,31 @@
-# 0.1.1
Roadmap / TODO:
- Add Kuehne & Nagel carrier
+ - Add Geodis EDI
- Add UPS carrier
- Support test_mode for some carriers
- Improve documentation
- Support additionnal methods of api
- - Support carrier tracking
- Write tests
+# 0.1.1 2017-03-20
+
+
+### Features / Refactorings
+ - Refactoring of error handling: exception are now always raised, returns code (success | warning) removed
+ - Simplification of return code
+ - Remove duplicate code of get_parts
+ - Handling of additionnal attachments ("annexes")
+ - tracking code is always in tracking.number
+ - label is always in label.data
+
+### BREAKING CHANGES
+ - errors are all raised with exceptions
+ - raise Exception removed in favor of better defined exceptions
+ - return of get_label harmonized
+
+
# 0.1.0 2016-12-19
### BREAKING CHANGES
diff --git a/README.md b/README.md
index 31f929d..67f4993 100755
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Roulier will get a label + tracking number to your carrier for you.
* Roulier runs on your server and call each carrier API directly.
* You have to use your own credentials provided by each carriers.
* Roulier is Open Source software, AGPL-3
-* Roulier integrate a multitude of carriers : Laposte, Geodis, DPD, K&N... more to come.
+* Roulier integrate a multitude of carriers : Laposte, Geodis, DPD, K&N, TRS... more to come.
@@ -74,7 +74,38 @@ pprint(laposte.api())
```
-Advanced usage for Laposte
+### Return
+
+```python
+from roulier import roulier
+laposte = roulier.get('laposte')
+api = laposte.api()
+api['auth']['login'] = '12345'
+...
+print laposte.get_label(api)
+
+# {
+# label: {
+# 'name':'label',
+# 'type': 'zpl',
+# 'data': 'base64 here',
+# },
+# tracking: {
+# 'number': 'tracking code here',
+# },
+# annexes: [
+# {
+# 'name': 'cn23',
+# 'type': 'pdf',
+# 'data': 'base64 here'
+# }, ...
+# ],
+#}
+```
+
+
+
+### Advanced usage for Laposte
Usefull for debugging: get the xml before the call, send an xml directly, analyse the response
@@ -138,7 +169,7 @@ l_api._parcel()
```
-###Contributors
+### Contributors
* [@hparfr](https://github.com/hparfr) ([Akretion.com](https://akretion.com))
@@ -153,3 +184,5 @@ l_api._parcel()
* [Jinja2](http://jinja.pocoo.org/) - templating
* [Requests](http://docs.python-requests.org/) - HTTP requests
* [zplgrf](https://github.com/kylemacfarlane/zplgrf) - PNG to ZPL conversion
+* [unidecode](https://pypi.python.org/pypi/Unidecode) - Remove accents from ZPL
+* [unicodecsv](https://github.com/jdunck/python-unicodecsv) - CSV generation
\ No newline at end of file
diff --git a/VERSION b/VERSION
index 6c6aa7c..6da28dd 100755
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.0
\ No newline at end of file
+0.1.1
\ No newline at end of file
diff --git a/roulier/api.py b/roulier/api.py
index f25e4e7..f2225da 100644
--- a/roulier/api.py
+++ b/roulier/api.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""API interface."""
from cerberus import Validator
+from unidecode import unidecode
class MyValidator(Validator):
@@ -14,6 +15,26 @@ def _validate_description(self, description, field, value):
"""
pass
+ def _normalize_coerce_zpl(self, value):
+ """Sanitze input for ZPL.
+
+ Remove ZPL ctrl caraters
+ Remove accents
+ """
+ if not isinstance(value, basestring):
+ return value
+
+ ctrl_cars = [
+ 0xFE, # Tilde ~
+ 0x5E, # Caret ^
+ 0x1E, # RS (^ substitution)
+ 0x10, # DLE (~ substitution)
+ ]
+ val = unidecode(value)
+ for ctrl in ctrl_cars:
+ val = val.replace("%c" % ctrl, "")
+ return val
+
class Api(object):
"""Define expected fields of carriers.
diff --git a/roulier/carriers/__init__.py b/roulier/carriers/__init__.py
index b3e4e60..d5a3e6f 100755
--- a/roulier/carriers/__init__.py
+++ b/roulier/carriers/__init__.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from . import laposte
+from . import dummy
from . import geodis
from . import dummy
from . import dpd
+from . import trs
diff --git a/roulier/carriers/dpd/dpd.py b/roulier/carriers/dpd/dpd.py
index 95570db..d797f9f 100755
--- a/roulier/carriers/dpd/dpd.py
+++ b/roulier/carriers/dpd/dpd.py
@@ -4,9 +4,7 @@
from .dpd_decoder import DpdDecoder
from .dpd_encoder import DpdEncoder
from .dpd_transport import DpdTransport
-from .dpd_api import DpdApi
from roulier.carrier import Carrier
-from roulier import ws_tools as tools
class Dpd(Carrier):
@@ -24,33 +22,10 @@ def get(self, data, action):
"""Run an action with data against Dpd WS."""
request = self.encoder.encode(data, action)
response = self.ws.send(request)
- if not response['payload']:
- return response
- payload = self.decoder.decode(response, {})
- zpl = self.handle_zpl(data, payload['label'])
- payload['label'] = zpl if zpl else payload['label']
- return payload
+ return self.decoder.decode(
+ response['body'], request['output_format'])
# shortcuts
def get_label(self, data):
"""Genereate a createShipmentWithLabels."""
return self.get(data, 'createShipmentWithLabels')
-
- # utils
- def handle_zpl(self, data, png):
- """Convert a png in zpl.
-
- if labelFormat was asked as ZPL, WS returns a png
- This function rotate it and convert it an suitable zpl format
- @params:
- data : full dict with all the params of the get method
- png : a base64 formatted string (returned by ws)
- @returns:
- a zpl in a string
-
- """
- label_format = DpdApi().normalize(data)['service']['labelFormat']
-
- if label_format == 'ZPL':
- return tools.png_to_zpl(png, True)
- return None
diff --git a/roulier/carriers/dpd/dpd_api.py b/roulier/carriers/dpd/dpd_api.py
index 8f413ab..b993915 100644
--- a/roulier/carriers/dpd/dpd_api.py
+++ b/roulier/carriers/dpd/dpd_api.py
@@ -12,7 +12,13 @@
'No',
'Predict',
'AutomaticSMS',
- 'AutomaticEmail',
+ 'AutomaticMail',
+)
+DPD_PRODUCTS = (
+ 'DPD_Classic',
+ 'DPD_Relais',
+ 'DPD_Predict',
+ # 'DPD Retour en Relais', # Not implemented yet
)
@@ -22,16 +28,35 @@ def _service(self):
schema['labelFormat']['allowed'] = DPD_LABEL_FORMAT
schema['labelFormat']['default'] = 'ZPL'
schema['labelFormat'].update({'required': True, 'empty': False})
- schema['agencyId'].update({'required': True, 'empty': False, 'description': 'Agency code int(3)'})
- schema['customerCountry'] = {'required': True, 'empty': False, 'description': 'Customer country code (France = 250) int(3)'}
- schema['customerId'].update({'required': True, 'empty': False, 'description': 'Customer number int(6)'})
+ schema['agencyId'].update({
+ 'required': True, 'empty': False,
+ 'description': 'Agency code int(3)'})
+ schema['customerCountry'] = {
+ 'required': True, 'empty': False,
+ 'description': 'Customer country code (France = 250) int(3)'}
+ schema['customerId'].update({
+ 'required': True, 'empty': False,
+ 'description': 'Customer number int(6)'})
schema['shippingDate'].update({'required': False, 'empty': True})
# mettre ça ensemble ?
- schema['notifications'] = {'default': 'Predict', 'allowed': DPD_ALLOWED_NOTIFICATIONS}
- schema['product'].update({'description': 'N/A for DPD'})
+ schema['notifications'] = {
+ 'default': DPD_ALLOWED_NOTIFICATIONS[0],
+ 'allowed': DPD_ALLOWED_NOTIFICATIONS}
+ schema['product'].update({
+ 'empty': False,
+ 'required': True,
+ 'default': DPD_PRODUCTS[0],
+ 'description': 'Type de produit',
+ 'allowed': DPD_PRODUCTS
+ })
- schema['dropOffLocation'] = {'default': '', 'description': 'Drop-off Location id (Relais Colis)' }
+ schema['dropOffLocation'] = {
+ 'default': '',
+ 'empty': True,
+ 'required': False,
+ 'description': 'Drop-off Location id (Relais Colis)'
+ }
return schema
diff --git a/roulier/carriers/dpd/dpd_decoder.py b/roulier/carriers/dpd/dpd_decoder.py
index da4caef..d9e7ef7 100755
--- a/roulier/carriers/dpd/dpd_decoder.py
+++ b/roulier/carriers/dpd/dpd_decoder.py
@@ -2,17 +2,13 @@
"""Dpd XML -> Python."""
from lxml import objectify
from roulier.codec import Decoder
+from roulier import ws_tools as tools
class DpdDecoder(Decoder):
"""Dpd XML -> Python."""
- def decode(self, response, parts):
- payload_xml = response['payload']
- tag, content = self.decode_payload(payload_xml)
- return content
-
- def decode_payload(self, xml_string):
+ def decode(self, body, output_format):
"""Dpd XML -> Python."""
def create_shipment_with_labels(msg):
"""Understand a CreateShipmentWithLabelsResponse."""
@@ -21,19 +17,41 @@ def create_shipment_with_labels(msg):
)
shipment = shipments.getchildren()[0]
label, attachment = labels.getchildren()
+ label_data = self.handle_zpl(label.label.text, output_format)
# .text because we want str instead of objectify.StringElement
x = {
- 'barcode': shipment.barcode.text,
- 'parcelnumber': shipment.parcelnumber.text,
- 'label': label.label.text,
- 'attachment': attachment.label.text
+ "tracking": {
+ 'number': shipment.barcode.text,
+ 'parcelnumber': shipment.parcelnumber.text,
+ },
+ "label": {
+ "data": label_data,
+ "name": "label",
+ "type": output_format,
+ },
+ "annexes": [{
+ "data": attachment.label.text,
+ "name": "Summary",
+ "type": output_format
+ }]
}
return x
- xml = objectify.fromstring(xml_string)
+ xml = objectify.fromstring(body)
tag = xml.tag
lookup = {
"{http://www.cargonet.software}CreateShipmentWithLabelsResponse":
create_shipment_with_labels,
}
- return tag, lookup[tag](xml)
+ return lookup[tag](xml)
+
+ def handle_zpl(self, png, label_format):
+ """Convert a png in zpl.
+
+ if labelFormat was asked as ZPL, WS returns a png
+ This function rotate it and convert it an suitable zpl format
+ """
+ if label_format == 'ZPL':
+ return tools.png_to_zpl(png, True)
+ else:
+ return png
diff --git a/roulier/carriers/dpd/dpd_encoder.py b/roulier/carriers/dpd/dpd_encoder.py
index c8935bf..a1d1c02 100755
--- a/roulier/carriers/dpd/dpd_encoder.py
+++ b/roulier/carriers/dpd/dpd_encoder.py
@@ -4,8 +4,11 @@
from roulier.codec import Encoder
from datetime import datetime
from .dpd_api import DpdApi
+from roulier.exception import InvalidApiInput
+import logging
DPD_ACTIONS = ('createShipmentWithLabels')
+log = logging.getLogger(__name__)
class DpdEncoder(Encoder):
@@ -14,21 +17,50 @@ class DpdEncoder(Encoder):
def encode(self, api_input, action):
"""Transform input to dpd compatible xml."""
if not (action in DPD_ACTIONS):
- raise Exception(
+ raise InvalidApiInput(
'action %s not in %s' % (action, ', '.join(DPD_ACTIONS)))
api = DpdApi()
if not api.validate(api_input):
- raise Exception(
+ raise InvalidApiInput(
'Input error : %s' % api.errors(api_input))
data = api.normalize(api_input)
+ # add some rules which are hard to implement with
+ # cerberus.
+ # TODO: add additional schemas for that
+ if data['service']['product'] == 'DPD_Predict':
+ if len(data['service']['dropOffLocation']) > 0:
+ raise InvalidApiInput(
+ "dropOffLocation can't be used with predict")
+ if data['service']['notifications'] != 'Predict':
+ log.info(
+ 'Notification forced to predict because of product')
+ data['service']['notifications'] = 'Predict'
+
+ if data['service']['product'] == 'DPD_Classic':
+ if len(data['service']['dropOffLocation']) > 0:
+ raise InvalidApiInput(
+ "dropOffLocation can't be used with classic")
+ if data['service']['notifications'] == 'Predict':
+ raise InvalidApiInput(
+ "Predict notifications can't be used with classic")
+
+ if data['service']['product'] == 'DPD_Relais':
+ if len(data['service']['dropOffLocation']) < 1:
+ raise InvalidApiInput(
+ "dropOffLocation is mandatory for this product")
+ if data['service']['notifications'] == 'Predict':
+ raise InvalidApiInput(
+ "Predict notifications can't be used with Relais")
+
data['service']['shippingDate'] = (
datetime
.strptime(data['service']['shippingDate'], '%Y/%M/%d')
.strftime('%d/%M/%Y')
)
+ output_format = data['service']['labelFormat']
if data['service']['labelFormat'] in ('PNG', 'ZPL'):
# WS doesn't handle zpl yet, we convert it later
# png is named Default, WTF DPD?
@@ -45,7 +77,8 @@ def encode(self, api_input, action):
parcel=data['parcel'],
sender_address=data['from_address'],
receiver_address=data['to_address']),
- "headers": data['auth']
+ "headers": data['auth'],
+ "output_format": output_format
}
def api(self):
diff --git a/roulier/carriers/dpd/dpd_transport.py b/roulier/carriers/dpd/dpd_transport.py
index 634b1eb..e794891 100755
--- a/roulier/carriers/dpd/dpd_transport.py
+++ b/roulier/carriers/dpd/dpd_transport.py
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
"""Implement dpdWS."""
import requests
-import email.parser
from lxml import objectify, etree
from jinja2 import Environment, PackageLoader
from roulier.transport import Transport
from roulier.ws_tools import remove_empty_tags
+from roulier.exception import CarrierError
+
import logging
log = logging.getLogger(__name__)
@@ -15,8 +16,6 @@ class DpdTransport(Transport):
"""Implement Dpd WS communication."""
DPD_WS = "https://e-station.cargonet.software/dpd-eprintwebservice/eprintwebservice.asmx"
- STATUS_SUCCES = "success"
- STATUS_ERROR = "error"
def send(self, payload):
"""Call this function.
@@ -26,11 +25,8 @@ def send(self, payload):
payload.header : auth
Return:
{
- status: STATUS_SUCCES or STATUS_ERROR, (string)
- message: more info about status of result (lxml)
response: (Requests.response)
- payload: usefull payload (if success) (xml as string)
-
+ body: XML response (without soap)
}
"""
body = payload['body']
@@ -65,21 +61,14 @@ def handle_500(self, response):
"""Handle reponse in case of ERROR 500 type."""
log.warning('Dpd error 500')
obj = objectify.fromstring(response.content)
- return {
+ errors = [{
"id": obj.xpath('//faultcode')[0],
- "status": self.STATUS_ERROR,
"message": obj.xpath('//faultstring')[0],
- "response": response,
- "payload": None
- }
+ }]
+ raise CarrierError(response, errors)
def handle_200(self, response):
- """
- Handle response type 200 (success).
-
- It still can be a success or a failure.
- """
-
+ """Handle response type 200 (success)."""
def extract_soap(response_xml):
obj = objectify.fromstring(response_xml)
return obj.Body.getchildren()[0]
@@ -87,22 +76,18 @@ def extract_soap(response_xml):
body = extract_soap(response.content)
body_xml = etree.tostring(body)
return {
- "status": "ok",
- "message": "",
- "payload": body_xml,
+ "body": body_xml,
"response": response,
}
def handle_response(self, response):
"""Handle response of webservice."""
- if response.status_code == 500:
- return self.handle_500(response)
- elif response.status_code == 200:
+ if response.status_code == 200:
return self.handle_200(response)
+ elif response.status_code == 500:
+ return self.handle_500(response)
else:
- return {
- "status": "error",
- "message": "Unexpected status code from server",
- "response": response
- }
-
+ raise CarrierError(response, [{
+ 'id': None,
+ 'message': "Unexpected status code from server",
+ }])
diff --git a/roulier/carriers/dpd/templates/dpd_createShipmentWithLabels.xml b/roulier/carriers/dpd/templates/dpd_createShipmentWithLabels.xml
index 4f1702d..61349fa 100644
--- a/roulier/carriers/dpd/templates/dpd_createShipmentWithLabels.xml
+++ b/roulier/carriers/dpd/templates/dpd_createShipmentWithLabels.xml
@@ -13,12 +13,16 @@
{% include "dpd_address.xml" %}
+ {% endwith %}
+
+ {% with address = receiver_address %}
{{ address.phone }}
{{ address.email }}
{{ service.notifications }}
+ {% endwith %}
{% if service.dropOffLocation %}
@@ -27,7 +31,6 @@
{% endif %}
- {% endwith %}
{{ service.labelFormat }}
diff --git a/roulier/carriers/geodis/geodis.py b/roulier/carriers/geodis/geodis.py
index ed3ba5f..a9568b1 100755
--- a/roulier/carriers/geodis/geodis.py
+++ b/roulier/carriers/geodis/geodis.py
@@ -21,10 +21,10 @@ def get(self, data, action):
"""Run an action with data against Geodis WS."""
request = self.encoder.encode(data, action)
response = self.ws.send(request)
- if response['status'] == 'error':
- return response
- parts = response['attachement']
- return self.decoder.decode(response, parts)
+ return self.decoder.decode(
+ response['body'],
+ response['parts'],
+ request['output_format'])
# shortcuts
def get_label(self, data):
diff --git a/roulier/carriers/geodis/geodis_api.py b/roulier/carriers/geodis/geodis_api.py
index 7dd9252..1d06abb 100644
--- a/roulier/carriers/geodis/geodis_api.py
+++ b/roulier/carriers/geodis/geodis_api.py
@@ -18,9 +18,13 @@ def _service(self):
schema['labelFormat']['default'] = 'ZPL'
schema['labelFormat'].update({'required': True, 'empty': False})
schema['product'].update({'required': True, 'empty': False})
- schema['agencyId'].update({'required': True, 'empty': False})
+ schema['agencyId'].update({'required': False, 'empty': True})
schema['customerId'].update({'required': True, 'empty': False})
schema['shippingId'].update({'required': True, 'empty': False})
+ schema['hubId'] = {
+ 'description': 'TEOS : code agence Hub de sortie',
+ 'default': ''
+ }
schema['is_test'] = {
'type': 'boolean', 'default': True,
'description': 'Use test WS'}
diff --git a/roulier/carriers/geodis/geodis_decoder.py b/roulier/carriers/geodis/geodis_decoder.py
index 74c40b0..35c58b2 100755
--- a/roulier/carriers/geodis/geodis_decoder.py
+++ b/roulier/carriers/geodis/geodis_decoder.py
@@ -1,32 +1,43 @@
# -*- coding: utf-8 -*-
"""Geodis XML -> Python."""
-from lxml import objectify
from roulier.codec import Decoder
class GeodisDecoder(Decoder):
"""Geodis XML -> Python."""
- def decode(self, response, parts):
- payload_xml = response['payload']
- tag, content = self.decode_payload(payload_xml, parts)
-
- return content
-
- def decode_payload(self, payload, parts):
+ def decode(self, body, parts, output_format):
"""Geodis XML -> Python."""
def reponse_impression_etiquette(msg, parts):
x = {
- 'barcode': payload.infoColis.cab,
- 'cab': payload.cabRouting,
- 'label': parts,
+ "tracking": {
+ "barcode": body.cabRouting,
+ },
+ "label": {
+ "name": "label",
+ "data": parts,
+ "type": output_format
+ },
+ "annexes": [
+ ],
+ "extra": {
+ "reseau": body.reseau,
+ "priorite": body.priorite,
+ "codeDirectionel": body.codire,
+ "cabRouting": body.cabRouting,
+ "colis": {
+ "codeUmg": body.infoColis.codumg,
+ "numero": body.infoColis.numero,
+ "cab": body.infoColis.cab,
+ }
+ }
}
return x
- tag = payload.tag
+ tag = body.tag
lookup = {
"{http://impression.service.web.etiquette.geodis.com}reponseImpressionEtiquette":
reponse_impression_etiquette,
}
- return tag, lookup[tag](payload, parts)
+ return lookup[tag](body, parts)
diff --git a/roulier/carriers/geodis/geodis_encoder.py b/roulier/carriers/geodis/geodis_encoder.py
index e4626dd..441ca3e 100755
--- a/roulier/carriers/geodis/geodis_encoder.py
+++ b/roulier/carriers/geodis/geodis_encoder.py
@@ -14,7 +14,7 @@ class GeodisEncoder(Encoder):
def encode(self, api_input, action):
"""Transform input to geodis compatible xml."""
if not (action in GEODIS_ACTIONS):
- raise Exception(
+ raise InvalidApiInput(
'action %s not in %s' % (action, ', '.join(GEODIS_ACTIONS)))
api = GeodisApi()
@@ -42,7 +42,8 @@ def encode(self, api_input, action):
sender_address=data['from_address'],
receiver_address=data['to_address']),
"headers": data['auth'],
- "is_test": is_test
+ "is_test": is_test,
+ "output_format": data['service']['labelFormat']
}
def api(self):
diff --git a/roulier/carriers/geodis/geodis_transport.py b/roulier/carriers/geodis/geodis_transport.py
index 5fe9ee2..25b5815 100755
--- a/roulier/carriers/geodis/geodis_transport.py
+++ b/roulier/carriers/geodis/geodis_transport.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
"""Implement geodisWS."""
import requests
-import email.parser
from lxml import objectify
from jinja2 import Environment, PackageLoader
from roulier.transport import Transport
-from roulier.ws_tools import remove_empty_tags
+from roulier.ws_tools import remove_empty_tags, get_parts
+from roulier.exception import CarrierError
import logging
log = logging.getLogger(__name__)
@@ -16,8 +16,6 @@ class GeodisTransport(Transport):
GEODIS_WS = "http://espace.geodis.com/geolabel/services/ImpressionEtiquette" # nopep8
GEODIS_WS_TEST = "http://espace.recette.geodis.com/geolabel/services/ImpressionEtiquette" # nopep8
- STATUS_SUCCES = "success"
- STATUS_ERROR = "error"
def send(self, payload):
"""Call this function.
@@ -27,18 +25,15 @@ def send(self, payload):
payload.header : auth
Return:
{
- status: STATUS_SUCCES or STATUS_ERROR, (string)
- message: more info about status of result (lxml)
response: (Requests.response)
- payload: usefull payload (if success) (xml as string)
-
+ body: XML response (without soap)
+ parts: dict of attachments
}
"""
body = payload['body']
headers = payload['headers']
is_test = payload['is_test']
soap_message = self.soap_wrap(body, headers)
- log.debug(soap_message)
response = self.send_request(soap_message, is_test)
log.info('WS response time %s' % response.elapsed.total_seconds())
return self.handle_response(response)
@@ -72,29 +67,21 @@ def handle_500(self, response):
# TODO : put a try catch (like wrong server)
# no need to extract_body shit here
log.warning('Geodis error 500')
- xml = self.get_parts(response)['start']
+ xml = get_parts(response)['start']
obj = objectify.fromstring(xml)
message = obj.xpath("//*[local-name() = 'message']")
if len(message) > 0:
message = message[0] or obj.xpath('//faultstring')[0]
id_message = obj.xpath("//*[local-name() = 'code']")[0]
- return {
- "id": obj.xpath('//faultcode')[0],
- "status": self.STATUS_ERROR,
- "messages": [{ # only one error msg is returned by ws
- 'id': id_message,
- 'message': message,
- }],
- "response": response,
- }
+ errors = [{
+ "id": id_message,
+ "message": message,
+ }]
+ raise CarrierError(response, errors)
def handle_200(self, response):
- """
- Handle response type 200 (success).
-
- It still can be a success or a failure.
- """
- parts = self.get_parts(response)
+ """Handle response type 200."""
+ parts = get_parts(response)
xml = parts['start']
def extract_soap(response_xml):
@@ -102,15 +89,12 @@ def extract_soap(response_xml):
return obj.Body.getchildren()[0]
payload = extract_soap(xml)
- attachement_id = payload.codeAttachement.text[4:] # remove cid:
- attachement = parts[attachement_id]
- # payload.infoColis.cab
+ attachement_cid = payload.codeAttachement.text[len('cid:'):]
+ attachement = parts[attachement_cid]
return {
- "status": "ok",
- "message": "",
- "payload": payload,
- "attachement": attachement,
+ "body": payload,
+ "parts": attachement,
"response": response,
}
@@ -121,33 +105,7 @@ def handle_response(self, response):
elif response.status_code == 200:
return self.handle_200(response)
else:
- return {
- "status": "error",
- "messages": [{
- 'id': False,
- 'message': "Unexpected status code from server",
- }],
- "response": response
- }
-
- def get_parts(self, response):
- head_lines = ''
- for k, v in response.raw.getheaders().iteritems():
- head_lines += str(k)+':'+str(v)+'\n'
-
- full = head_lines + response.content
-
- parser = email.parser.Parser()
- decoded_reply = parser.parsestr(full)
- parts = {}
- start = decoded_reply.get_param('start').lstrip('<').rstrip('>')
- i = 0
- for part in decoded_reply.get_payload():
- cid = part.get('content-Id', '').lstrip('<').rstrip('>')
- if (not start or start == cid) and 'start' not in parts:
- parts['start'] = part.get_payload()
- else:
- parts[cid or 'Attachment%d' % i] = part.get_payload()
- i += 1
-
- return parts
+ raise CarrierError(response, [{
+ 'id': None,
+ 'message': "Unexpected status code from server",
+ }])
diff --git a/roulier/carriers/geodis/templates/geodis_demandeImpressionEtiquette.xml b/roulier/carriers/geodis/templates/geodis_demandeImpressionEtiquette.xml
index 548fae6..e351ee4 100644
--- a/roulier/carriers/geodis/templates/geodis_demandeImpressionEtiquette.xml
+++ b/roulier/carriers/geodis/templates/geodis_demandeImpressionEtiquette.xml
@@ -1,5 +1,6 @@
{{ service.agencyId }}
+ {{ service.hubId }}
{{ service.customerId }}
{{ service.labelFormat }}
1
@@ -25,5 +26,5 @@
{% endwith %}
1
{{ parcel.weight }}
- {{ service.reference }}
+ {{ service.reference1 }}
\ No newline at end of file
diff --git a/roulier/carriers/laposte/laposte.py b/roulier/carriers/laposte/laposte.py
index 3d0b909..feeb561 100755
--- a/roulier/carriers/laposte/laposte.py
+++ b/roulier/carriers/laposte/laposte.py
@@ -22,15 +22,11 @@ def get(self, data, action):
"""Run an action with data against Laposte WS."""
request = self.encoder.encode(data, action)
response = self.ws.send(request)
- if response.get('message') and response['message']['exception']:
- return {
- 'status': 'error',
- 'messages': self.ws.exception_handling(
- response['message']['message']),
- 'response': response['response'],
- }
- parts = self.ws.get_parts(response['response'])
- return self.decoder.decode(response, parts)
+ return self.decoder.decode(
+ response['body'],
+ response['parts'],
+ request['output_format']
+ )
# shortcuts
def get_label(self, data):
diff --git a/roulier/carriers/laposte/laposte_api.py b/roulier/carriers/laposte/laposte_api.py
index b2d3fd9..c6b9e67 100644
--- a/roulier/carriers/laposte/laposte_api.py
+++ b/roulier/carriers/laposte/laposte_api.py
@@ -45,6 +45,12 @@ def _service(self):
'default': '', 'description':
"""Utilisé pour le Colissimo Retour uniquement. "
"Définit le mode de transmission de l’étiquette"""}
+ schema['reference1'].update({
+ 'description': """Référence expediteur ('Réf client')"""
+ })
+ schema['reference2'].update({
+ 'description': """Référence destinataire ('Réf destinataire')"""
+ })
return schema
def _address(self):
@@ -53,15 +59,18 @@ def _address(self):
schema['zip'].update({'required': True, 'empty': False})
schema['city'].update({'required': True, 'empty': False})
schema['street0'] = {
- 'default': '', 'description': 'Etage, couloir, escalier, appart.'}
+ 'required': False, 'empty': True,
+ 'description': """Entrée, bâtiment, immeuble, résidence. """
+ """Non utilisé pour la Belgique."""}
+ schema['street2'].update({
+ 'default': '', 'description': 'Etage, couloir, escalier, appart.'})
schema['street1'].update({
'required': True, 'empty': False,
'description': 'Numéro et libellé de voie. Ex : 5 rue du Bellay'})
- schema['street2'].update({
- 'required': False, 'empty': True,
- 'description': 'Entrée, bâtiment, immeuble, résidence'})
schema['street3'] = {
- 'default': '', 'description': """Lieu dit ou autre mention"""}
+ 'default': '',
+ 'description': """Lieu dit ou autre mention. """
+ """Non utilisé pour la Belgique."""}
schema['door1'] = {'default': '', 'description': """Code porte 1"""}
schema['door2'] = {'default': '', 'description': """Code porte 2"""}
schema['intercom'] = {'default': '', 'description': """Interphone"""}
@@ -77,12 +86,6 @@ def _to_address(self):
schema['firstName'] = {
'default': '',
'description': """Prénom. Obligatoire pour So Colissimo"""}
- schema['street2'].update(
- {'required': True, 'description': """Numéro et libellé de voie. "
- "Ex : « 5 rue du Bellay »"""})
- schema['street1'].update(
- {'required': False, 'empty': True,
- 'description': 'Entrée, bâtiment, immeuble, résidence'})
return schema
def _parcel(self):
diff --git a/roulier/carriers/laposte/laposte_decoder.py b/roulier/carriers/laposte/laposte_decoder.py
index bbdf863..6e6f5ad 100755
--- a/roulier/carriers/laposte/laposte_decoder.py
+++ b/roulier/carriers/laposte/laposte_decoder.py
@@ -7,27 +7,7 @@
class LaposteDecoder(Decoder):
"""Laposte XML -> Python."""
- def decode(self, response, parts):
- payload_xml = response['payload']
- tag, content = self.decode_payload(payload_xml)
- if tag.endswith('getProductInterResponse'):
- return content
- else:
- # tag is generateLabelResponse
- label_cid = content.get('label').getchildren()[0].attrib['href']
- if content.get('cn23'):
- cn23_cid = content.get('cn23').getchildren()[0].attrib['href']
- else:
- cn23_cid = False
- content['cn23'] = False
-
- content['label'] = parts[label_cid.replace('cid:', '')]
- if cn23_cid:
- content['cn23'] = parts[cn23_cid.replace('cid:', '')]
-
- return content
-
- def decode_payload(self, xml_string):
+ def decode(self, body, parts, output_format):
"""Laposte XML -> Python."""
def get_product_inter(msg):
"""Understand a getProductInterResponse."""
@@ -39,16 +19,48 @@ def get_product_inter(msg):
def generate_label_response(msg):
"""Understand a generateLabelResponse."""
- x = {
- "parcelNumber": msg.labelResponse.parcelNumber,
- "parcelNumberPartner": msg.labelResponse.find(
- 'parcelNumberPartner'),
- "cn23": msg.labelResponse.find('cn23'),
- "label": msg.labelResponse.find('label'),
+ def get_cid(tag, tree):
+ element = tree.find(tag)
+ if element is None:
+ return None
+ href = element.getchildren()[0].attrib['href']
+ # href contains cid:236212...-38932@cfx.apache.org
+ return href[len('cid:'):] # remove prefix
+
+ rep = msg.labelResponse
+ cn23_cid = get_cid('cn23', rep)
+ label_cid = get_cid('label', rep)
+
+ annexes = []
+
+ if cn23_cid:
+ annexes.append({
+ "name": 'cn23',
+ "data": parts.get(cn23_cid),
+ "type": "pdf"
+ })
+
+ if rep.find('pdfUrl'):
+ annexes.append({
+ "name": "label",
+ "data": rep.find('pdfUrl'),
+ "type": "url"
+ })
+
+ return {
+ "tracking": {
+ "number": rep.parcelNumber,
+ "partner": rep.find('parcelNumberPartner'),
+ },
+ "label": {
+ "name": "label",
+ "data": parts.get(label_cid),
+ "type": output_format
+ },
+ "annexes": annexes
}
- return x
- xml = objectify.fromstring(xml_string)
+ xml = objectify.fromstring(body)
tag = xml.tag
lookup = {
"{http://sls.ws.coliposte.fr}getProductInterResponse":
@@ -56,4 +68,4 @@ def generate_label_response(msg):
"{http://sls.ws.coliposte.fr}generateLabelResponse":
generate_label_response
}
- return tag, lookup[tag](xml.xpath('//return')[0])
+ return lookup[tag](xml.xpath('//return')[0])
diff --git a/roulier/carriers/laposte/laposte_encoder.py b/roulier/carriers/laposte/laposte_encoder.py
index 5d20c7d..c9af5ec 100755
--- a/roulier/carriers/laposte/laposte_encoder.py
+++ b/roulier/carriers/laposte/laposte_encoder.py
@@ -15,7 +15,7 @@ class LaposteEncoder(Encoder):
def encode(self, api_input, action):
"""Transform input to laposte compatible xml."""
if not (action in LAPOSTE_ACTIONS):
- raise Exception(
+ raise InvalidApiInput(
'action %s not in %s' % (action, ', '.join(LAPOSTE_ACTIONS)))
api = LaposteApi()
@@ -42,7 +42,8 @@ def encode(self, api_input, action):
sender_address=data['from_address'],
receiver_address=data['to_address'],
customs=data['customs']),
- "headers": data['auth']
+ "headers": data['auth'],
+ "output_format": data['service']['labelFormat']
}
def api(self):
diff --git a/roulier/carriers/laposte/laposte_transport.py b/roulier/carriers/laposte/laposte_transport.py
index 75c8d86..11d5aec 100755
--- a/roulier/carriers/laposte/laposte_transport.py
+++ b/roulier/carriers/laposte/laposte_transport.py
@@ -5,7 +5,8 @@
from lxml import objectify, etree
from jinja2 import Environment, PackageLoader
from roulier.transport import Transport
-from roulier.ws_tools import remove_empty_tags
+from roulier.ws_tools import remove_empty_tags, get_parts
+from roulier.exception import CarrierError
import logging
log = logging.getLogger(__name__)
@@ -15,22 +16,18 @@ class LaposteTransport(Transport):
"""Implement Laposte WS communication."""
LAPOSTE_WS = "https://ws.colissimo.fr/sls-ws/SlsServiceWS"
- STATUS_SUCCES = "success"
- STATUS_ERROR = "error"
def send(self, payload):
"""Call this function.
Args:
payload.body: XML in a string
- payload.headers: auth
+ payload.header : auth
Return:
{
- status: STATUS_SUCCES or STATUS_ERROR, (string)
- message: more info about status of result (lxml)
response: (Requests.response)
- payload: usefull payload (if success) (xml as string)
-
+ body: XML response (without soap)
+ parts: dict of attachments
}
"""
body = payload['body']
@@ -38,7 +35,6 @@ def send(self, payload):
soap_message = self.soap_wrap(body, headers)
log.debug(soap_message)
response = self.send_request(soap_message)
- log.info('WS response time %s' % response.elapsed.total_seconds())
return self.handle_response(response)
def soap_wrap(self, body, headers):
@@ -61,17 +57,13 @@ def send_request(self, body):
def handle_500(self, response):
"""Handle reponse in case of ERROR 500 type."""
- # TODO : put a try catch (like wrong server)
- # no need to extract_body shit here
log.warning('Laposte error 500')
obj = objectify.fromstring(response.text)
- return {
+ errors = [{
"id": obj.xpath('//faultcode')[0],
- "status": self.STATUS_ERROR,
"message": obj.xpath('//faultstring')[0],
- "response": response,
- "payload": None
- }
+ }]
+ raise CarrierError(response, errors)
def handle_200(self, response):
"""
@@ -79,26 +71,20 @@ def handle_200(self, response):
It still can be a success or a failure.
"""
- def extract_message(response_xml):
+ def raise_on_error(response_xml):
xml = objectify.fromstring(response_xml)
messages = xml.xpath('//messages')
- exception = False
- for message in messages:
- mess_type = str(message.type)
- if mess_type.lower() == self.STATUS_ERROR.lower():
- exception = True
- # dirty serialization
- return {
- "exception": exception,
- "message": messages,
- }
-
- def extract_payload(response_xml):
- xml = objectify.fromstring(response_xml)
- payload_xml = xml.Body.getchildren()[0]
- return etree.tostring(payload_xml)
+ errors = [
+ {
+ 'id': message.id,
+ 'message': unicode(message.messageContent),
+ }
+ for message in messages if message.type == "ERROR"
+ ]
+ if len(errors) > 0:
+ raise CarrierError(response, errors)
- def extract_body(response):
+ def extract_xml(response):
"""Because the answer is mixedpart we need to extract."""
content_type = response.headers['Content-Type']
boundary = content_type.split('boundary="')[1].split('";')[0]
@@ -109,67 +95,30 @@ def extract_body(response):
clean_xml = after_start.strip() # = trim()
return clean_xml
- response_xml = extract_body(response)
-
- message = extract_message(response_xml)
-
- payload = None
+ def extract_body(response_xml):
+ """Remove soap wrapper."""
+ xml = objectify.fromstring(response_xml)
+ payload_xml = xml.Body.getchildren()[0]
+ return etree.tostring(payload_xml)
- if message['exception']:
- log.warning('Laposte error 200')
- status = self.STATUS_ERROR
- else:
- status = self.STATUS_SUCCES
- payload = extract_payload(response_xml)
- log.info('status: %s' % status)
+ response_xml = extract_xml(response)
+ raise_on_error(response_xml)
return {
- "status": status,
- "message": message,
- "payload": payload,
- "response": response,
+ 'body': extract_body(response_xml),
+ 'parts': get_parts(response),
+ 'response': response,
}
def handle_response(self, response):
"""Handle response of webservice."""
- if response.status_code == 500:
- return self.handle_500(response)
- elif response.status_code == 200:
+ if response.status_code == 200:
return self.handle_200(response)
+ elif response.status_code == 500:
+ return self.handle_500(response) # will raise
else:
- return {
- "status": "error",
- "message": "Unexpected status code from server",
- "response": response
- }
+ raise CarrierError(response, [{
+ 'id': None,
+ 'message': "Unexpected status code from server",
+ }])
- def get_parts(self, response):
- head_lines = ''
- for k, v in response.raw.getheaders().iteritems():
- head_lines += str(k) + ':' + str(v) + '\n'
-
- full = head_lines + response.content
-
- parser = email.parser.Parser()
- decoded_reply = parser.parsestr(full)
- parts = {}
- start = decoded_reply.get_param('start').lstrip('<').rstrip('>')
- i = 0
- for part in decoded_reply.get_payload():
- cid = part.get('content-Id', '').lstrip('<').rstrip('>')
- if (not start or start == cid) and 'start' not in parts:
- parts['start'] = part.get_payload()
- else:
- parts[cid or 'Attachment%d' % i] = part.get_payload()
- i += 1
- return parts
-
- def exception_handling(self, messages):
- message_labels = []
- for message in messages:
- if message.messageContent:
- message_labels.append({
- 'id': message.id,
- 'message': unicode(message.messageContent),
- })
- log.debug('message: %s' % message_labels)
- return message_labels
+
\ No newline at end of file
diff --git a/roulier/carriers/laposte/templates/laposte_address.xml b/roulier/carriers/laposte/templates/laposte_address.xml
index b2045b3..3aaf199 100755
--- a/roulier/carriers/laposte/templates/laposte_address.xml
+++ b/roulier/carriers/laposte/templates/laposte_address.xml
@@ -2,8 +2,8 @@
{{ address.company }}
{{ address.name }}
{{ address.firstName }}
- {{ address.street0 }}
- {{ address.street2 }}
+ {{ address.street2 }}
+ {{ address.street0 }}
{{ address.street1 }}
{{ address.street3 }}
{{ address.country }}
@@ -15,4 +15,4 @@
{{ address.email | default('') }}
{{ address.intercom }}
{{ address.language }}
-
\ No newline at end of file
+
diff --git a/roulier/carriers/laposte/templates/laposte_generateLabelRequest.xml b/roulier/carriers/laposte/templates/laposte_generateLabelRequest.xml
index 36f3955..837a48b 100755
--- a/roulier/carriers/laposte/templates/laposte_generateLabelRequest.xml
+++ b/roulier/carriers/laposte/templates/laposte_generateLabelRequest.xml
@@ -8,13 +8,13 @@
{% include "laposte_parcel.xml" %}
{% include "laposte_customsDeclarations.xml" %}
- refsenderXXX
+ {{ service.reference1 }}
{% with address = sender_address %}
{% include "laposte_address.xml" %}
{% endwith %}
- 12345
+ {{ service.reference2 }}
false
service info
diff --git a/roulier/carriers/trs/__init__.py b/roulier/carriers/trs/__init__.py
new file mode 100755
index 0000000..4e09c76
--- /dev/null
+++ b/roulier/carriers/trs/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+from . import trs
+from . import trs_encoder
+from . import trs_decoder
+from . import trs_transport
diff --git a/roulier/carriers/trs/templates/trs_deposit_slip.csv b/roulier/carriers/trs/templates/trs_deposit_slip.csv
new file mode 100644
index 0000000..dfc0eff
--- /dev/null
+++ b/roulier/carriers/trs/templates/trs_deposit_slip.csv
@@ -0,0 +1,4 @@
+client,siret,refCommande,dateEnlevement,dateLivraison,cr,va,nom,adr1,adr2,cp,ville,telephone,mobile,email,refDest,commentLiv,nbConducteurs,Poids,nbColis,qtéFacturée,article1,regroupement,refComfour,codeBarre,descColis,porteur,jourLivraison
+{{sender_address.name}},{{#siret}},{{parcel.ref?}},{{ ?.date}},{{ #dateLivraison}},{{#cod}},{{#va}},{{ receiver_address.name}},{{ receiver_address.street1}},{{ receiver_address.street2}},{{ receiver_address.zipCode}},{{ receiver_address.city}},{{ receiver_address.phone}},{{ receiver_address.email}},{{#refDest}},{{#commentaire livraison}},{{#nombre de conduteurs?}},{{parcel.weight}},{{#nombre de colis}},{{#qteFacture?}},{{#article}},{{#regroupement}},{{#refComfour}},{{codebar}},descColis,porteur,jourLivraison
+
+
diff --git a/roulier/carriers/trs/templates/trs_generateLabel.zpl b/roulier/carriers/trs/templates/trs_generateLabel.zpl
new file mode 100755
index 0000000..446efe1
--- /dev/null
+++ b/roulier/carriers/trs/templates/trs_generateLabel.zpl
@@ -0,0 +1,43 @@
+^XA
+
+^FX Grand cadre
+^FO30,15^GB800,1200,3^FS
+
+^FX Expediteur
+^CF0,40
+^FO50,30^FD{{ from_address.company }}^FS
+^FO30,15^GB800,50,3^FS
+
+^FX Num colis et poids
+^FO60,290^FDColis {{ service.reference2 }}^FS
+^FO60,340^FDPoids {{ parcel.weight }}^FS
+^FO40,270^GB200,120,3^FS
+
+^FX Adresse
+^FO300,130^TB,480,120^FD{{ to_address.name }}^FS
+^FO300,220^TB,480,90^FD^FD{{ to_address.street1 }}^FS
+^FO300,300^TB,480,90^FD^FD{{ to_address.street2 }}^FS
+^FO300,380^FD{{ to_address.zip }}^FS
+^FO400,420^FD{{ to_address.city }}^FS
+^FO280,90^GB500,400,3^FS
+
+^FO300,510^FDTel: {{ to_address.phone }}^FS
+
+^FX Ref, commentaires
+^FO60,800^FDReference / Commentaires: ^FS
+^FO60,840^FD{{ service.reference1 }} ^FS
+^FO60,880^FD{{ service.reference3 }} ^FS
+^FO30,780^GB800,0,3^FS
+
+
+^FX Code barre
+^BY3,1,150
+^FO60,580^B3^FD{{ service.shippingId }}^FS
+
+
+^FX Departement
+^CF0,160
+^FO60,100^FD{{ to_address.dept }}^FS
+^FO40,90^GB200,150,3^FS
+
+^XZ
diff --git a/roulier/carriers/trs/trs.py b/roulier/carriers/trs/trs.py
new file mode 100755
index 0000000..81a6532
--- /dev/null
+++ b/roulier/carriers/trs/trs.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+"""Implementation for trs."""
+from trs_encoder import TrsEncoder
+from trs_decoder import TrsDecoder
+from trs_transport import TrsTransport
+from roulier.carrier import Carrier
+
+
+class Trs(Carrier):
+ """Implementation for trs."""
+
+ encoder = TrsEncoder()
+ decoder = TrsDecoder()
+ ws = TrsTransport()
+
+ def api(self):
+ """Expose how to communicate with trs."""
+ return self.encoder.api()
+
+ def get(self, data, action):
+ """Run an action."""
+ if action == "generateLabel":
+ return self.get_label(data)
+ if action == "depositSlip":
+ return self.get_deposit_slip(data)
+ raise Exception(
+ 'action %s not in %s' % (
+ action, ', '.join(self.encoder.TRS_ACTIONS)))
+
+ def get_label(self, data):
+ """Generate a label."""
+ request = self.encoder.encode(data, 'generateLabel')
+ response = self.ws.send(request)
+
+ return self.decoder.decode(response, request)
+
+ def get_deposit_slip(self, data):
+ """Generate a deposit slip (csv file)."""
+ return self.ws.generate_deposit_slip(data)
diff --git a/roulier/carriers/trs/trs_api.py b/roulier/carriers/trs/trs_api.py
new file mode 100644
index 0000000..10e227d
--- /dev/null
+++ b/roulier/carriers/trs/trs_api.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+"""Implementation of TRS Api."""
+from roulier.api import Api
+
+TRS_LABEL_FORMAT = (
+ 'ZPL',
+)
+
+
+class TrsApi(Api):
+ def _service(self):
+ schema = super(TrsApi, self)._service()
+ schema['labelFormat']['allowed'] = TRS_LABEL_FORMAT
+ schema['labelFormat']['default'] = TRS_LABEL_FORMAT[0]
+ schema['shippingId'].update({'required': True, 'empty': False})
+
+ return schema
+
+ def _address(self):
+ schema = super(TrsApi, self)._address()
+ schema['country'].update({'required': True, 'empty': False})
+ schema['zip'].update({'required': True, 'empty': False})
+ schema['city'].update({'required': True, 'empty': False})
+ return schema
+
+ def _from_address(self):
+ schema = super(TrsApi, self)._from_address()
+ schema['company'].update({'required': True, 'empty': False})
+ schema['phone'].update({'required': False, 'empty': True})
+ schema['street1']['required'] = False
+ return schema
+
+ def _auth(self):
+ schema = super(TrsApi, self)._auth()
+ schema['login'].update({'required': False, 'empty': True})
+ schema['password']['required'] = False
+ return schema
+
+ def _schemas(self):
+ # Santize all fields for zpl
+ schemas = super(TrsApi, self)._schemas()
+ for schema in schemas:
+ for field in schemas[schema]:
+ schemas[schema][field].update({'coerce': 'zpl'})
+ return schemas
diff --git a/roulier/carriers/trs/trs_decoder.py b/roulier/carriers/trs/trs_decoder.py
new file mode 100755
index 0000000..084a6b8
--- /dev/null
+++ b/roulier/carriers/trs/trs_decoder.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+"""Mock class."""
+from roulier.codec import Decoder
+
+
+class TrsDecoder(Decoder):
+ """Prepare output"""
+
+ def decode(self, response, request):
+ """Return {}."""
+
+ return {
+ 'label': response['payload'],
+ 'tracking_number': request['body']['service']['shippingId'],
+ 'tracking_url': '',
+ 'annexes': [
+ {
+ 'name': 'meta',
+ 'type': 'csv',
+ 'data': response['attachment']
+ }
+ ]
+ }
diff --git a/roulier/carriers/trs/trs_encoder.py b/roulier/carriers/trs/trs_encoder.py
new file mode 100755
index 0000000..bcfaae0
--- /dev/null
+++ b/roulier/carriers/trs/trs_encoder.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+"""Transform input to trs zpl."""
+from roulier.codec import Encoder
+from roulier.exception import InvalidApiInput
+from .trs_api import TrsApi
+
+TRS_ACTIONS = ('generateLabel', 'generateDepositSlip')
+
+
+class TrsEncoder(Encoder):
+ """Transform input to trs zpl."""
+
+ def encode(self, api_input, action):
+ """Dispatcher."""
+ if action == 'generateLabel':
+ return self.generate_label(api_input)
+ raise Exception(
+ 'action %s not in %s' % (action, ', '.join(TRS_ACTIONS)))
+
+ def generate_label(self, api_input):
+ """Transform input to trs zpl."""
+ api = TrsApi()
+ if not api.validate(api_input):
+ raise InvalidApiInput(
+ 'Input error : %s' % api.errors(api_input))
+ data = api.normalize(api_input)
+
+ data['to_address']["dept"] = data['to_address']['zip'][0:2]
+
+ return {'body': data, 'header': None}
+
+ def api(self):
+ api = TrsApi()
+ return api.api_values()
diff --git a/roulier/carriers/trs/trs_transport.py b/roulier/carriers/trs/trs_transport.py
new file mode 100755
index 0000000..55c70e0
--- /dev/null
+++ b/roulier/carriers/trs/trs_transport.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+"""Implementation for trs."""
+from roulier.transport import Transport
+from jinja2 import Environment, PackageLoader
+from collections import OrderedDict
+import unicodecsv as csv
+from io import BytesIO
+
+
+class TrsTransport(Transport):
+ """Generate ZPL offline and csv for EDI."""
+
+ STATUS_SUCCESS = "Success"
+
+ def send(self, payload):
+ """Call this function.
+
+ Args:
+ body: an object with a lot usefull values
+ Return:
+ {
+ status: STATUS_SUCCES or STATUS_ERROR, (string)
+ message: more info about status of result (None)
+ response: (None)
+ payload: usefull payload (if success) (body as string)
+
+ }
+ """
+ body = payload['body']
+ label = self.generate_zpl(body)
+ attachment = self.map_delivery_line(body)
+ return {
+ "status": self.STATUS_SUCCESS,
+ "message": None,
+ "response": None,
+ "payload": label,
+ "attachment": attachment
+ }
+
+ def generate_zpl(self, body):
+ env = Environment(
+ loader=PackageLoader('roulier', '/carriers/trs/templates'),
+ extensions=['jinja2.ext.with_'])
+
+ template = env.get_template("trs_generateLabel.zpl")
+ return template.render(
+ service=body['service'],
+ parcel=body['parcel'],
+ from_address=body['from_address'],
+ to_address=body['to_address'])
+
+ def map_delivery_line(self, body):
+ data = OrderedDict([
+ (u'client', body['from_address']['company']),
+ (u'siret', None),
+ (u'refCommande', body['service']['reference1']),
+ (u'dateEnlevement', body['service']['shippingDate']),
+ (u'cr', None),
+ (u'va', None),
+ (u'nom', body['to_address']['name']),
+ (u'adr1', body['to_address']['street1']),
+ (u'adr2', body['to_address']['street2']),
+ (u'cp', body['to_address']['zip']),
+ (u'ville', body['to_address']['city']),
+ (u'telephone', body['to_address']['phone']),
+ (u'mobile', body['to_address']['phone']),
+ (u'email', body['to_address']['email']),
+ (u'refDest', body['service']['reference1']),
+ (u'commentLiv', None),
+ (u'nbConducteurs', None),
+ (u'Poids', '%.f' % (body['parcel']['weight'] * 1000)), # kg to g
+ (u'nbColis', 1), # one row per parcel
+ (u'qtéFacturée1', None),
+ (u'qtéFacturée2', None),
+ (u'qtéFacturée3', None),
+ (u'qtéFacturée4', None),
+ (u'qtéFacturée5', None),
+ (u'qtéFacturée6', None),
+ (u'qtéFacturée7', None),
+ (u'qtéFacturée8', None),
+ (u'qtéFacturée9', None),
+ (u'qtéFacturée10', None),
+ (u'article1', None),
+ (u'article2', None),
+ (u'article3', None),
+ (u'article4', None),
+ (u'article5', None),
+ (u'article6', None),
+ (u'article7', None),
+ (u'article8', None),
+ (u'article9', None),
+ (u'article10', None),
+ (u'regroupement', None),
+ (u'refComfour', None),
+ (u'codeBarre', body['service']['shippingId']),
+ (u'descColis', None),
+ (u'porteur', None),
+ (u'jourLivraison', None),
+ ])
+ return data
+
+ def generate_deposit_slip(self, rows):
+ output = BytesIO()
+
+ # l'ordre est important
+ headers = rows[0].keys()
+
+ # l'ordre est fixé par headers
+ writer = csv.DictWriter(output, headers, encoding='utf-8')
+ writer.writeheader()
+ writer.writerows(rows)
+ return output
diff --git a/roulier/exception.py b/roulier/exception.py
index 22cfb13..6906eaf 100644
--- a/roulier/exception.py
+++ b/roulier/exception.py
@@ -1,8 +1,28 @@
# -*- coding: utf-8 -*-
-"""Exception class"""
+"""Exception classes"""
+import logging
+
+log = logging.getLogger(__name__)
class InvalidApiInput(Exception):
- """ Use this class in your application to manage
- exception with api call
+ """Bad input.
+
+ Use this class in your application to manage
+ exception with api call
+ """
+
+
+class CarrierError(Exception):
+ """Error from WS.
+
+ Use this class in your application to manage
+ exception with the carrier WS
"""
+ def __init__(self, response, msg=None):
+ if msg is None:
+ msg = "An error occured with WS"
+ super(CarrierError, self).__init__(msg)
+ self.response = response
+ if self.response:
+ log.debug(response.text)
diff --git a/roulier/roulier.py b/roulier/roulier.py
index e3fa45c..4adb704 100755
--- a/roulier/roulier.py
+++ b/roulier/roulier.py
@@ -4,6 +4,7 @@
from .carriers.dummy.dummy import Dummy
from .carriers.geodis.geodis import Geodis
from .carriers.dpd.dpd import Dpd
+from .carriers.trs.trs import Trs
def _carriers():
@@ -16,6 +17,7 @@ def _carriers():
"dummy": Dummy,
"geodis": Geodis,
"dpd": Dpd,
+ "trs": Trs,
}
diff --git a/roulier/ws_tools.py b/roulier/ws_tools.py
index b6375c3..9e59236 100755
--- a/roulier/ws_tools.py
+++ b/roulier/ws_tools.py
@@ -5,6 +5,7 @@
from zplgrf import GRF
from PIL import Image
from io import BytesIO
+import email.parser
import re
import base64
@@ -34,6 +35,35 @@ def remove_empty_tags(xml, ouput_as_string=True):
return transform(xml)
+def get_parts(response):
+ """Extract parts from headers.
+
+ Params:
+ response: a request object
+ Returns:
+ an array of content-ids
+ """
+ head_lines = ''
+ for k, v in response.raw.getheaders().iteritems():
+ head_lines += str(k) + ':' + str(v) + '\n'
+
+ full = head_lines + response.content
+
+ parser = email.parser.Parser()
+ decoded_reply = parser.parsestr(full)
+ parts = {}
+ start = decoded_reply.get_param('start').lstrip('<').rstrip('>')
+ i = 0
+ for part in decoded_reply.get_payload():
+ cid = part.get('content-Id', '').lstrip('<').rstrip('>')
+ if (not start or start == cid) and 'start' not in parts:
+ parts['start'] = part.get_payload()
+ else:
+ parts[cid or 'Attachment%d' % i] = part.get_payload()
+ i += 1
+ return parts
+
+
def png_to_zpl(png, rotate):
u"""Transform a PNG in a suitable format for ZPL.
diff --git a/setup.py b/setup.py
index 62e93d4..b72e57a 100755
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,9 @@
name="roulier",
version=version,
packages=find_packages(),
- install_requires=['lxml', 'Jinja2', 'requests', 'cerberus', 'zplgrf'],
+ install_requires=[
+ 'lxml', 'Jinja2', 'requests', 'cerberus', 'zplgrf',
+ 'unicodecsv', 'unidecode'],
author="Hparfr ",
author_email="roulier@hpar.fr",
description="Label parcels without pain",