From fccf493f8e46ea3f37fee2ea9cc8456997c03556 Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Fri, 24 Apr 2020 10:50:14 +0200 Subject: [PATCH] Add tracking/shipment info --- formity/admin.py | 5 +++ formity/models.py | 25 +++++++++-- requirements.txt | 2 + shipping/dpd.py | 61 +++++++++++++++++++++++++++ shipping/kurjerzy.py | 2 +- templates/admin/tracking.html | 26 ++++++++++++ templates/faceshieldrequest_list.html | 5 +++ templates/shipping_label.html | 4 +- 8 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 shipping/dpd.py create mode 100644 templates/admin/tracking.html diff --git a/formity/admin.py b/formity/admin.py index 0e987ef..6a4329c 100644 --- a/formity/admin.py +++ b/formity/admin.py @@ -173,6 +173,11 @@ class FaceshieldRequestAdmin(ModelView): model = self.get_one(request.args.get('id')) return self.render('label.html', models=[model]) + @flask_admin.expose('/shipmentinfo') + def tracking(self): + model = self.get_one(request.args.get('id')) + return self.render('admin/tracking.html', models=[model]) + @flask_admin.expose('/import-dpd', methods=['POST', 'GET']) def dpd_import(self): if request.method == 'POST': diff --git a/formity/models.py b/formity/models.py index ad31188..d7ab84f 100644 --- a/formity/models.py +++ b/formity/models.py @@ -101,18 +101,37 @@ class FaceshieldRequest(db.Model): return self.faceshield_full_delivered // 150 return 1 - @cache.memoize(timeout=10 * 60) def fetch_shipping_info(self): if self.shipping_provider == 'kurjerzy': from shipping.kurjerzy import Kurjerzy k = Kurjerzy(current_app) k.authenticate() - return k.shipment_info(self.shipping_id) + return [self.cache_shipment_info(k.shipment_info, i) for i in self.shipping_id.split(',')] + elif self.shipping_provider == 'xbs': + from shipping.dpd import DPD + d = DPD(current_app) + return [self.cache_shipment_info(d.shipment_info, i) for i in self.shipping_id.split(',')] return None + def cache_shipment_info(self, fetch, id): + key = 'shipment_info_%d_%s' % (self.id, id) + res = cache.get(key) + if not res: + try: + res = fetch(id) + except Exception as exc: + res = {'error': str(exc)} + if res.get('tracking', {}).get('delivered'): + cache.set(key, res, timeout=60 * 60 * 24 * 7) + else: + cache.set(key, res, timeout=60 * 10) + + return res + def refresh_shipping_info(self): - cache.delete_memoized(self.fetch_shipping_info, self) + for i in self.shipping_id.split(','): + cache.delete('shipment_info_%d_%s' % (self.id, id)) @property def shipping_info(self): diff --git a/requirements.txt b/requirements.txt index c1803ae..94d2242 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ alembic==1.4.2 Babel==2.8.0 +beautifulsoup4==4.9.0 blinker==1.4 cairocffi==1.1.0 CairoSVG==2.4.2 @@ -46,6 +47,7 @@ requests==2.23.0 requests-oauthlib==1.3.0 six==1.14.0 sortedcontainers==2.1.0 +soupsieve==2.0 SQLAlchemy==1.3.15 tinycss2==1.0.2 unicodecsv==0.14.1 diff --git a/shipping/dpd.py b/shipping/dpd.py new file mode 100644 index 0000000..42536a3 --- /dev/null +++ b/shipping/dpd.py @@ -0,0 +1,61 @@ +import requests +import bs4 +import re + + +class DPD: + def __init__(self, app): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', + }) + self.regexes = { + 'booked': r'Zarejestrowano dane przesyłki, przesyłka jeszcze nienadana', + 'collected': r'Przesyłka odebrana przez Kuriera', + 'inDelivery': r'Wydanie przesyłki do doręczenia', + 'delivered': r'Przesyłka doręczona Odbiorca: .*', + } + + def authenticate(self): + pass + + def shipment_info(self, id): + resp = self.session.post('https://tracktrace.dpd.com.pl/findPackage', data={ + 'q': id, + 'typ': '1', + }) + resp.raise_for_status() + soup = bs4.BeautifulSoup(resp.text, features="html5lib") + statuses = [] + tracking = { + 'statuses': statuses, + 'booked': None, + 'collected': None, + 'inDelivery': None, + 'delivered': None, + } + + for row in soup.find(class_='table-track').find('tbody').findAll('tr'): + cols = [item.get_text(' ', strip=True).replace('\xa0', ' ') for item in row.findAll('td')] + status = { + 'timestamp': '%s %s' % (cols[0], cols[1]), + 'description': cols[2], + 'location': cols[3], + } + for k, r in self.regexes.items(): + if tracking[k] is None and re.match(r, status['description']): + tracking[k] = status['timestamp'] + + statuses.append(status) + + return { + 'numberParcel': id, + 'tracking': tracking, + } + + +if __name__ == '__main__': + d = DPD(None) + d.authenticate() + import pprint + pprint.pprint(d.shipment_info('xxxxxxxxxx')) diff --git a/shipping/kurjerzy.py b/shipping/kurjerzy.py index 4ab19d3..ef94388 100644 --- a/shipping/kurjerzy.py +++ b/shipping/kurjerzy.py @@ -133,7 +133,7 @@ class Kurjerzy: model.shipping_id = result['data']['createShipment']['shipment']['id'] def shipment_info(self, id): - result = self.send_query(r'query($id: Int) { shipments(ids:[$id]) { id, labelRef, labelSource, status, tracking { statuses } } }', { + result = self.send_query(r'query($id: Int) { shipments(ids:[$id]) { labelSource, numberParcel, status, tracking { booked, collected, inDelivery, delivered, returned, statuses } } }', { 'id': id, }) diff --git a/templates/admin/tracking.html b/templates/admin/tracking.html new file mode 100644 index 0000000..2c8f56e --- /dev/null +++ b/templates/admin/tracking.html @@ -0,0 +1,26 @@ + diff --git a/templates/faceshieldrequest_list.html b/templates/faceshieldrequest_list.html index 3b587b1..573679d 100644 --- a/templates/faceshieldrequest_list.html +++ b/templates/faceshieldrequest_list.html @@ -4,6 +4,11 @@ + {% if row.shipping_id %} + + + + {% endif %} {% endblock %} {% block head_css %}{{ super() }} diff --git a/templates/shipping_label.html b/templates/shipping_label.html index cea641d..619a58a 100644 --- a/templates/shipping_label.html +++ b/templates/shipping_label.html @@ -16,10 +16,12 @@ height: 100%; {% endblock %} {% block content %} {% for model in models %} + {% for shipment in model.shipping_info %}
- label {{ model.id }} + label {{ model.id }}
+ {% endfor %} {% endfor %} {% endblock %}