first release of comapny-data app

This commit is contained in:
2026-03-31 22:07:11 +02:00
parent c033456bf8
commit 2f34b9b8be
12 changed files with 1434 additions and 10 deletions
+22 -3
View File
@@ -1,4 +1,23 @@
Office,Invoice,Receipt,OrgId,TaxId,Organization,City,Street,Zip,Country
PL,A282_T000_260110_1,A28220260110102114,0,0,-customer not found-,,,,EU
HR,A100_T000_260126_1,A10020260126101017,,,Test,Zagreb,Test 12,10000,HR
BE,AN19_T000_260209_2,AN1920260104105115,,,,Heppen,Leopoldsburgsesteenweg 58,3971,BE
Office,Invoice,Receipt,OrgId,TaxId,Organization,City,Street,Zip,Country
HR,A275_T000_260205_5,A27520260110101225,94781370847,94781370847,DYNAMIC CARS,PREGRADA,JANKA LESKOVARA 69/2,49218,RH
BE,AN03_T000_260209_7,AN0320260104101097,,,,ixelles,rue des chevaliers 14,1050,BE
PL,A177_T000_260203_1,A17720260203101048,,,,,,,PL
PL,A282_T000_260225_1,A28220260225102015,,,,,,,PL
PL,A947_T000_260226_1,A94720260226102071,,,Sebastian Zawierucha,Wilczyn,15 Śwętne,62-550,PL
PL,A947_T000_260226_2,A94720260226102070,,,Sebastian Zawierucha,Wilczyn,Świętna 15,62-550,PL
BE,AN14_T000_260228_1,AN1420260228101013,,,,,,,BE
PL,A900_T000_260302_2,A90020260228102300,,,KONSTANCJA RACZKOWSKA,PIASECZNO,WARSZAWSKA 23/21,05-500,PL
PL,A109_T000_260304_1,A10920260304101111,,,DAWID HALCZAK,ZIELONA GORA,DUNIKOWSKIEGO 7,65-140,PL
PL,F061_T000_260304_1,F06120260304101051,,,Nicola Di Ciano,Kraków,Rakowicka 20J/173,31-510,PL
PL,A285_T000_260305_1,A28520260305101011,5651038584,5651038584,,sosnowica,ul. Parczewska 24,21-230,PL
PL,A285_T000_260305_3,A28520260305101015,5651038584,5651038584,,sosnowica,ul. Parczewska 24,21-230,PL
PL,A947_T000_260306_1,A94720260306102007,,,Sebastian Zawierucha,Wilczyn,Świętne 15,62-550,PL
BE,AN03_T000_260307_1,AN0320260209101068,,,,,,,BE
PL,A943_T000_260307_1,A94320260306104281,,,Larysa Zmura,SKARZEWY,KOŚCIERSKA 49,83-250,PL
BE,AN05_T000_260309_1,AN0520260217101041,0778435688,0778435688,Logopedie Silke ,Erpe-Mere,oudenaardsesteenweg 740 b1,9420,b
BE,AN05_T000_260309_2,AN0520260215101029,0759391719,0759391719,SRL Schiettecatte ,Ressaix,Rue Du Village 7,7134,b
BE,AN05_T000_260309_3,AN0520260308101101,0757920188,0757920188,LG Cars ,Lievegem,Grote Baan 5,9920,B
BE,AN05_T000_260309_5,AN0520260228101046,0778435688,0778435688,logopedie silke commv ,erpe-mere,oudenaardsesteenweg 740 b1,9420,b
PL,A286_T000_260310_1,A28620260310101031,,,,,,,PL
PL,A903_T000_260311_1,A90320260311102061,,,,,,,PL
1 Office Invoice Receipt OrgId TaxId Organization City Street Zip Country
2 PL Office A282_T000_260110_1 Invoice A28220260110102114 Receipt 0 OrgId 0 TaxId -customer not found- Organization City Street Zip EU Country
3 HR A100_T000_260126_1 A275_T000_260205_5 A10020260126101017 A27520260110101225 94781370847 94781370847 Test DYNAMIC CARS Zagreb PREGRADA Test 12 JANKA LESKOVARA 69/2 10000 49218 HR RH
4 BE AN19_T000_260209_2 AN03_T000_260209_7 AN1920260104105115 AN0320260104101097 Heppen ixelles Leopoldsburgsesteenweg 58 rue des chevaliers 14 3971 1050 BE
5 PL A177_T000_260203_1 A17720260203101048 PL
6 PL A282_T000_260225_1 A28220260225102015 PL
7 PL A947_T000_260226_1 A94720260226102071 Sebastian Zawierucha Wilczyn 15 Śwętne 62-550 PL
8 PL A947_T000_260226_2 A94720260226102070 Sebastian Zawierucha Wilczyn Świętna 15 62-550 PL
9 BE AN14_T000_260228_1 AN1420260228101013 BE
10 PL A900_T000_260302_2 A90020260228102300 KONSTANCJA RACZKOWSKA PIASECZNO WARSZAWSKA 23/21 05-500 PL
11 PL A109_T000_260304_1 A10920260304101111 DAWID HALCZAK ZIELONA GORA DUNIKOWSKIEGO 7 65-140 PL
12 PL F061_T000_260304_1 F06120260304101051 Nicola Di Ciano Kraków Rakowicka 20J/173 31-510 PL
13 PL A285_T000_260305_1 A28520260305101011 5651038584 5651038584 sosnowica ul. Parczewska 24 21-230 PL
14 PL A285_T000_260305_3 A28520260305101015 5651038584 5651038584 sosnowica ul. Parczewska 24 21-230 PL
15 PL A947_T000_260306_1 A94720260306102007 Sebastian Zawierucha Wilczyn Świętne 15 62-550 PL
16 BE AN03_T000_260307_1 AN0320260209101068 BE
17 PL A943_T000_260307_1 A94320260306104281 Larysa Zmura SKARZEWY KOŚCIERSKA 49 83-250 PL
18 BE AN05_T000_260309_1 AN0520260217101041 0778435688 0778435688 Logopedie Silke Erpe-Mere oudenaardsesteenweg 740 b1 9420 b
19 BE AN05_T000_260309_2 AN0520260215101029 0759391719 0759391719 SRL Schiettecatte Ressaix Rue Du Village 7 7134 b
20 BE AN05_T000_260309_3 AN0520260308101101 0757920188 0757920188 LG Cars Lievegem Grote Baan 5 9920 B
21 BE AN05_T000_260309_5 AN0520260228101046 0778435688 0778435688 logopedie silke commv erpe-mere oudenaardsesteenweg 740 b1 9420 b
22 PL A286_T000_260310_1 A28620260310101031 PL
23 PL A903_T000_260311_1 A90320260311102061 PL
+7 -7
View File
@@ -622,13 +622,13 @@ A968,Tommy Hilfiger Warsaw (Piaseczno) Outlet Temp,PL,1,0,
F05Z,Tommy Hilfiger Sosnowiec Designer Outlet,PL,1,0,
F061,Tommy Hilfiger Krakow (Designer Outlet),PL,1,1,
F062,Calvin Klein Krakow (Designer Outlet),PL,1,0,
A040,Calvin Klein Porto Outlet,PT,1,0,
A053,Calvin Klein Underwear Alcabideche (Cascai Shopping Mall),PT,1,0,
A059,Calvin Klein Underwear Lisbon (Vasco de Gama Mall),PT,1,0,
A065,Calvin Klein Jeans Alcochete (Freeport) Outlet,PT,1,0,
A064,Calvin Klein Underwear Alcochete (Freeport) Outlet,PT,1,0,
A063,Calvin Klein Jeans Loule (Algarve) Outlet,PT,1,0,
A1C1,Calvin Klein Porto (Norteshopping),PT,1,0,
A040,Calvin Klein Porto Outlet,PT,,0,
A053,Calvin Klein Underwear Alcabideche (Cascai Shopping Mall),PT,,0,
A059,Calvin Klein Underwear Lisbon (Vasco de Gama Mall),PT,,0,
A065,Calvin Klein Jeans Alcochete (Freeport) Outlet,PT,,0,
A064,Calvin Klein Underwear Alcochete (Freeport) Outlet,PT,,0,
A063,Calvin Klein Jeans Loule (Algarve) Outlet,PT,,0,
A1C1,Calvin Klein Porto (Norteshopping),PT,,0,
AS04,Tommy Hilfiger Taby,SE,,0,
AA26,Tommy Hilfiger Stockholm (Bibliotek),SE,,0,
AS07,Tommy Hilfiger Jarfalla (Barkarby) Outlet,SE,,0,
1 store name country invoice found new
622 F05Z Tommy Hilfiger Sosnowiec Designer Outlet PL 1 0
623 F061 Tommy Hilfiger Krakow (Designer Outlet) PL 1 1
624 F062 Calvin Klein Krakow (Designer Outlet) PL 1 0
625 A040 Calvin Klein Porto Outlet PT 1 0
626 A053 Calvin Klein Underwear Alcabideche (Cascai Shopping Mall) PT 1 0
627 A059 Calvin Klein Underwear Lisbon (Vasco de Gama Mall) PT 1 0
628 A065 Calvin Klein Jeans Alcochete (Freeport) Outlet PT 1 0
629 A064 Calvin Klein Underwear Alcochete (Freeport) Outlet PT 1 0
630 A063 Calvin Klein Jeans Loule (Algarve) Outlet PT 1 0
631 A1C1 Calvin Klein Porto (Norteshopping) PT 1 0
632 AS04 Tommy Hilfiger Taby SE 0
633 AA26 Tommy Hilfiger Stockholm (Bibliotek) SE 0
634 AS07 Tommy Hilfiger Jarfalla (Barkarby) Outlet SE 0
+211
View File
@@ -0,0 +1,211 @@
from datetime import date
import re
import requests
from flask import Flask, jsonify, render_template, request
from requests import Session
from zeep import Client
from zeep.transports import Transport
app = Flask(__name__)
VIES_WSDL_URL = "https://ec.europa.eu/taxation_customs/vies/services/checkVatService.wsdl"
POLISH_WL_API = "https://wl-api.mf.gov.pl/api/search/nip/{nip}?date={query_date}"
def get_vies_client():
session = Session()
transport = Transport(session=session, timeout=20)
return Client(wsdl=VIES_WSDL_URL, transport=transport)
def normalize_vat(country_code: str, vat_number: str):
country_code = (country_code or "").strip().upper()
vat_number = (vat_number or "").strip().replace(" ", "")
if vat_number.upper().startswith(country_code):
vat_number = vat_number[len(country_code):]
return country_code, vat_number
def parse_polish_working_address(address: str):
if not address:
return "", "", ""
address = " ".join(address.split())
match = re.match(r"^(.*?)(\d{2}-\d{3})\s+(.+)$", address)
if match:
street = match.group(1).strip(" ,")
zip_code = match.group(2).strip()
city = match.group(3).strip()
return street, zip_code, city
return address, "", ""
def parse_vies_address(address: str):
"""
Assumption for VIES address:
- address is comma-separated
- first part = street
- last part = ZIP + CITY
Examples:
"Main Street 12, Some Region, 1000 Brussels"
"ul. Prosta 12, Warszawa, 00-838 Warszawa"
"Ilica 1, Zagreb, HR-10000 Zagreb"
"""
if not address:
return "", "", ""
parts = [part.strip() for part in address.split(",") if part.strip()]
if not parts:
return "", "", ""
street = parts[0]
last_part = parts[-1]
# Examples:
# 1000 Brussels
# 00-838 Warszawa
# HR-10000 Zagreb
match = re.match(r"^([A-Z]{0,2}-?\d[\dA-Z -]*)\s+(.+)$", last_part)
if match:
zip_code = match.group(1).strip()
city = match.group(2).strip()
return street, zip_code, city
return street, "", last_part
def check_vies(country_code: str, vat_number: str):
client = get_vies_client()
result = client.service.checkVat(countryCode=country_code, vatNumber=vat_number)
name = (result.name or "").strip()
address = (result.address or "").strip()
street, zip_code, city = parse_vies_address(address)
return {
"source": "vies",
"found": bool(result.valid),
"country": country_code,
"vat_number": vat_number,
"name": name if result.valid else "",
"street": street if result.valid else "",
"zip": zip_code if result.valid else "",
"city": city if result.valid else "",
"raw_address": address if result.valid else "",
}
def check_polish_wl(nip: str):
today = date.today().isoformat()
url = POLISH_WL_API.format(nip=nip, query_date=today)
response = requests.get(url, headers={"Accept": "application/json"}, timeout=20)
response.raise_for_status()
data = response.json()
result = data.get("result", {})
subject = result.get("subject")
if not subject:
subjects = result.get("subjects") or []
subject = subjects[0] if subjects else None
if not subject:
return {
"source": "polish_wl",
"found": False,
"country": "PL",
"vat_number": nip,
"name": "",
"street": "",
"zip": "",
"city": "",
"raw_address": "",
}
name = (subject.get("name") or "").strip()
working_address = (subject.get("workingAddress") or "").strip()
street, zip_code, city = parse_polish_working_address(working_address)
return {
"source": "polish_wl",
"found": True,
"country": "PL",
"vat_number": nip,
"name": name,
"street": street,
"zip": zip_code,
"city": city,
"raw_address": working_address,
}
@app.get("/")
def index():
return render_template("index.html")
@app.get("/api/company_lookup")
def company_lookup():
country = request.args.get("country", "")
vat_number = request.args.get("vat_number", "")
country, vat_number = normalize_vat(country, vat_number)
if not country or not vat_number:
return jsonify({"error": "country and vat_number are required"}), 400
try:
vies_result = check_vies(country, vat_number)
except Exception as exc:
vies_result = {
"source": "vies",
"found": False,
"country": country,
"vat_number": vat_number,
"name": "",
"street": "",
"zip": "",
"city": "",
"raw_address": "",
"warning": f"VIES lookup failed: {exc}",
}
if vies_result.get("found"):
return jsonify(vies_result)
if country == "PL":
try:
polish_result = check_polish_wl(vat_number)
if "warning" in vies_result:
polish_result["warning"] = vies_result["warning"]
return jsonify(polish_result)
except Exception as exc:
return jsonify(
{
"source": "polish_wl",
"found": False,
"country": country,
"vat_number": vat_number,
"name": "",
"street": "",
"zip": "",
"city": "",
"raw_address": "",
"warning": vies_result.get("warning"),
"fallback_error": f"Polish WL lookup failed: {exc}",
}
)
return jsonify(vies_result)
if __name__ == "__main__":
app.run(debug=True)
+1
View File
@@ -206,6 +206,7 @@ def write_repaired_csv(rows):
writer.writerows(rows)
def write_broken_csv(header, rows):
print("write broken")
with open(BUYERS_BROKEN, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(header)
+484
View File
@@ -0,0 +1,484 @@
from flask import Flask, request, jsonify, render_template_string
from datetime import date
import re
import requests
from requests import Session
from zeep import Client
from zeep.transports import Transport
app = Flask(__name__)
VIES_WSDL_URL = "https://ec.europa.eu/taxation_customs/vies/services/checkVatService.wsdl"
POLISH_WL_API = "https://wl-api.mf.gov.pl/api/search/nip/{nip}?date={query_date}"
def get_vies_client():
session = Session()
transport = Transport(session=session, timeout=20)
return Client(wsdl=VIES_WSDL_URL, transport=transport)
def normalize_vat(country_code: str, vat_number: str):
country_code = (country_code or "").strip().upper()
vat_number = (vat_number or "").strip().replace(" ", "")
if vat_number.upper().startswith(country_code):
vat_number = vat_number[len(country_code):]
return country_code, vat_number
def split_zip_city(text: str):
if not text:
return "", ""
text = " ".join(text.split())
m = re.match(r"^(\d{2}-\d{3})\s+(.+)$", text)
if m:
return m.group(1), m.group(2)
return "", text
def parse_polish_working_address(address: str):
if not address:
return "", "", ""
address = " ".join(address.split())
m = re.match(r"^(.*?)(\d{2}-\d{3})\s+(.+)$", address)
if m:
street = m.group(1).strip(" ,")
zip_code = m.group(2).strip()
city = m.group(3).strip()
return street, zip_code, city
return address, "", ""
def parse_vies_address(address: str):
if not address:
return "", "", ""
parts = [p.strip() for p in address.split(",") if p.strip()]
if not parts:
return "", "", ""
street = parts[0]
last_part = parts[-1]
# Common cases:
# "1000 Brussels"
# "00-838 Warszawa"
# "HR-10000 Zagreb"
m = re.match(r"^([A-Z]{0,2}-?\d[\dA-Z -]*)\s+(.+)$", last_part)
if m:
zip_code = m.group(1).strip()
city = m.group(2).strip()
return street, zip_code, city
return street, "", last_part
def check_vies(country_code: str, vat_number: str):
client = get_vies_client()
result = client.service.checkVat(countryCode=country_code, vatNumber=vat_number)
name = (result.name or "").strip()
address = (result.address or "").strip()
street, zip_code, city = parse_vies_address(address)
return {
"source": "vies",
"found": bool(result.valid),
"country": country_code,
"vat_number": vat_number,
"name": name if result.valid else "",
"street": street if result.valid else "",
"zip": zip_code if result.valid else "",
"city": city if result.valid else "",
"raw_address": address if result.valid else "",
}
def check_polish_wl(nip: str):
today = date.today().isoformat()
url = POLISH_WL_API.format(nip=nip, query_date=today)
response = requests.get(url, headers={"Accept": "application/json"}, timeout=20)
response.raise_for_status()
data = response.json()
result = data.get("result", {})
subject = result.get("subject")
if not subject:
subjects = result.get("subjects") or []
subject = subjects[0] if subjects else None
if not subject:
return {
"source": "polish_wl",
"found": False,
"country": "PL",
"vat_number": nip,
"name": "",
"street": "",
"zip": "",
"city": "",
"raw_address": "",
}
name = (subject.get("name") or "").strip()
working_address = (subject.get("workingAddress") or "").strip()
street, zip_code, city = parse_polish_working_address(working_address)
return {
"source": "polish_wl",
"found": True,
"country": "PL",
"vat_number": nip,
"name": name,
"street": street,
"zip": zip_code,
"city": city,
"raw_address": working_address,
}
@app.get("/api/company_lookup")
def company_lookup():
country = request.args.get("country", "")
vat_number = request.args.get("vat_number", "")
country, vat_number = normalize_vat(country, vat_number)
if not country or not vat_number:
return jsonify({"error": "country and vat_number are required"}), 400
try:
vies_result = check_vies(country, vat_number)
except Exception as e:
vies_result = {
"source": "vies",
"found": False,
"country": country,
"vat_number": vat_number,
"name": "",
"street": "",
"zip": "",
"city": "",
"raw_address": "",
"warning": f"VIES lookup failed: {e}",
}
if vies_result.get("found"):
return jsonify(vies_result)
if country == "PL":
try:
polish_result = check_polish_wl(vat_number)
if "warning" in vies_result:
polish_result["warning"] = vies_result["warning"]
return jsonify(polish_result)
except Exception as e:
response = {
"source": "polish_wl",
"found": False,
"country": country,
"vat_number": vat_number,
"name": "",
"street": "",
"zip": "",
"city": "",
"raw_address": "",
"warning": vies_result.get("warning"),
"fallback_error": f"Polish WL lookup failed: {e}",
}
return jsonify(response)
return jsonify(vies_result)
HTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VAT Company Lookup</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
:root {
--bg1: #eef6ff;
--bg2: #dcecff;
--panel: rgba(255, 255, 255, 0.9);
--blue: #1f5fae;
--blue-dark: #123d73;
--blue-soft: #6fa6e8;
--line: #4d8fdb;
--text: #1a2f4d;
--muted: #4f6f92;
--shadow: 0 18px 50px rgba(20, 71, 132, 0.16);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: 'Inter', sans-serif;
font-size: 24px;
color: var(--text);
background:
radial-gradient(circle at top left, #f7fbff 0%, transparent 32%),
linear-gradient(135deg, var(--bg1), var(--bg2));
display: flex;
justify-content: center;
align-items: flex-start;
padding: 48px 24px;
}
.container {
width: 100%;
max-width: 1100px;
background: var(--panel);
backdrop-filter: blur(8px);
border-radius: 28px;
box-shadow: var(--shadow);
padding: 40px 48px;
}
h1 {
margin: 0 0 36px 0;
color: var(--blue-dark);
font-size: 34px;
font-weight: 700;
letter-spacing: -0.5px;
}
.form-grid {
display: flex;
flex-direction: column;
gap: 28px;
}
.row {
display: grid;
grid-template-columns: 220px 1fr 1fr auto;
gap: 20px;
align-items: end;
}
label {
font-weight: 600;
color: var(--blue);
padding-bottom: 8px;
}
input, select {
width: 100%;
font-family: inherit;
font-size: 24px;
color: var(--text);
padding: 10px 4px;
border: none;
border-bottom: 2px solid var(--blue-soft);
background: transparent;
outline: none;
transition: border-color 0.2s ease;
border-radius: 0;
}
input:focus, select:focus {
border-bottom-color: var(--blue);
box-shadow: none;
}
input::placeholder {
color: #7f9fc4;
}
button {
font-family: inherit;
font-size: 24px;
font-weight: 700;
padding: 14px 24px;
border: none;
border-radius: 16px;
cursor: pointer;
background: linear-gradient(135deg, #3b82d6, #1f5fae);
color: white;
box-shadow: 0 10px 24px rgba(31, 95, 174, 0.24);
transition: transform 0.12s ease, box-shadow 0.12s ease;
white-space: nowrap;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 14px 28px rgba(31, 95, 174, 0.28);
}
button:active {
transform: translateY(0);
}
.button-row {
margin-top: 36px;
}
.status {
margin-top: 28px;
padding: 20px 24px;
border-radius: 18px;
background: linear-gradient(135deg, rgba(204, 227, 255, 0.7), rgba(232, 243, 255, 0.95));
color: var(--muted);
white-space: pre-wrap;
font-size: 22px;
border: 1px solid rgba(77, 143, 219, 0.18);
}
@media (max-width: 900px) {
body {
padding: 20px;
}
.container {
padding: 28px 24px;
}
.row {
grid-template-columns: 1fr;
gap: 14px;
}
h1 {
font-size: 30px;
}
input, select, button {
font-size: 20px;
}
.status {
font-size: 18px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>VAT Company Lookup</h1>
<div class="form-grid">
<div class="row">
<label for="country">Country / VAT</label>
<select id="country">
<option value="BE">Belgium</option>
<option value="HR">Croatia</option>
<option value="PL">Poland</option>
</select>
<input id="vat_number" type="text" placeholder="VAT number">
<button type="button" onclick="searchCompany()">Search</button>
</div>
<div class="row">
<label for="company_name">Company</label>
<input id="company_name" type="text" style="grid-column: span 3;">
</div>
<div class="row">
<label for="street">Street</label>
<input id="street" type="text" style="grid-column: span 3;">
</div>
<div class="row">
<label for="zip">ZIP / City</label>
<input id="zip" type="text" placeholder="ZIP">
<input id="city" type="text" placeholder="City">
<div></div>
</div>
<div class="row">
<label for="email">Email</label>
<input id="email" type="email" placeholder="example@company.com" style="grid-column: span 3;">
</div>
<div class="row">
<label for="phone">Phone</label>
<input id="phone" type="text" placeholder="+32 123 45 67 89" style="grid-column: span 3;">
</div>
</div>
<div class="button-row">
<button type="button" onclick="saveData()">Save</button>
</div>
<div id="status" class="status">Ready.</div>
</div>
<script>
async function searchCompany() {
const country = document.getElementById("country").value;
const vatNumber = document.getElementById("vat_number").value.trim();
const status = document.getElementById("status");
if (!country || !vatNumber) {
status.textContent = "Please enter a country and VAT number.";
return;
}
status.textContent = "Searching...";
try {
const response = await fetch(`/api/company_lookup?country=${encodeURIComponent(country)}&vat_number=${encodeURIComponent(vatNumber)}`);
const data = await response.json();
if (!response.ok) {
status.textContent = data.error || "Lookup failed.";
return;
}
document.getElementById("company_name").value = data.name || "";
document.getElementById("street").value = data.street || "";
document.getElementById("zip").value = data.zip || "";
document.getElementById("city").value = data.city || "";
let msg = data.found ? `Found via ${data.source}.` : "No company found.";
if (data.warning) {
msg += `\\nWarning: ${data.warning}`;
}
if (data.fallback_error) {
msg += `\\nFallback error: ${data.fallback_error}`;
}
status.textContent = msg;
} catch (err) {
status.textContent = "Request failed: " + err;
}
}
function saveData() {
const payload = {
country: document.getElementById("country").value,
vat_number: document.getElementById("vat_number").value.trim(),
company_name: document.getElementById("company_name").value.trim(),
street: document.getElementById("street").value.trim(),
zip: document.getElementById("zip").value.trim(),
city: document.getElementById("city").value.trim(),
email: document.getElementById("email").value.trim(),
phone: document.getElementById("phone").value.trim()
};
alert(JSON.stringify(payload, null, 2));
}
</script>
</body>
</html>
"""
@app.get("/")
def index():
return render_template_string(HTML)
if __name__ == "__main__":
app.run(debug=True)
+322
View File
@@ -0,0 +1,322 @@
PL 5252312380
PL 5223326818
PL 8371691451
PL 5211443926
PL 7122479987
PL 8351517715
PL 5272175093
PL 7010249294
PL 6812064950
PL 6282094698
PL 8641689330
PL 5862342505
PL 9510057883
PL 1133120152
PL 5223184063
PL 9581642778
PL 8411738355
PL 6441555115
PL 9372177136
PL 7123099979
PL 8621392710
PL 5651038584
PL 7831864882
PL 7882035701
PL 8871814655
PL 8992374497
PL 5130177811
PL 6751770109
PL 6282097188
PL 7262441743
PL 8511216828
PL 8541114564
PL 9551436838
PL 1251621119
PL 5242928346
PL 5240104641
PL 5252094977
PL 5662003112
PL 8212542282
PL 9511645369
PL 5342647606
PL 5110239532
PL 5221834918
PL 5223340020
PL 7792426792
PL 5223151715
PL 7010343395
PL 1181403221
PL 5213197319
PL 5961650279
PL 8961588694
PL 9131583969
PL 8943170228
PL 8992475577
PL 5882226788
PL 9581352827
PL 5891987110
PL 5892044936
PL 8410009967
PL 7861723608
PL 8733292391
PL 2220899222
PL 8871164128
PL 6040251786
PL 8272088792
PL 6772012242
BE 0405746743
BE 0437809005
BE 0444799636
BE 0445378567
BE 0449272128
BE 0458131196
BE 0458131196
BE 0460534224
BE 0460549862
BE 0460549862
BE 0460549862
BE 0468813074
BE 0471642506
BE 0473641102
BE 0475548042
BE 0476123411
BE 0477477352
BE 0479650845
BE 0499827053
BE 0525863031
BE 0533876122
BE 0536446127
BE 0542742120
BE 0550572493
BE 0553907117
BE 0553907117
BE 0633976459
BE 0644635868
BE 0653792371
BE 0659760445
BE 0663823557
BE 0664458512
BE 0670639489
BE 0671768847
BE 0673369743
BE 0689926455
BE 0695819701
BE 0700748388
BE 0718628458
BE 0724901487
BE 0729882240
BE 0735796468
BE 0740903618
BE 0742676738
BE 0742676738
BE 0744509444
BE 0746634041
BE 0748850193
BE 0749451494
BE 0753533711
BE 0754867658
BE 0756959591
BE 0757920188
BE 0758855546
BE 0759391719
BE 0764330009
BE 0768682141
BE 0769935223
BE 0771370922
BE 0776737297
BE 0778435688
BE 0778435688
BE 0780604926
BE 0783770589
BE 0784408712
BE 0786255868
BE 0787736010
BE 0788033443
BE 0788179141
BE 0790494075
BE 0793289556
BE 0793416943
BE 0795338731
BE 0797647529
BE 0802150408
BE 0802342032
BE 0806156607
BE 0806463245
BE 0807677428
BE 0809707795
BE 0810583072
BE 0811250392
BE 0811250392
BE 0819258040
BE 0834659561
BE 0839865491
BE 0842831812
BE 0843094306
BE 0843720747
BE 0849197980
BE 0889235226
BE 0889235226
BE 0889235226
BE 0890993005
BE 0894104329
BE 0894448183
BE 0897169826
BE 0899546920
BE 0899736168
BE 1000196395
BE 1000967150
BE 1001431067
BE 1002587545
BE 1002587545
BE 1002587545
BE 1003658703
BE 1004502207
BE 1004652358
BE 1010102570
BE 1012166492
BE 1014616238
BE 1014616238
BE 1014729866
BE 1015634045
BE 1016426673
BE 1016600679
BE 1016600679
BE 1017649368
BE 1018214146
BE 1018699146
BE 1019933719
BE 1021425737
BE 1021957851
BE 1025453217
BE 1027367481
BE 1029067060
BE 1030172464
HR 01554625423
HR 02040448967
HR 08129951577
HR 09710116594
HR 09710116594
HR 10663663549
HR 10663663549
HR 10757059293
HR 11377032066
HR 11642311869
HR 11642311869
HR 14069513398
HR 14299551051
HR 14299551051
HR 18629637231
HR 24343131061
HR 29249157622
HR 33789366096
HR 33789366096
HR 34734835141
HR 35328808373
HR 39336270727
HR 39336270727
HR 40547768143
HR 42003905178
HR 42003905178
HR 43644971818
HR 43644971818
HR 43644971818
HR 43966367762
HR 45561404814
HR 46193476875
HR 52452475872
HR 53674250410
HR 53674250410
HR 53674250410
HR 53674250410
HR 53674250410
HR 53674250410
HR 53747311795
HR 56211904821
HR 56338510000
HR 56338510000
HR 58921039473
HR 63112900674
HR 63991444504
HR 64150104602
HR 64278511473
HR 64797210995
HR 66170601459
HR 72383284772
HR 76945714039
HR 79492122719
HR 79492122719
HR 85476136549
HR 87143125889
HR 87856102906
HR 88283563670
HR 89273653827
HR 89668863546
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 90656275879
HR 93823575846
HR 93823575846
HR 97003245557
HR 99087306845
+3
View File
@@ -0,0 +1,3 @@
PL 5252312380
BE 0718628458
HR 56211904821
+2
View File
@@ -80,8 +80,10 @@ def get_elm_value(tree, name, default=''):
return default
def dump_buyer_info(buyer, invoice_ref, receipt_ref, p_country):
print("dump buyers")
if buyer:
message = f"{p_country},{invoice_ref},{receipt_ref},{buyer.orgid},{buyer.taxID},{buyer.name},{buyer.city},{buyer.street1},{buyer.zip},{buyer.country}\n"
print(message)
with open(BUYERS_BROKEN, "a", encoding="utf-8") as lfile:
lfile.write(message)
+90
View File
@@ -981,3 +981,93 @@
2026-02-24 11:28:50.270849 - INFO Receipt AN1920260120103075: Avoid rounding of -0.14 by updating max 88 articles.
2026-02-24 11:28:50.289230 - INFO Receipt AN1920260120103074: Avoid rounding of -0.08 by updating max 85 articles.
2026-02-24 11:28:50.308309 - INFO Receipt AN1920260201102083: Avoid rounding of 0.02 by updating max 8 articles.
2026-03-12 11:43:16.877496 - WARNING Invoice AN02_T000_260203_1: Buyer country XX does not match company country BE
2026-03-12 11:43:16.939878 - ERROR No linked receipt file found for A100_T101_260121_30.xml
2026-03-12 11:45:35.070905 - WARNING Invoice AN02_T000_260203_1: Buyer country XX does not match company country BE
2026-03-12 11:45:35.108581 - ERROR No linked receipt file found for A100_T101_260121_30.xml
2026-03-12 11:45:35.280810 - INFO Receipt A10020260103101188: Avoid rounding of -0.01 by updating max 3 articles.
2026-03-12 11:45:35.290898 - INFO Receipt A10020260115101212: Avoid rounding of -0.01 by updating max 4 articles.
2026-03-12 11:45:35.332701 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 11:45:35.345416 - ERROR No linked receipt file found for AN19_T102_260111_101.xml
2026-03-12 11:45:35.350302 - ERROR No linked receipt file found for AN19_T102_260111_122.xml
2026-03-12 11:45:35.354657 - ERROR No linked receipt file found for AN19_T102_260111_117.xml
2026-03-12 11:45:35.358474 - ERROR No linked receipt file found for AN19_T103_260111_98.xml
2026-03-12 11:45:35.362851 - ERROR No linked receipt file found for AN19_T103_260111_123.xml
2026-03-12 11:45:35.367026 - ERROR No linked receipt file found for AN19_T102_260113_87.xml
2026-03-12 11:45:35.371114 - ERROR No linked receipt file found for AN19_T101_260115_80.xml
2026-03-12 11:45:35.377378 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 11:45:35.383681 - ERROR No linked receipt file found for AN19_T101_260104_148.xml
2026-03-12 11:45:35.388161 - ERROR No linked receipt file found for AN19_T104_260103_188.xml
2026-03-12 11:45:35.392386 - ERROR No linked receipt file found for AN19_T101_260103_105.xml
2026-03-12 11:45:35.396871 - ERROR No linked receipt file found for AN19_T103_260102_255.xml
2026-03-12 11:45:35.402013 - ERROR No linked receipt file found for AN19_T102_260108_110.xml
2026-03-12 11:45:35.411347 - ERROR No linked receipt file found for AN19_T104_260110_78.xml
2026-03-12 11:45:35.493328 - INFO Receipt AN1920260120103075: Avoid rounding of -0.14 by updating max 88 articles.
2026-03-12 11:45:35.514824 - INFO Receipt AN1920260120103074: Avoid rounding of -0.08 by updating max 85 articles.
2026-03-12 11:45:35.541657 - INFO Receipt AN1920260201102083: Avoid rounding of 0.02 by updating max 8 articles.
2026-03-12 11:46:33.848100 - WARNING Invoice AN02_T000_260203_1: Buyer country XX does not match company country BE
2026-03-12 11:46:33.880421 - ERROR No linked receipt file found for A100_T101_260121_30.xml
2026-03-12 11:46:33.972187 - INFO Receipt A10020260103101188: Avoid rounding of -0.01 by updating max 3 articles.
2026-03-12 11:46:33.981501 - INFO Receipt A10020260115101212: Avoid rounding of -0.01 by updating max 4 articles.
2026-03-12 11:46:34.014710 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 11:46:34.023193 - ERROR No linked receipt file found for AN19_T102_260111_101.xml
2026-03-12 11:46:34.026756 - ERROR No linked receipt file found for AN19_T102_260111_122.xml
2026-03-12 11:46:34.030094 - ERROR No linked receipt file found for AN19_T102_260111_117.xml
2026-03-12 11:46:34.032757 - ERROR No linked receipt file found for AN19_T103_260111_98.xml
2026-03-12 11:46:34.035798 - ERROR No linked receipt file found for AN19_T103_260111_123.xml
2026-03-12 11:46:34.038968 - ERROR No linked receipt file found for AN19_T102_260113_87.xml
2026-03-12 11:46:34.041433 - ERROR No linked receipt file found for AN19_T101_260115_80.xml
2026-03-12 11:46:34.047107 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 11:46:34.052946 - ERROR No linked receipt file found for AN19_T101_260104_148.xml
2026-03-12 11:46:34.055801 - ERROR No linked receipt file found for AN19_T104_260103_188.xml
2026-03-12 11:46:34.058460 - ERROR No linked receipt file found for AN19_T101_260103_105.xml
2026-03-12 11:46:34.061759 - ERROR No linked receipt file found for AN19_T103_260102_255.xml
2026-03-12 11:46:34.065481 - ERROR No linked receipt file found for AN19_T102_260108_110.xml
2026-03-12 11:46:34.070597 - ERROR No linked receipt file found for AN19_T104_260110_78.xml
2026-03-12 11:46:34.131931 - INFO Receipt AN1920260120103075: Avoid rounding of -0.14 by updating max 88 articles.
2026-03-12 11:46:34.151978 - INFO Receipt AN1920260120103074: Avoid rounding of -0.08 by updating max 85 articles.
2026-03-12 11:46:34.173302 - INFO Receipt AN1920260201102083: Avoid rounding of 0.02 by updating max 8 articles.
2026-03-12 12:23:15.765003 - WARNING Invoice AN02_T000_260203_1: Buyer country XX does not match company country BE
2026-03-12 12:23:15.822124 - ERROR No linked receipt file found for A100_T101_260121_30.xml
2026-03-12 12:23:15.978224 - INFO Receipt A10020260103101188: Avoid rounding of -0.01 by updating max 3 articles.
2026-03-12 12:23:15.992007 - INFO Receipt A10020260115101212: Avoid rounding of -0.01 by updating max 4 articles.
2026-03-12 12:23:16.046791 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 12:23:16.060196 - ERROR No linked receipt file found for AN19_T102_260111_101.xml
2026-03-12 12:23:16.072034 - ERROR No linked receipt file found for AN19_T102_260111_122.xml
2026-03-12 12:23:16.084611 - ERROR No linked receipt file found for AN19_T102_260111_117.xml
2026-03-12 12:23:16.092492 - ERROR No linked receipt file found for AN19_T103_260111_98.xml
2026-03-12 12:23:16.097356 - ERROR No linked receipt file found for AN19_T103_260111_123.xml
2026-03-12 12:23:16.103395 - ERROR No linked receipt file found for AN19_T102_260113_87.xml
2026-03-12 12:23:16.109512 - ERROR No linked receipt file found for AN19_T101_260115_80.xml
2026-03-12 12:23:16.117200 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 12:23:16.123065 - ERROR No linked receipt file found for AN19_T101_260104_148.xml
2026-03-12 12:23:16.128018 - ERROR No linked receipt file found for AN19_T104_260103_188.xml
2026-03-12 12:23:16.132909 - ERROR No linked receipt file found for AN19_T101_260103_105.xml
2026-03-12 12:23:16.137598 - ERROR No linked receipt file found for AN19_T103_260102_255.xml
2026-03-12 12:23:16.142263 - ERROR No linked receipt file found for AN19_T102_260108_110.xml
2026-03-12 12:23:16.152216 - ERROR No linked receipt file found for AN19_T104_260110_78.xml
2026-03-12 12:23:16.244792 - INFO Receipt AN1920260120103075: Avoid rounding of -0.14 by updating max 88 articles.
2026-03-12 12:23:16.269411 - INFO Receipt AN1920260120103074: Avoid rounding of -0.08 by updating max 85 articles.
2026-03-12 12:23:16.294935 - INFO Receipt AN1920260201102083: Avoid rounding of 0.02 by updating max 8 articles.
2026-03-12 12:24:10.319982 - WARNING Invoice AN02_T000_260203_1: Buyer country XX does not match company country BE
2026-03-12 12:24:10.357773 - ERROR No linked receipt file found for A100_T101_260121_30.xml
2026-03-12 12:24:10.455483 - INFO Receipt A10020260103101188: Avoid rounding of -0.01 by updating max 3 articles.
2026-03-12 12:24:10.466522 - INFO Receipt A10020260115101212: Avoid rounding of -0.01 by updating max 4 articles.
2026-03-12 12:24:10.499786 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 12:24:10.508145 - ERROR No linked receipt file found for AN19_T102_260111_101.xml
2026-03-12 12:24:10.511826 - ERROR No linked receipt file found for AN19_T102_260111_122.xml
2026-03-12 12:24:10.514475 - ERROR No linked receipt file found for AN19_T102_260111_117.xml
2026-03-12 12:24:10.517296 - ERROR No linked receipt file found for AN19_T103_260111_98.xml
2026-03-12 12:24:10.520011 - ERROR No linked receipt file found for AN19_T103_260111_123.xml
2026-03-12 12:24:10.522573 - ERROR No linked receipt file found for AN19_T102_260113_87.xml
2026-03-12 12:24:10.525808 - ERROR No linked receipt file found for AN19_T101_260115_80.xml
2026-03-12 12:24:10.530370 - ERROR receipt AN1920260104105115: buyer has insufficient details - repair before export
2026-03-12 12:24:10.534451 - ERROR No linked receipt file found for AN19_T101_260104_148.xml
2026-03-12 12:24:10.537367 - ERROR No linked receipt file found for AN19_T104_260103_188.xml
2026-03-12 12:24:10.540454 - ERROR No linked receipt file found for AN19_T101_260103_105.xml
2026-03-12 12:24:10.543686 - ERROR No linked receipt file found for AN19_T103_260102_255.xml
2026-03-12 12:24:10.546001 - ERROR No linked receipt file found for AN19_T102_260108_110.xml
2026-03-12 12:24:10.550573 - ERROR No linked receipt file found for AN19_T104_260110_78.xml
2026-03-12 12:24:10.609768 - INFO Receipt AN1920260120103075: Avoid rounding of -0.14 by updating max 88 articles.
2026-03-12 12:24:10.629560 - INFO Receipt AN1920260120103074: Avoid rounding of -0.08 by updating max 85 articles.
2026-03-12 12:24:10.649139 - INFO Receipt AN1920260201102083: Avoid rounding of 0.02 by updating max 8 articles.
+59
View File
@@ -0,0 +1,59 @@
async function searchCompany() {
const country = document.getElementById("country").value;
const vatNumber = document.getElementById("vat_number").value.trim();
const status = document.getElementById("status");
if (!country || !vatNumber) {
status.textContent = "Please enter a country and VAT number.";
return;
}
status.textContent = "Searching...";
try {
const response = await fetch(
`/api/company_lookup?country=${encodeURIComponent(country)}&vat_number=${encodeURIComponent(vatNumber)}`
);
const data = await response.json();
if (!response.ok) {
status.textContent = data.error || "Lookup failed.";
return;
}
document.getElementById("company_name").value = data.name || "";
document.getElementById("street").value = data.street || "";
document.getElementById("zip").value = data.zip || "";
document.getElementById("city").value = data.city || "";
let message = data.found ? `Found via ${data.source}.` : "No company found.";
if (data.warning) {
message += `\nWarning: ${data.warning}`;
}
if (data.fallback_error) {
message += `\nFallback error: ${data.fallback_error}`;
}
status.textContent = message;
} catch (error) {
status.textContent = "Request failed: " + error;
}
}
function saveData() {
const payload = {
country: document.getElementById("country").value,
vat_number: document.getElementById("vat_number").value.trim(),
company_name: document.getElementById("company_name").value.trim(),
street: document.getElementById("street").value.trim(),
zip: document.getElementById("zip").value.trim(),
city: document.getElementById("city").value.trim(),
email: document.getElementById("email").value.trim(),
phone: document.getElementById("phone").value.trim()
};
alert(JSON.stringify(payload, null, 2));
}
+171
View File
@@ -0,0 +1,171 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
:root {
--bg1: #eef6ff;
--bg2: #dcecff;
--panel: rgba(255, 255, 255, 0.9);
--blue: #1f5fae;
--blue-dark: #123d73;
--blue-soft: #6fa6e8;
--line: #4d8fdb;
--text: #1a2f4d;
--muted: #4f6f92;
--shadow: 0 18px 50px rgba(20, 71, 132, 0.16);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: 'Inter', sans-serif;
font-size: 24px;
color: var(--text);
background:
radial-gradient(circle at top left, #f7fbff 0%, transparent 32%),
linear-gradient(135deg, var(--bg1), var(--bg2));
display: flex;
justify-content: center;
align-items: flex-start;
padding: 48px 24px;
}
.container {
width: 100%;
max-width: 1100px;
background: var(--panel);
backdrop-filter: blur(8px);
border-radius: 28px;
box-shadow: var(--shadow);
padding: 40px 48px;
}
h1 {
margin: 0 0 36px 0;
color: var(--blue-dark);
font-size: 34px;
font-weight: 700;
letter-spacing: -0.5px;
}
.form-grid {
display: flex;
flex-direction: column;
gap: 28px;
}
.row {
display: grid;
grid-template-columns: 220px 1fr 1fr auto;
gap: 20px;
align-items: end;
}
label {
font-weight: 600;
color: var(--blue);
padding-bottom: 8px;
}
input,
select {
width: 100%;
font-family: inherit;
font-size: 24px;
color: var(--text);
padding: 10px 4px;
border: none;
border-bottom: 2px solid var(--blue-soft);
background: transparent;
outline: none;
transition: border-color 0.2s ease;
border-radius: 0;
}
input:focus,
select:focus {
border-bottom-color: var(--blue);
box-shadow: none;
}
input::placeholder {
color: #7f9fc4;
}
button {
font-family: inherit;
font-size: 24px;
font-weight: 700;
padding: 14px 24px;
border: none;
border-radius: 16px;
cursor: pointer;
background: linear-gradient(135deg, #3b82d6, #1f5fae);
color: white;
box-shadow: 0 10px 24px rgba(31, 95, 174, 0.24);
transition: transform 0.12s ease, box-shadow 0.12s ease;
white-space: nowrap;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 14px 28px rgba(31, 95, 174, 0.28);
}
button:active {
transform: translateY(0);
}
.button-row {
margin-top: 36px;
}
.status {
margin-top: 28px;
padding: 20px 24px;
border-radius: 18px;
background: linear-gradient(135deg, rgba(204, 227, 255, 0.7), rgba(232, 243, 255, 0.95));
color: var(--muted);
white-space: pre-wrap;
font-size: 22px;
border: 1px solid rgba(77, 143, 219, 0.18);
}
.span-3 {
grid-column: span 3;
}
@media (max-width: 900px) {
body {
padding: 20px;
}
.container {
padding: 28px 24px;
}
.row {
grid-template-columns: 1fr;
gap: 14px;
}
h1 {
font-size: 30px;
}
input,
select,
button {
font-size: 20px;
}
.status {
font-size: 18px;
}
.span-3 {
grid-column: span 1;
}
}
+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VAT Company Lookup</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>VAT Company Lookup</h1>
<div class="form-grid">
<div class="row">
<label for="country">Country / VAT</label>
<select id="country">
<option value="BE">Belgium</option>
<option value="HR">Croatia</option>
<option value="PL">Poland</option>
</select>
<input id="vat_number" type="text" placeholder="VAT number">
<button type="button" onclick="searchCompany()">Search</button>
</div>
<div class="row">
<label for="company_name">Company</label>
<input id="company_name" type="text" class="span-3">
</div>
<div class="row">
<label for="street">Street</label>
<input id="street" type="text" class="span-3">
</div>
<div class="row">
<label for="zip">ZIP / City</label>
<input id="zip" type="text" placeholder="ZIP">
<input id="city" type="text" placeholder="City">
<div></div>
</div>
<div class="row">
<label for="email">Email</label>
<input id="email" type="email" placeholder="example@company.com" class="span-3">
</div>
<div class="row">
<label for="phone">Phone</label>
<input id="phone" type="text" placeholder="+32 123 45 67 89" class="span-3">
</div>
</div>
<div class="button-row">
<button type="button" onclick="saveData()">Save</button>
</div>
<div id="status" class="status">Ready.</div>
</div>
<script src="{{ url_for('static', filename='app.js') }}"></script>
</body>
</html>