Extend KRS data collection and display: 1. NIP - displayed in company profile 2. REGON - displayed in company profile 3. Email from KRS (adresPocztyElektronicznej) 4. WWW from KRS (adresStronyInternetowej) 5. ePUAP address (adresDoDoreczenElektronicznychWpisanyDoBAE) 6. Company agreement date (data zawarcia umowy) 7. Company duration (czas trwania spółki) 8. Share information (informacja o udziałach) 9. Financial statements history (sprawozdania finansowe) 10. Full PKD codes with class/subclass (e.g., 62.03.Z) 11. Court registry data (sygnatura, sąd, rok obrotowy) Updated krs_api_service.py: - Extended KRSCompanyData dataclass with new fields - Updated parse_krs_response() to extract all data - PKD now returns dict with kod, opis, glowna Updated templates/company_detail.html: - Display NIP and REGON from KRS - Contact section with email, www, ePUAP - Company agreement section - Financial statements history grid - Court registry information - Improved PKD display with full codes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
481 lines
16 KiB
Python
481 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
KRS Open API Integration Service for NordaBiznes.
|
|
Fetches official company data from Krajowy Rejestr Sądowy (Ministry of Justice).
|
|
|
|
API Documentation: https://prs.ms.gov.pl/krs/openApi
|
|
"""
|
|
import requests
|
|
from datetime import datetime
|
|
from typing import Optional, Dict, Any, List
|
|
from dataclasses import dataclass
|
|
|
|
|
|
# API Configuration
|
|
KRS_API_BASE_URL = "https://api-krs.ms.gov.pl/api/krs"
|
|
KRS_API_TIMEOUT = 15 # seconds
|
|
|
|
|
|
@dataclass
|
|
class KRSCompanyData:
|
|
"""Parsed company data from KRS API."""
|
|
krs: str
|
|
nazwa: str
|
|
nazwa_skrocona: Optional[str]
|
|
nip: Optional[str]
|
|
regon: Optional[str]
|
|
forma_prawna: str
|
|
|
|
# Address
|
|
ulica: Optional[str]
|
|
nr_domu: Optional[str]
|
|
nr_lokalu: Optional[str]
|
|
kod_pocztowy: Optional[str]
|
|
miejscowosc: Optional[str]
|
|
wojewodztwo: Optional[str]
|
|
powiat: Optional[str]
|
|
gmina: Optional[str]
|
|
kraj: str
|
|
poczta: Optional[str] # NOWE
|
|
|
|
# Contact from KRS (NOWE)
|
|
email_krs: Optional[str]
|
|
www_krs: Optional[str]
|
|
adres_epuap: Optional[str]
|
|
|
|
# Capital
|
|
kapital_zakladowy: Optional[float]
|
|
kapital_waluta: str
|
|
|
|
# Dates
|
|
data_rejestracji: Optional[str]
|
|
data_ostatniego_wpisu: Optional[str]
|
|
numer_ostatniego_wpisu: Optional[int]
|
|
|
|
# Company agreement/statute (NOWE)
|
|
data_umowy_spolki: Optional[str]
|
|
czas_trwania_spolki: Optional[str]
|
|
informacja_o_udzialach: Optional[str]
|
|
|
|
# Management (anonymized in Open API)
|
|
zarzad: List[Dict[str, str]]
|
|
nazwa_organu: Optional[str] # NOWE
|
|
sposob_reprezentacji: Optional[str]
|
|
|
|
# Shareholders (anonymized in Open API)
|
|
wspolnicy: List[Dict[str, Any]]
|
|
|
|
# Other
|
|
przedmiot_dzialalnosci: List[Dict[str, str]] # ZMIANA: teraz słownik z pełnymi kodami
|
|
czy_opp: bool
|
|
|
|
# Financial statements (NOWE)
|
|
sprawozdania_finansowe: List[Dict[str, str]]
|
|
|
|
# Court/Registry info (NOWE)
|
|
sygnatura_akt: Optional[str]
|
|
sad_rejestrowy: Optional[str]
|
|
dzien_konczacy_rok_obrotowy: Optional[str]
|
|
|
|
# Metadata
|
|
data_odpisu: str
|
|
stan_z_dnia: str
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary for JSON serialization."""
|
|
return {
|
|
'krs': self.krs,
|
|
'nazwa': self.nazwa,
|
|
'nazwa_skrocona': self.nazwa_skrocona,
|
|
'nip': self.nip,
|
|
'regon': self.regon,
|
|
'forma_prawna': self.forma_prawna,
|
|
'adres': {
|
|
'ulica': self.ulica,
|
|
'nr_domu': self.nr_domu,
|
|
'nr_lokalu': self.nr_lokalu,
|
|
'kod_pocztowy': self.kod_pocztowy,
|
|
'miejscowosc': self.miejscowosc,
|
|
'wojewodztwo': self.wojewodztwo,
|
|
'powiat': self.powiat,
|
|
'gmina': self.gmina,
|
|
'kraj': self.kraj,
|
|
'poczta': self.poczta,
|
|
},
|
|
'kontakt_krs': {
|
|
'email': self.email_krs,
|
|
'www': self.www_krs,
|
|
'adres_epuap': self.adres_epuap,
|
|
},
|
|
'kapital': {
|
|
'zakladowy': self.kapital_zakladowy,
|
|
'waluta': self.kapital_waluta,
|
|
},
|
|
'daty': {
|
|
'rejestracji': self.data_rejestracji,
|
|
'ostatniego_wpisu': self.data_ostatniego_wpisu,
|
|
'numer_ostatniego_wpisu': self.numer_ostatniego_wpisu,
|
|
},
|
|
'umowa_spolki': {
|
|
'data_umowy': self.data_umowy_spolki,
|
|
'czas_trwania': self.czas_trwania_spolki,
|
|
'informacja_o_udzialach': self.informacja_o_udzialach,
|
|
},
|
|
'zarzad': self.zarzad,
|
|
'nazwa_organu': self.nazwa_organu,
|
|
'sposob_reprezentacji': self.sposob_reprezentacji,
|
|
'wspolnicy': self.wspolnicy,
|
|
'przedmiot_dzialalnosci': self.przedmiot_dzialalnosci,
|
|
'czy_opp': self.czy_opp,
|
|
'sprawozdania_finansowe': self.sprawozdania_finansowe,
|
|
'rejestr': {
|
|
'sygnatura_akt': self.sygnatura_akt,
|
|
'sad_rejestrowy': self.sad_rejestrowy,
|
|
'dzien_konczacy_rok_obrotowy': self.dzien_konczacy_rok_obrotowy,
|
|
},
|
|
'metadata': {
|
|
'data_odpisu': self.data_odpisu,
|
|
'stan_z_dnia': self.stan_z_dnia,
|
|
'zrodlo': 'KRS Open API (prs.ms.gov.pl)',
|
|
}
|
|
}
|
|
|
|
|
|
def fetch_krs_data(krs_number: str, rejestr: str = 'P') -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Fetch raw data from KRS Open API.
|
|
|
|
Args:
|
|
krs_number: KRS number (with or without leading zeros)
|
|
rejestr: 'P' for przedsiębiorców (companies), 'S' for stowarzyszeń (associations)
|
|
|
|
Returns:
|
|
Raw JSON response or None if not found/error
|
|
"""
|
|
# Normalize KRS number to 10 digits with leading zeros
|
|
krs_normalized = krs_number.zfill(10)
|
|
|
|
url = f"{KRS_API_BASE_URL}/OdpisAktualny/{krs_normalized}"
|
|
params = {
|
|
'rejestr': rejestr,
|
|
'format': 'json'
|
|
}
|
|
|
|
try:
|
|
response = requests.get(url, params=params, timeout=KRS_API_TIMEOUT)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
elif response.status_code == 404:
|
|
return None # Company not found
|
|
else:
|
|
print(f"KRS API error: {response.status_code}")
|
|
return None
|
|
|
|
except requests.RequestException as e:
|
|
print(f"KRS API request failed: {e}")
|
|
return None
|
|
|
|
|
|
def parse_krs_response(data: Dict[str, Any]) -> Optional[KRSCompanyData]:
|
|
"""
|
|
Parse raw KRS API response into structured KRSCompanyData.
|
|
|
|
Args:
|
|
data: Raw JSON response from KRS API
|
|
|
|
Returns:
|
|
Parsed KRSCompanyData or None if parsing fails
|
|
"""
|
|
try:
|
|
odpis = data.get('odpis', {})
|
|
naglowek = odpis.get('naglowekA', {})
|
|
dane = odpis.get('dane', {})
|
|
dzial1 = dane.get('dzial1', {})
|
|
dzial2 = dane.get('dzial2', {})
|
|
dzial3 = dane.get('dzial3', {})
|
|
|
|
# Basic data
|
|
dane_podmiotu = dzial1.get('danePodmiotu', {})
|
|
identyfikatory = dane_podmiotu.get('identyfikatory', {})
|
|
|
|
# Address and Contact
|
|
siedziba_adres = dzial1.get('siedzibaIAdres', {})
|
|
siedziba = siedziba_adres.get('siedziba', {})
|
|
adres = siedziba_adres.get('adres', {})
|
|
|
|
# Capital
|
|
kapital = dzial1.get('kapital', {})
|
|
kapital_zakladowy = kapital.get('wysokoscKapitaluZakladowego', {})
|
|
|
|
# Company agreement/statute
|
|
umowa_statut = dzial1.get('umowaStatut', {})
|
|
pozostale = dzial1.get('pozostaleInformacje', {})
|
|
|
|
# Get first date of company agreement
|
|
data_umowy = None
|
|
umowy_list = umowa_statut.get('informacjaOZawarciuZmianieUmowyStatutu', [])
|
|
if umowy_list:
|
|
data_umowy = umowy_list[0].get('zawarcieZmianaUmowyStatutu')
|
|
|
|
# Management
|
|
reprezentacja = dzial2.get('reprezentacja', {})
|
|
sklad_zarzadu = reprezentacja.get('sklad', [])
|
|
|
|
zarzad = []
|
|
for osoba in sklad_zarzadu:
|
|
zarzad.append({
|
|
'imie': osoba.get('imiona', {}).get('imie', ''),
|
|
'imie_drugie': osoba.get('imiona', {}).get('imieDrugie', ''),
|
|
'nazwisko': osoba.get('nazwisko', {}).get('nazwiskoICzlon', ''),
|
|
'funkcja': osoba.get('funkcjaWOrganie', ''),
|
|
'zawieszona': osoba.get('czyZawieszona', False),
|
|
})
|
|
|
|
# Shareholders
|
|
wspolnicy_raw = dzial1.get('wspolnicySpzoo', [])
|
|
wspolnicy = []
|
|
for wspolnik in wspolnicy_raw:
|
|
wspolnicy.append({
|
|
'imie': wspolnik.get('imiona', {}).get('imie', ''),
|
|
'nazwisko': wspolnik.get('nazwisko', {}).get('nazwiskoICzlon', ''),
|
|
'udzialy': wspolnik.get('posiadaneUdzialy', ''),
|
|
'calosc_udzialow': wspolnik.get('czyPosiadaCaloscUdzialow', False),
|
|
})
|
|
|
|
# Business activities (PKD) - with full codes
|
|
przedmiot = []
|
|
przedmiot_dzial = dzial3.get('przedmiotDzialalnosci', {})
|
|
for pkd in przedmiot_dzial.get('przedmiotPrzewazajacejDzialalnosci', []):
|
|
kod_pelny = f"{pkd.get('kodDzial', '')}.{pkd.get('kodKlasa', '')}.{pkd.get('kodPodklasa', '')}"
|
|
przedmiot.append({
|
|
'kod': kod_pelny,
|
|
'opis': pkd.get('opis', ''),
|
|
'glowna': True,
|
|
})
|
|
for pkd in przedmiot_dzial.get('przedmiotPozostalejDzialalnosci', []):
|
|
kod_pelny = f"{pkd.get('kodDzial', '')}.{pkd.get('kodKlasa', '')}.{pkd.get('kodPodklasa', '')}"
|
|
przedmiot.append({
|
|
'kod': kod_pelny,
|
|
'opis': pkd.get('opis', ''),
|
|
'glowna': False,
|
|
})
|
|
|
|
# Financial statements
|
|
sprawozdania = []
|
|
wzmianki = dzial3.get('wzmiankiOZlozonychDokumentach', {})
|
|
for sf in wzmianki.get('wzmiankaOZlozeniuRocznegoSprawozdaniaFinansowego', []):
|
|
sprawozdania.append({
|
|
'data_zlozenia': sf.get('dataZlozenia', ''),
|
|
'za_okres': sf.get('zaOkresOdDo', ''),
|
|
})
|
|
|
|
# Fiscal year end
|
|
rok_obrotowy = dzial3.get('informacjaODniuKonczacymRokObrotowy', {})
|
|
dzien_konczacy = rok_obrotowy.get('dzienKonczacyPierwszyRokObrotowy')
|
|
|
|
# Parse capital value
|
|
kapital_value = None
|
|
if kapital_zakladowy.get('wartosc'):
|
|
try:
|
|
kapital_value = float(kapital_zakladowy['wartosc'].replace(',', '.').replace(' ', ''))
|
|
except ValueError:
|
|
pass
|
|
|
|
return KRSCompanyData(
|
|
krs=naglowek.get('numerKRS', ''),
|
|
nazwa=dane_podmiotu.get('nazwa', ''),
|
|
nazwa_skrocona=dane_podmiotu.get('nazwaSkrocona'),
|
|
nip=identyfikatory.get('nip'),
|
|
regon=identyfikatory.get('regon'),
|
|
forma_prawna=dane_podmiotu.get('formaPrawna', ''),
|
|
|
|
# Address
|
|
ulica=adres.get('ulica'),
|
|
nr_domu=adres.get('nrDomu'),
|
|
nr_lokalu=adres.get('nrLokalu'),
|
|
kod_pocztowy=adres.get('kodPocztowy'),
|
|
miejscowosc=adres.get('miejscowosc'),
|
|
wojewodztwo=siedziba.get('wojewodztwo'),
|
|
powiat=siedziba.get('powiat'),
|
|
gmina=siedziba.get('gmina'),
|
|
kraj=adres.get('kraj', 'POLSKA'),
|
|
poczta=adres.get('poczta'),
|
|
|
|
# Contact from KRS
|
|
email_krs=siedziba_adres.get('adresPocztyElektronicznej'),
|
|
www_krs=siedziba_adres.get('adresStronyInternetowej'),
|
|
adres_epuap=siedziba_adres.get('adresDoDoreczenElektronicznychWpisanyDoBAE'),
|
|
|
|
# Capital
|
|
kapital_zakladowy=kapital_value,
|
|
kapital_waluta=kapital_zakladowy.get('waluta', 'PLN'),
|
|
|
|
# Dates
|
|
data_rejestracji=naglowek.get('dataRejestracjiWKRS'),
|
|
data_ostatniego_wpisu=naglowek.get('dataOstatniegoWpisu'),
|
|
numer_ostatniego_wpisu=naglowek.get('numerOstatniegoWpisu'),
|
|
|
|
# Company agreement
|
|
data_umowy_spolki=data_umowy,
|
|
czas_trwania_spolki=pozostale.get('czasNaJakiUtworzonyZostalPodmiot'),
|
|
informacja_o_udzialach=pozostale.get('informacjaOLiczbieUdzialow'),
|
|
|
|
# Management
|
|
zarzad=zarzad,
|
|
nazwa_organu=reprezentacja.get('nazwaOrganu'),
|
|
sposob_reprezentacji=reprezentacja.get('sposobReprezentacji'),
|
|
|
|
# Shareholders
|
|
wspolnicy=wspolnicy,
|
|
|
|
# Business activities
|
|
przedmiot_dzialalnosci=przedmiot,
|
|
czy_opp=dane_podmiotu.get('czyPosiadaStatusOPP', False),
|
|
|
|
# Financial statements
|
|
sprawozdania_finansowe=sprawozdania,
|
|
|
|
# Court/Registry info
|
|
sygnatura_akt=naglowek.get('sygnaturaAktSprawyDotyczacejOstatniegoWpisu'),
|
|
sad_rejestrowy=naglowek.get('oznaczenieSaduDokonujacegoOstatniegoWpisu'),
|
|
dzien_konczacy_rok_obrotowy=dzien_konczacy,
|
|
|
|
# Metadata
|
|
data_odpisu=naglowek.get('dataCzasOdpisu', ''),
|
|
stan_z_dnia=naglowek.get('stanZDnia', ''),
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"Error parsing KRS response: {e}")
|
|
return None
|
|
|
|
|
|
def get_company_from_krs(krs_number: str) -> Optional[KRSCompanyData]:
|
|
"""
|
|
Fetch and parse company data from KRS Open API.
|
|
|
|
Args:
|
|
krs_number: KRS number
|
|
|
|
Returns:
|
|
Parsed KRSCompanyData or None if not found/error
|
|
"""
|
|
raw_data = fetch_krs_data(krs_number)
|
|
if raw_data:
|
|
return parse_krs_response(raw_data)
|
|
return None
|
|
|
|
|
|
def verify_company_data(company_krs: str, company_nip: str = None, company_regon: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Verify company data against KRS Open API.
|
|
|
|
Args:
|
|
company_krs: KRS number to verify
|
|
company_nip: Expected NIP (optional)
|
|
company_regon: Expected REGON (optional)
|
|
|
|
Returns:
|
|
Dictionary with verification results
|
|
"""
|
|
result = {
|
|
'verified': False,
|
|
'krs_found': False,
|
|
'nip_match': None,
|
|
'regon_match': None,
|
|
'krs_data': None,
|
|
'errors': [],
|
|
'timestamp': datetime.now().isoformat(),
|
|
}
|
|
|
|
krs_data = get_company_from_krs(company_krs)
|
|
|
|
if krs_data is None:
|
|
result['errors'].append(f"Nie znaleziono podmiotu o KRS {company_krs}")
|
|
return result
|
|
|
|
result['krs_found'] = True
|
|
result['krs_data'] = krs_data.to_dict()
|
|
|
|
# Verify NIP if provided
|
|
if company_nip:
|
|
krs_nip = krs_data.nip.replace('-', '').replace(' ', '') if krs_data.nip else ''
|
|
expected_nip = company_nip.replace('-', '').replace(' ', '')
|
|
result['nip_match'] = krs_nip == expected_nip
|
|
if not result['nip_match']:
|
|
result['errors'].append(f"NIP niezgodny: oczekiwano {expected_nip}, w KRS: {krs_nip}")
|
|
|
|
# Verify REGON if provided
|
|
if company_regon:
|
|
krs_regon = krs_data.regon[:9] if krs_data.regon else '' # Use first 9 digits
|
|
expected_regon = company_regon[:9].replace('-', '').replace(' ', '')
|
|
result['regon_match'] = krs_regon == expected_regon
|
|
if not result['regon_match']:
|
|
result['errors'].append(f"REGON niezgodny: oczekiwano {expected_regon}, w KRS: {krs_regon}")
|
|
|
|
# Overall verification
|
|
result['verified'] = result['krs_found'] and len(result['errors']) == 0
|
|
|
|
return result
|
|
|
|
|
|
def format_address(krs_data: KRSCompanyData) -> str:
|
|
"""Format address from KRS data."""
|
|
parts = []
|
|
|
|
if krs_data.ulica:
|
|
addr = krs_data.ulica
|
|
if krs_data.nr_domu:
|
|
addr += f" {krs_data.nr_domu}"
|
|
if krs_data.nr_lokalu:
|
|
addr += f"/{krs_data.nr_lokalu}"
|
|
parts.append(addr)
|
|
|
|
if krs_data.kod_pocztowy and krs_data.miejscowosc:
|
|
parts.append(f"{krs_data.kod_pocztowy} {krs_data.miejscowosc}")
|
|
elif krs_data.miejscowosc:
|
|
parts.append(krs_data.miejscowosc)
|
|
|
|
return ', '.join(parts)
|
|
|
|
|
|
# CLI for testing
|
|
if __name__ == '__main__':
|
|
import sys
|
|
import json
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python krs_api_service.py <KRS_NUMBER>")
|
|
print("Example: python krs_api_service.py 0000817317")
|
|
sys.exit(1)
|
|
|
|
krs = sys.argv[1]
|
|
print(f"Pobieranie danych z KRS Open API dla: {krs}")
|
|
print("=" * 60)
|
|
|
|
data = get_company_from_krs(krs)
|
|
|
|
if data:
|
|
print(f"Nazwa: {data.nazwa}")
|
|
print(f"NIP: {data.nip}")
|
|
print(f"REGON: {data.regon}")
|
|
print(f"Forma prawna: {data.forma_prawna}")
|
|
print(f"Adres: {format_address(data)}")
|
|
print(f"Kapitał zakładowy: {data.kapital_zakladowy} {data.kapital_waluta}")
|
|
print(f"Data rejestracji: {data.data_rejestracji}")
|
|
print(f"Ostatni wpis: {data.data_ostatniego_wpisu} (nr {data.numer_ostatniego_wpisu})")
|
|
print()
|
|
print("Zarząd (dane zanonimizowane w Open API):")
|
|
for osoba in data.zarzad:
|
|
print(f" - {osoba['imie']} {osoba['nazwisko']} - {osoba['funkcja']}")
|
|
print()
|
|
print("Wspólnicy (dane zanonimizowane w Open API):")
|
|
for w in data.wspolnicy:
|
|
print(f" - {w['imie']} {w['nazwisko']}: {w['udzialy']}")
|
|
print()
|
|
print(f"Stan z dnia: {data.stan_z_dnia}")
|
|
print(f"Data odpisu: {data.data_odpisu}")
|
|
else:
|
|
print(f"Nie znaleziono podmiotu o KRS {krs}")
|