154 lines
5.9 KiB
Python
154 lines
5.9 KiB
Python
"""Supplier reputation checks against Czech public registers.
|
|
|
|
Sources (all free, public):
|
|
- ARES (ares.gov.cz) REST → existence, name, address, legal form, dates,
|
|
VAT registration (stavZdrojeDph), insolvency
|
|
flag (stavZdrojeIr), and name→IČO search.
|
|
- MFČR CRPDPH SOAP → 'nespolehlivý plátce DPH' (unreliable VAT
|
|
payer) + published bank accounts.
|
|
"""
|
|
import logging
|
|
import re
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ARES_BASE = "https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty"
|
|
MFCR_URL = ("https://adisrws.mfcr.cz/adistc/axis2/services/"
|
|
"rozhraniCRPDPH.rozhraniCRPDPHSOAP")
|
|
_TIMEOUT = 20.0
|
|
|
|
|
|
def _clean_ico(ico: str) -> str:
|
|
return re.sub(r"\D", "", str(ico or ""))
|
|
|
|
|
|
def ares_by_ico(ico: str) -> dict | None:
|
|
"""Look up one subject by IČO. Returns a normalized dict or None."""
|
|
ico = _clean_ico(ico).zfill(8) if _clean_ico(ico) else ""
|
|
if len(ico) != 8:
|
|
return None
|
|
try:
|
|
r = httpx.get(f"{ARES_BASE}/{ico}",
|
|
headers={"accept": "application/json"}, timeout=_TIMEOUT)
|
|
if r.status_code == 404:
|
|
return None
|
|
r.raise_for_status()
|
|
d = r.json()
|
|
except Exception as exc:
|
|
logger.warning("ARES lookup failed for %s: %s", ico, exc)
|
|
return None
|
|
reg = d.get("seznamRegistraci") or {}
|
|
sidlo = (d.get("sidlo") or {}).get("textovaAdresa")
|
|
return {
|
|
"ico": d.get("ico"),
|
|
"dic": d.get("dic"),
|
|
"name": d.get("obchodniJmeno"),
|
|
"address": sidlo,
|
|
"legal_form": d.get("pravniForma"),
|
|
"founded": d.get("datumVzniku"),
|
|
"dissolved": d.get("datumZaniku"),
|
|
# AKTIVNI in the business/commercial register or RES
|
|
"active": reg.get("stavZdrojeVr") == "AKTIVNI"
|
|
or reg.get("stavZdrojeRes") == "AKTIVNI",
|
|
"vat_registered": reg.get("stavZdrojeDph") == "AKTIVNI",
|
|
"in_insolvency": reg.get("stavZdrojeIr") == "AKTIVNI",
|
|
}
|
|
|
|
|
|
def ares_search_by_name(name: str, limit: int = 5) -> list[dict]:
|
|
"""Find candidates by trade name. Returns [{ico, name, address}]."""
|
|
name = (name or "").strip()
|
|
if len(name) < 3:
|
|
return []
|
|
try:
|
|
r = httpx.post(f"{ARES_BASE}/vyhledat",
|
|
json={"obchodniJmeno": name, "pocet": limit},
|
|
headers={"accept": "application/json"}, timeout=_TIMEOUT)
|
|
r.raise_for_status()
|
|
d = r.json()
|
|
except Exception as exc:
|
|
logger.warning("ARES name search failed for %r: %s", name, exc)
|
|
return []
|
|
out = []
|
|
for s in (d.get("ekonomickeSubjekty") or [])[:limit]:
|
|
out.append({
|
|
"ico": s.get("ico"),
|
|
"name": s.get("obchodniJmeno"),
|
|
"address": (s.get("sidlo") or {}).get("textovaAdresa"),
|
|
})
|
|
return out
|
|
|
|
|
|
def mfcr_unreliable_vat(ico_or_dic: str) -> dict:
|
|
"""MFČR 'nespolehlivý plátce DPH' check. DIČ number = CZ + IČO usually,
|
|
the service takes the numeric part. Returns {checked, unreliable, accounts}."""
|
|
num = _clean_ico(ico_or_dic)
|
|
if len(num) < 8:
|
|
return {"checked": False, "unreliable": None, "accounts": []}
|
|
body = (
|
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
|
'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'
|
|
' xmlns:urn="http://adis.mfcr.cz/rozhraniCRPDPH/">'
|
|
'<soapenv:Body><urn:StatusNespolehlivyPlatceRequest>'
|
|
f'<urn:dic>{num}</urn:dic>'
|
|
'</urn:StatusNespolehlivyPlatceRequest></soapenv:Body></soapenv:Envelope>'
|
|
)
|
|
try:
|
|
r = httpx.post(MFCR_URL, content=body,
|
|
headers={"Content-Type": "text/xml; charset=utf-8",
|
|
"SOAPAction": ""}, timeout=_TIMEOUT)
|
|
r.raise_for_status()
|
|
# strip namespaces for easy find
|
|
xml = re.sub(r'\sxmlns(:\w+)?="[^"]*"', "", r.text)
|
|
xml = re.sub(r"<(/?)\w+:", r"<\1", xml)
|
|
root = ET.fromstring(xml)
|
|
node = root.find(".//statusPlatceDPH")
|
|
if node is None:
|
|
return {"checked": False, "unreliable": None, "accounts": []}
|
|
flag = node.get("nespolehlivyPlatce") # ANO/NE/NENALEZEN
|
|
accounts = []
|
|
for u in node.findall(".//ucet"):
|
|
std = u.find("standardniUcet")
|
|
nst = u.find("nestandardniUcet")
|
|
if std is not None:
|
|
pref = std.get("predcisli")
|
|
cislo = std.get("cislo")
|
|
kb = std.get("kodBanky")
|
|
acc = (f"{pref}-" if pref else "") + f"{cislo}/{kb}"
|
|
accounts.append(acc)
|
|
elif nst is not None:
|
|
accounts.append(nst.get("cislo"))
|
|
return {
|
|
"checked": flag in ("ANO", "NE"),
|
|
"unreliable": (flag == "ANO") if flag in ("ANO", "NE") else None,
|
|
"vat_status": flag,
|
|
"accounts": accounts,
|
|
}
|
|
except Exception as exc:
|
|
logger.warning("MFČR VAT check failed for %s: %s", num, exc)
|
|
return {"checked": False, "unreliable": None, "accounts": []}
|
|
|
|
|
|
def reputation(ico: str) -> dict | None:
|
|
"""Full reputation report for one IČO. None if the subject doesn't exist."""
|
|
ares = ares_by_ico(ico)
|
|
if ares is None:
|
|
return None
|
|
vat = mfcr_unreliable_vat(ares.get("dic") or ico)
|
|
# Simple flag set for the UI to highlight.
|
|
flags = []
|
|
if ares.get("dissolved"):
|
|
flags.append(("danger", "Subjekt zanikl"))
|
|
if not ares.get("active"):
|
|
flags.append(("danger", "Není aktivní v rejstříku"))
|
|
if ares.get("in_insolvency"):
|
|
flags.append(("danger", "Vedeno v insolvenčním rejstříku"))
|
|
if vat.get("unreliable") is True:
|
|
flags.append(("danger", "Nespolehlivý plátce DPH"))
|
|
if not flags:
|
|
flags.append(("ok", "Bez zjištěných rizik v rejstřících"))
|
|
return {"ares": ares, "vat": vat, "flags": flags}
|