2026-03-31 22:07:11 +02:00
|
|
|
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()
|
2026-04-01 09:35:40 +02:00
|
|
|
vat_number = (vat_number or "").strip().replace(" ", "").replace(".", "").replace("-", "")
|
2026-03-31 22:07:11 +02:00
|
|
|
|
|
|
|
|
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:
|
2026-04-01 09:35:40 +02:00
|
|
|
- address is comma-separated or line separated
|
2026-03-31 22:07:11 +02:00
|
|
|
- first part = street
|
2026-04-01 09:35:40 +02:00
|
|
|
- last part = ZIP + CITY or middlepart is city
|
2026-03-31 22:07:11 +02:00
|
|
|
|
|
|
|
|
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 "", "", ""
|
2026-04-01 09:35:40 +02:00
|
|
|
|
2026-03-31 22:07:11 +02:00
|
|
|
parts = [part.strip() for part in address.split(",") if part.strip()]
|
2026-04-01 09:35:40 +02:00
|
|
|
if "\n" in address:
|
|
|
|
|
parts = [part.strip() for part in address.split("\n") if part.strip()]
|
|
|
|
|
|
2026-03-31 22:07:11 +02:00
|
|
|
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 {
|
2026-04-01 09:35:40 +02:00
|
|
|
"source": "mf.gov.pl",
|
2026-03-31 22:07:11 +02:00
|
|
|
"found": False,
|
|
|
|
|
"country": "PL",
|
|
|
|
|
"vat_number": nip,
|
|
|
|
|
"name": "",
|
|
|
|
|
"street": "",
|
|
|
|
|
"zip": "",
|
|
|
|
|
"city": "",
|
|
|
|
|
"raw_address": "",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name = (subject.get("name") or "").strip()
|
2026-04-01 09:35:40 +02:00
|
|
|
working_address = (subject.get("workingAddress")or subject.get("residenceAddress") or "").strip()
|
2026-03-31 22:07:11 +02:00
|
|
|
street, zip_code, city = parse_polish_working_address(working_address)
|
|
|
|
|
|
|
|
|
|
return {
|
2026-04-01 09:35:40 +02:00
|
|
|
"source": "mf.gov.pl",
|
2026-03-31 22:07:11 +02:00
|
|
|
"found": True,
|
|
|
|
|
"country": "PL",
|
|
|
|
|
"vat_number": nip,
|
|
|
|
|
"name": name,
|
|
|
|
|
"street": street,
|
|
|
|
|
"zip": zip_code,
|
|
|
|
|
"city": city,
|
|
|
|
|
"raw_address": working_address,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
def index():
|
2026-04-01 09:35:40 +02:00
|
|
|
return render_template("company_data.html")
|
2026-03-31 22:07:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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 = {
|
2026-04-01 09:35:40 +02:00
|
|
|
"source": "ec.europa.eu",
|
2026-03-31 22:07:11 +02:00
|
|
|
"found": False,
|
|
|
|
|
"country": country,
|
|
|
|
|
"vat_number": vat_number,
|
|
|
|
|
"name": "",
|
|
|
|
|
"street": "",
|
|
|
|
|
"zip": "",
|
|
|
|
|
"city": "",
|
|
|
|
|
"raw_address": "",
|
2026-04-01 09:35:40 +02:00
|
|
|
"warning": f"ec.europa.eu lookup failed: {exc}",
|
2026-03-31 22:07:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
{
|
2026-04-01 09:35:40 +02:00
|
|
|
"source": "mf.gov.pl",
|
2026-03-31 22:07:11 +02:00
|
|
|
"found": False,
|
|
|
|
|
"country": country,
|
|
|
|
|
"vat_number": vat_number,
|
|
|
|
|
"name": "",
|
|
|
|
|
"street": "",
|
|
|
|
|
"zip": "",
|
|
|
|
|
"city": "",
|
|
|
|
|
"raw_address": "",
|
|
|
|
|
"warning": vies_result.get("warning"),
|
2026-04-01 09:35:40 +02:00
|
|
|
"fallback_error": f"mf.gov.pl lookup failed: {exc}",
|
2026-03-31 22:07:11 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return jsonify(vies_result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
app.run(debug=True)
|