covid-formity/formity/admin.py

438 lines
20 KiB
Python

import enum
import io
import datetime
import csv
from flask import redirect, flash, request, url_for, make_response, current_app, session
import flask_admin
import pdfplumber
from formity.extensions import admin, db, ModelView, ModelViewHighSecurity, AdminSecurityMixin
from wtforms import TextAreaField, validators
from formity.models import FaceshieldRequest, RequestChange, Status, PostalCode, ExternalUser
from spaceauth import current_user
from flask_weasyprint import HTML, render_pdf
from sqlalchemy import func
class IndexView(AdminSecurityMixin, flask_admin.AdminIndexView):
@flask_admin.expose('/')
def index(self):
stats = FaceshieldRequest.query.with_entities(
FaceshieldRequest.status.label('status'),
FaceshieldRequest.handling_orga.label('handling_orga'),
func.count().label('count'),
func.sum(FaceshieldRequest.faceshield_full_required).label('faceshield_full_required'),
func.sum(FaceshieldRequest.faceshield_full_delivered).label('faceshield_full_delivered'),
func.sum(FaceshieldRequest.faceshield_front_required).label('faceshield_front_required'),
func.sum(FaceshieldRequest.faceshield_front_delivered).label('faceshield_front_delivered'),
).filter(FaceshieldRequest.status != Status.rejected, FaceshieldRequest.status != Status.spam).group_by(FaceshieldRequest.status, FaceshieldRequest.handling_orga).order_by(FaceshieldRequest.status, FaceshieldRequest.handling_orga).all()
vstats = FaceshieldRequest.query.select_from(FaceshieldRequest).with_entities(
func.coalesce(PostalCode.voivodeship, 'unknown').label('voivodeship'),
func.count().label('count'),
func.sum(FaceshieldRequest.faceshield_full_required).label('faceshield_full_required'),
func.sum(FaceshieldRequest.faceshield_full_delivered).label('faceshield_full_delivered'),
func.sum(FaceshieldRequest.faceshield_front_required).label('faceshield_front_required'),
func.sum(FaceshieldRequest.faceshield_front_delivered).label('faceshield_front_delivered'),
).filter(
FaceshieldRequest.status != Status.rejected,
FaceshieldRequest.status != Status.spam,
).join(FaceshieldRequest.postalcode_info, isouter=True).group_by('voivodeship').order_by(func.count().desc()).all()
dstats = FaceshieldRequest.query.select_from(FaceshieldRequest).with_entities(
func.date_trunc('day', FaceshieldRequest.created).label('date'),
func.count().label('count'),
).filter(FaceshieldRequest.status != Status.rejected, FaceshieldRequest.status != Status.spam).group_by(func.date_trunc('day', FaceshieldRequest.created)).order_by(func.date_trunc('day', FaceshieldRequest.created)).all()
return self.render('admin_index.html', stats=stats, vstats=vstats, dstats=dstats)
class MapView(AdminSecurityMixin, flask_admin.BaseView):
@flask_admin.expose('/')
def index(self):
mode = request.args.get('mode', 'all')
if mode == 'new':
query = FaceshieldRequest.query.filter(FaceshieldRequest.shipping_latitude != None, FaceshieldRequest.status == Status.new)
else:
query = FaceshieldRequest.query.filter(FaceshieldRequest.shipping_latitude != None, FaceshieldRequest.status != Status.spam, FaceshieldRequest.status != Status.rejected)
mapdata = [
{
key: getattr(request, key).name if isinstance(getattr(request, key), enum.Enum) else getattr(request, key)
for key in ['id', 'entity_info', 'shipping_latitude', 'shipping_longitude', 'status', 'handling_orga', 'extra', 'remarks', 'faceshield_full_required']
}
for request in query
]
return self.render('admin_map.html', mapdata=mapdata, focus=request.args.get('id', None))
class FaceshieldRequestAdmin(ModelView):
column_default_sort = 'created'
details_modal_template = 'changelog_details_modal.html'
edit_template = 'changelog_edit.html'
list_template = 'faceshieldrequest_list.html'
column_searchable_list = ('id', 'email', 'remarks', 'extra', 'entity_info', 'full_name')
column_filters = (
'id',
'entity_info', 'full_name', 'phone_number', 'email', 'extra',
'faceshield_front_required', 'faceshield_model',
'faceshield_front_delivered',
'faceshield_full_required', 'faceshield_full_delivered',
'adapter_3m_dar_required', 'adapter_3m_dar_delivered',
'adapter_easybreath_dar_required', 'adapter_easybreath_dar_delivered',
'adapter_rd40_dar_required', 'adapter_rd40_dar_delivered',
'adapter_secura_dar_required', 'adapter_secura_dar_delivered',
'handling_orga',
'created',
# nie dla psa x-D
# 'ua', 'ip',
'status', 'remarks',
'parent_id',
'shipping_name', 'shipping_street', 'shipping_postalcode', 'shipping_city',
'shipping_latitude', 'shipping_longitude', 'shipping_provider', 'shipping_id',
'postalcode_info',
)
form_overrides = {'entity_info': TextAreaField, 'extra': TextAreaField, 'remarks': TextAreaField}
form_args = {
'shipping_name': {'validators': [validators.Length(max=35)]},
}
form_excluded_columns = ('changelog', 'postalcode_info')
column_export_list = column_filters
column_labels = {
'faceshield_front_required': 'Front required',
'faceshield_full_required': 'Full required',
'faceshield_front_delivered': 'Front delivered',
'faceshield_full_delivered': 'Full delivered',
}
column_list = ('id', 'entity_info', 'full_name', 'faceshield_full_required', 'faceshield_full_delivered', 'faceshield_front_required', 'faceshield_front_delivered', 'handling_orga', 'created', 'status')
column_editable_list = ('status', 'remarks', 'handling_orga')
form_choices = {
'handling_orga': [
('hswaw', 'hswaw'),
('hskrk', 'hskrk'),
('hswro', 'hswro'),
],
'shipping_provider': [
('kurjerzy', 'Kurjerzy'),
('xbs', 'XBS Group'),
],
}
can_delete = False
can_view_details = True
details_modal = True
can_set_page_size = True
can_export = True
@flask_admin.expose('/label/')
def label(self):
return render_pdf(HTML(string=self.label_html()))
@flask_admin.expose('/label/html')
def label_html(self):
model = self.get_one(request.args.get('id'))
return self.render('label.html', models=[model])
@flask_admin.expose('/import-dpd', methods=['POST', 'GET'])
def dpd_import(self):
if request.method == 'POST':
# check if the post request has the file part
if request.form.get('accept') == '1':
changed = 0
for k, v in session.get('dpd-import', {}).items():
m = FaceshieldRequest.query.filter(FaceshieldRequest.id == k).first()
if not m:
flash('Request #{} not found'.format(k), 'warning')
continue
m.shipping_id = ','.join(v)
m.shipping_provider = 'xbs'
changed += 1
db.session.commit()
flash('{} requests changed'.format(changed), 'info')
session.pop('dpd-import')
return redirect(request.url)
if 'file' not in request.files:
flash('No file uploaded', 'error')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('No selected file', 'error')
return redirect(request.url)
p = pdfplumber.load(file.stream)
data = self.parse_dpd(p)
session['dpd-import'] = {k: list(v) for k, v in data.items()}
return self.render('admin/dpd_pdf_import.html', data=session['dpd-import'])
return self.render('admin/dpd_pdf_import.html')
def parse_dpd(self, pdf):
data = {}
for page in pdf.pages:
table = page.extract_table()
if table[0] != [None, None, None, None, 'MIEJSCE DORĘCZENIA PACZKI', None, None, None, None, None]:
raise Exception('Invalid header (1/2)')
if table[1] != ['LP', 'Numkat', 'NUMER PACZKI', 'NUMER REFERENCYJNY\n( np. numer faktury, zamówienia itp.)', 'OPIS', 'Odbiorca', 'Wartość\nprzesyłki', 'KWOTA\nCOD PLN', 'Waga dekl.\n(kg)', 'Zawartość']:
raise Exception('Invalid header (2/2)')
for row in table[2:]:
ref = row[3].split('/')[0]
tracking = row[2]
if ref not in data:
data[ref] = set()
data[ref].add(tracking)
return data
def bulk_status_change(self, ids, status):
try:
query = self.get_query().filter(FaceshieldRequest.id.in_(ids))
count = 0
for req in query.all():
if req.status != status:
req.status = status
count += 1
db.session.commit()
flash('{} requests were successfully marked as {}.'.format(count, status.name))
except Exception as ex:
if not self.handle_view_exception(ex):
raise
flash('Failed to mark request as {}: {}'.format(status.name, str(ex)), 'error')
@flask_admin.actions.action('new', 'Mark as new', 'Are you sure you want to mark as new?')
def action_new(self, ids):
self.bulk_status_change(ids, Status.new)
@flask_admin.actions.action('fulfilled', 'Mark as fulfilled', 'Are you sure you want to mark as fulfilled?')
def action_fulfilled(self, ids):
self.bulk_status_change(ids, Status.fulfilled)
@flask_admin.actions.action('intransit', 'Mark as intransit', 'Are you sure you want to mark as intransit?')
def action_intransit(self, ids):
self.bulk_status_change(ids, Status.intransit)
@flask_admin.actions.action('shippingpending', 'Mark as shippingpending', 'Are you sure you want to mark as shippingpending?')
def action_shippingpending(self, ids):
self.bulk_status_change(ids, Status.shippingpending)
@flask_admin.actions.action('pickuppending', 'Mark as pickuppending', 'Are you sure you want to mark as pickuppending?')
def action_pickuppending(self, ids):
self.bulk_status_change(ids, Status.pickuppending)
@flask_admin.actions.action('bulkprint', 'Print')
def action_bulkprint(self, ids):
models = self.get_query().filter(FaceshieldRequest.id.in_(ids)).all()
return render_pdf(HTML(string=self.render('label.html', models=models)))
@flask_admin.actions.action('csv_kurjerzy', 'Export Kurjerzy.pl CSV')
def action_csv_kurjerzy(self, ids):
models = self.get_query().filter(FaceshieldRequest.id.in_(ids)).all()
fields = [
'kurier',
'nadawca_nazwa', 'nadawca_email', 'nadawca_ulica',
'nadawca_nr_lok', 'nadawca_kod', 'nadawca_miasto',
'nadawca_telefon', 'nadawca_kraj', 'nadawca_region',
'nadawca_punkt',
'odbiorca_nazwa', 'odbiorca_email', 'odbiorca_ulica',
'odbiorca_nr_lok', 'odbiorca_kod', 'odbiorca_miasto',
'odbiorca_telefon', 'odbiorca_kraj', 'odbiorca_region',
'odbiorca_punkt',
'przesylka_rodzaj', 'przesylka_opakowanie', 'przesylka_waga',
'przesylka_szerokosc', 'przesylka_wysokosc', 'przesylka_dlugosc',
'przesylka_wartosc', 'przesylka_zawartosc',
'pobranie_wartosc', 'pobranie_nr_bank', 'pobranie_1_dzien',
'sms_nadawca', 'sms_odbiorca', 'dostawa_nast_dnia',
'dokumenty_zwrotne', 'bez_odbioru', 'numer_ref', 'kod_promo'
]
fname = 'kurjerzy-export-%s.csv' % (datetime.datetime.now().strftime(r'%Y%m%d-%H%M%S'),)
si = io.StringIO()
writer = csv.DictWriter(si, fields, delimiter=';')
writer.writeheader()
config = current_app.config
for row in models:
writer.writerow({
'kurier': 'ups',
'nadawca_nazwa': config['SHIPPING_SENDER_NAME'],
'nadawca_email': config['SHIPPING_SENDER_EMAIL'],
'nadawca_ulica': config['SHIPPING_SENDER_STREET'],
'nadawca_nr_lok': config['SHIPPING_SENDER_NUMBER'],
'nadawca_kod': config['SHIPPING_SENDER_POSTALCODE'],
'nadawca_miasto': config['SHIPPING_SENDER_CITY'],
'nadawca_telefon': config['SHIPPING_SENDER_PHONE_NUMBER'],
'nadawca_kraj': 'PL',
'odbiorca_nazwa': row.shipping_name,
'odbiorca_email': row.email,
'odbiorca_ulica': row.shipping_street,
'odbiorca_nr_lok': '1/1', # FIXME
'odbiorca_kod': row.shipping_postalcode,
'odbiorca_miasto': row.shipping_city,
'odbiorca_telefon': row.phone_number,
'odbiorca_kraj': 'PL',
# TODO
'przesylka_rodzaj': 1,
'przesylka_opakowanie': 1,
'przesylka_waga': 4,
'przesylka_szerokosc': 40,
'przesylka_wysokosc': 30,
'przesylka_dlugosc': 20,
'przesylka_wartosc': 100,
'przesylka_zawartosc': 'przyłbice dla medyków %d' % (row.id,),
'sms_odbiorca': 1,
'dostawa_nast_dnia': 1,
})
row.changelog.append(RequestChange(
remarks='included on Kurjerzy export (%s)' % (fname,),
))
db.session.commit()
output = make_response(si.getvalue())
output.headers["Content-Disposition"] = "attachment; filename=%s" % (fname,)
output.headers["Content-type"] = "text/csv"
return output
@flask_admin.actions.action('csv_xbs', 'Export XBS Group/DPD CSV')
def action_csv_xbs(self, ids):
models = self.get_query().filter(FaceshieldRequest.id.in_(ids)).all()
fields = [
'NAZWA NADAWCY', 'OSOBA KONTAKTOWA NADAWCA',
'TELEFON KONTAKTOWY NADAWCA ', 'ULICA NADAWCA',
'KOD POCZTOWY NADAWCA', 'MIASTO NADAWCA',
'NAZWA ODBIORCA', 'OSOBA KONTAKTOWA ODBIORCA',
'TELEFON KONTAKTOWYODBIORCA', 'ULICA ODBIORCA',
'KOD POCZTOWY ODBIORCA', 'MIASTO ODBIORCA',
'WAGA', 'ILOŚĆ PACZEK', 'NUMKAT', 'NEXT DAY', 'SOBOTA',
'GWARANT 9:30', 'GWARANT 12:00', 'DZ', 'COD', 'WARTOŚĆ',
'NR REFERENCYJNY 1', 'NR REFERENCYJNY 2',
'ZAWARTOŚĆ', 'UWAGII',
]
fname = 'xbs-dpd-export-%s.csv' % (datetime.datetime.now().strftime(r'%Y%m%d-%H%M%S'),)
si = io.StringIO()
writer = csv.DictWriter(si, fields)
writer.writeheader()
config = current_app.config
for row in models:
for label_id in range(row.label_count):
writer.writerow({
'NAZWA NADAWCY': config['SHIPPING_SENDER_NAME'],
'OSOBA KONTAKTOWA NADAWCA': config['SHIPPING_SENDER_NAME2'],
'ULICA NADAWCA': '%s %s' % (config['SHIPPING_SENDER_STREET'], config['SHIPPING_SENDER_NUMBER']),
'KOD POCZTOWY NADAWCA': config['SHIPPING_SENDER_POSTALCODE'],
'MIASTO NADAWCA': config['SHIPPING_SENDER_CITY'],
'TELEFON KONTAKTOWY NADAWCA ': config['SHIPPING_SENDER_PHONE_NUMBER'],
'NAZWA ODBIORCA': row.shipping_name,
'OSOBA KONTAKTOWA ODBIORCA': row.full_name,
'TELEFON KONTAKTOWYODBIORCA': row.phone_number,
'ULICA ODBIORCA': row.shipping_street,
'KOD POCZTOWY ODBIORCA': row.shipping_postalcode,
'MIASTO ODBIORCA': row.shipping_city,
# TODO kartony?
'WAGA': 20,
'ILOŚĆ PACZEK': 1,
'NR REFERENCYJNY 1': '%s/%s/%s' % (row.id, label_id + 1, row.label_count),
'ZAWARTOŚĆ': 'przyłbice dla medyków',
})
row.changelog.append(RequestChange(
remarks='included on XBS export (%s)' % (fname,),
))
db.session.commit()
output = make_response(si.getvalue())
output.headers["Content-Disposition"] = "attachment; filename=%s" % (fname,)
output.headers["Content-type"] = "text/csv"
return output
@flask_admin.actions.action('shipping_create', 'Kurjerzy / Create shipment')
def action_shipping_create(self, ids):
from shipping.kurjerzy import Kurjerzy
models = self.get_query().filter(FaceshieldRequest.id.in_(ids)).all()
k = Kurjerzy(current_app)
k.authenticate()
created = 0
for model in models:
try:
if model.shipping_id is not None or model.shipping_provider is not None:
if k.shipment_info(model.shipping_id):
flash('Ignoring #%s (shipment already created)' % (model.id,), 'warning')
continue
k.create_shipment(model)
db.session.commit()
created += 1
except Exception as exc:
flash('Shipment creation failed for #%s: %s' % (model.id, exc), 'error')
return
if created:
flash('%d shipments created' % (created,), 'info')
@flask_admin.actions.action('shipping_print', 'Kurjerzy / Print shipping labels')
def action_shipping_print(self, ids):
models = self.get_query().filter(FaceshieldRequest.id.in_(ids)).all()
return render_pdf(HTML(string=self.render('shipping_label.html', models=models)))
class FilteredFaceshieldRequestAdmin(FaceshieldRequestAdmin):
def get_query(self):
return super(FilteredFaceshieldRequestAdmin, self).get_query().filter(~FaceshieldRequest.status.in_([Status.rejected, Status.spam, Status.fulfilled, Status.delegated]))
def get_count_query(self):
return super(FilteredFaceshieldRequestAdmin, self).get_count_query().filter(~FaceshieldRequest.status.in_([Status.rejected, Status.spam, Status.fulfilled, Status.delegated]))
class ShippingFaceshieldRequestAdmin(FilteredFaceshieldRequestAdmin):
column_editable_list = ('shipping_name', 'shipping_street', 'shipping_postalcode', 'shipping_city', 'shipping_latitude', 'shipping_longitude', 'status')
column_list = ['id', 'entity_info', 'full_name', *column_editable_list]
class ExternalUserAdmin(ModelViewHighSecurity):
column_default_sort = 'id'
column_list = ('id', 'email', 'password', 'remarks')
form_columns = ('email', 'password', 'remarks')
can_delete = True
class ChangelogAdmin(ModelView):
can_delete = False
can_edit = False
can_set_page_size = True
can_create = False
column_list = ('user_id', 'request', 'state_after', 'created')
column_filters = ('request_id', 'request', 'user_id', 'created')
column_default_sort = ('created', True)
admin.add_view(FilteredFaceshieldRequestAdmin(FaceshieldRequest, db.session))
admin.add_view(FaceshieldRequestAdmin(FaceshieldRequest, db.session, name='FaceshieldRequest (Unfiltered)', endpoint='request_unfiltered'))
admin.add_view(ShippingFaceshieldRequestAdmin(FaceshieldRequest, db.session, name='FaceshieldRequest (Shipping)', endpoint='request_shipping'))
admin.add_view(ExternalUserAdmin(ExternalUser, db.session, name='External Users', endpoint='external_user'))
admin.add_view(MapView(name='Map', endpoint='map'))
admin.add_view(ChangelogAdmin(RequestChange, db.session))