nordabiz/krs_api_service.py
Maciej Pienczyn 66bb66dd36 feat: Add all 10 missing KRS data fields
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>
2026-02-01 07:56:56 +01:00

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}")