Files
AI_portal/contract-check/registry.py
2026-07-07 13:16:26 +02:00

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}