feat: Display ALL KRS data in company profile

- Add krs_raw_data, krs_fetched_at, krs_registration_date,
  krs_representation, krs_activities columns to Company model
- Save complete KRS API response for full data access
- Display in company profile:
  - Board members (zarząd) with functions and avatars
  - Shareholders (wspólnicy) with share amounts
  - Representation method (sposób reprezentacji)
  - Business activities (PKD codes)
  - Registration date with years active
  - KRS address with region info
  - OPP (public benefit) status
  - Metadata (stan_z_dnia, data_odpisu)
- Add migration 037_krs_extended_data.sql

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-01 07:44:33 +01:00
parent a73117ad4a
commit 52061fa949
4 changed files with 231 additions and 7 deletions

View File

@ -693,6 +693,13 @@ class Company(Base):
ceidg_raw_data = Column(PG_JSONB)
ceidg_fetched_at = Column(DateTime)
# === KRS DATA (API prs.ms.gov.pl) ===
krs_raw_data = Column(PG_JSONB) # Pełna odpowiedź z KRS API
krs_fetched_at = Column(DateTime)
krs_registration_date = Column(Date) # Data rejestracji w KRS
krs_representation = Column(Text) # Sposób reprezentacji spółki
krs_activities = Column(PG_JSONB, default=[]) # Przedmiot działalności
# Data source tracking
data_source = Column(String(100))
data_quality_score = Column(Integer)

View File

@ -0,0 +1,29 @@
-- ============================================================
-- 037_krs_extended_data.sql
-- Rozszerzone dane z KRS API
-- ============================================================
-- Surowe dane z KRS API (JSONB)
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_raw_data JSONB;
-- Timestamp ostatniego pobrania z KRS
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_fetched_at TIMESTAMP;
-- Data rejestracji w KRS
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_registration_date DATE;
-- Sposób reprezentacji
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_representation TEXT;
-- Przedmiot działalności z KRS (JSONB array)
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_activities JSONB DEFAULT '[]';
-- Komentarze
COMMENT ON COLUMN companies.krs_raw_data IS 'Pełna odpowiedź z API KRS (JSON)';
COMMENT ON COLUMN companies.krs_fetched_at IS 'Data ostatniego pobrania danych z KRS';
COMMENT ON COLUMN companies.krs_registration_date IS 'Data rejestracji w KRS';
COMMENT ON COLUMN companies.krs_representation IS 'Sposób reprezentacji spółki';
COMMENT ON COLUMN companies.krs_activities IS 'Przedmiot działalności z KRS jako JSON array';
-- Grant permissions
GRANT ALL ON TABLE companies TO nordabiz_app;

View File

@ -495,7 +495,7 @@ def update_company_from_ceidg(company_id: int, ceidg_data: dict, db) -> bool:
def update_company_from_krs(company_id: int, krs_data: dict, db) -> bool:
"""
Aktualizuje firmę w bazie danymi z KRS API.
Aktualizuje firmę w bazie WSZYSTKIMI danymi z KRS API.
Args:
company_id: ID firmy w naszej bazie
@ -518,7 +518,7 @@ def update_company_from_krs(company_id: int, krs_data: dict, db) -> bool:
if krs_data.get("nip") and not company.nip:
company.nip = krs_data.get("nip")
if krs_data.get("regon") and not company.regon:
company.regon = krs_data.get("regon")
company.regon = krs_data.get("regon")[:14] if krs_data.get("regon") else None
# Forma prawna
if krs_data.get("forma_prawna"):
@ -549,16 +549,32 @@ def update_company_from_krs(company_id: int, krs_data: dict, db) -> bool:
if kapital.get("zakladowy"):
company.capital_amount = kapital.get("zakladowy")
# Data rejestracji
# Data rejestracji w KRS
daty = krs_data.get("daty", {})
if daty.get("rejestracji"):
from datetime import datetime as dt
try:
company.business_start_date = dt.strptime(
daty.get("rejestracji"), "%Y-%m-%d"
# Format: "10.02.2021"
company.krs_registration_date = dt.strptime(
daty.get("rejestracji"), "%d.%m.%Y"
).date()
except:
pass
# Też ustaw business_start_date jeśli puste
if not company.business_start_date:
company.business_start_date = company.krs_registration_date
except Exception as e:
print(f" [WARN] Nie można sparsować daty: {daty.get('rejestracji')} - {e}")
# Sposób reprezentacji
if krs_data.get("sposob_reprezentacji"):
company.krs_representation = krs_data.get("sposob_reprezentacji")
# Przedmiot działalności (PKD z KRS)
if krs_data.get("przedmiot_dzialalnosci"):
company.krs_activities = krs_data.get("przedmiot_dzialalnosci")
# SUROWE DANE - zapisz całą odpowiedź z API
company.krs_raw_data = krs_data
company.krs_fetched_at = datetime.now()
# Data source
company.data_source = "KRS API"
@ -568,6 +584,8 @@ def update_company_from_krs(company_id: int, krs_data: dict, db) -> bool:
except Exception as e:
print(f" [ERROR] Błąd aktualizacji: {e}")
import traceback
traceback.print_exc()
return False

View File

@ -1185,6 +1185,8 @@
<div style="font-size: var(--font-size-xs); color: #15803d; margin-top: 2px;">
{% if company.ceidg_fetched_at %}
Pobrano: {{ company.ceidg_fetched_at.strftime('%d.%m.%Y %H:%M') }} &bull;
{% elif company.krs_fetched_at %}
Pobrano: {{ company.krs_fetched_at.strftime('%d.%m.%Y %H:%M') }} &bull;
{% elif company.last_verified_at %}
Zweryfikowano: {{ company.last_verified_at.strftime('%d.%m.%Y %H:%M') }} &bull;
{% endif %}
@ -1336,6 +1338,7 @@
<!-- ===== DANE KRS ===== -->
{% if company.krs and company.data_source == 'KRS API' and not company.ceidg_id %}
{% set krs = company.krs_raw_data or {} %}
<!-- Forma prawna -->
{% if company.legal_form %}
@ -1354,6 +1357,9 @@
<div style="font-size: var(--font-size-xl); font-weight: 700; color: #22c55e; margin-top: 4px;">
{{ '{:,.2f}'.format(company.capital_amount).replace(',', ' ') }} PLN
</div>
{% if krs.kapital and krs.kapital.waluta %}
<div style="font-size: var(--font-size-xs); color: var(--text-muted); margin-top: 4px;">Waluta: {{ krs.kapital.waluta }}</div>
{% endif %}
</div>
{% endif %}
@ -1365,6 +1371,170 @@
</div>
</div>
<!-- Data rejestracji w KRS -->
{% if company.krs_registration_date or (krs.daty and krs.daty.rejestracji) %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #059669;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Data rejestracji w KRS</div>
<div style="font-size: var(--font-size-xl); font-weight: 700; color: #059669; margin-top: 4px;">
{% if company.krs_registration_date %}
{{ company.krs_registration_date.strftime('%d.%m.%Y') }}
{% else %}
{{ krs.daty.rejestracji }}
{% endif %}
</div>
{% if company.krs_registration_date %}
{% set years_active = ((now.date() - company.krs_registration_date).days / 365.25)|int %}
{% if years_active > 0 %}
<div style="font-size: var(--font-size-xs); color: var(--text-muted); margin-top: 4px;">{{ years_active }} lat działalności</div>
{% endif %}
{% endif %}
</div>
{% endif %}
<!-- Sposób reprezentacji -->
{% if company.krs_representation or krs.sposob_reprezentacji %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #8b5cf6; grid-column: span 2;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--spacing-sm);">Sposób reprezentacji</div>
<div style="font-size: var(--font-size-base); color: var(--text-primary); line-height: 1.6;">
{{ company.krs_representation or krs.sposob_reprezentacji }}
</div>
</div>
{% endif %}
<!-- Zarząd -->
{% if krs.zarzad and krs.zarzad|length > 0 %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #0ea5e9; grid-column: span 2;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--spacing-md);">
Zarząd ({{ krs.zarzad|length }} {{ 'osoba' if krs.zarzad|length == 1 else 'osoby' if krs.zarzad|length < 5 else 'osób' }})
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--spacing-md);">
{% for osoba in krs.zarzad %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); background: var(--surface); border-radius: var(--radius);">
<div style="width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; font-size: var(--font-size-sm);">
{{ osoba.imie[0] if osoba.imie else '?' }}{{ osoba.nazwisko[0] if osoba.nazwisko else '' }}
</div>
<div>
<div style="font-weight: 600; color: var(--text-primary);">
{{ osoba.imie }} {% if osoba.imie_drugie %}{{ osoba.imie_drugie }} {% endif %}{{ osoba.nazwisko }}
</div>
{% if osoba.funkcja %}
<div style="font-size: var(--font-size-xs); color: #0ea5e9; font-weight: 500;">{{ osoba.funkcja }}</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Wspólnicy -->
{% if krs.wspolnicy and krs.wspolnicy|length > 0 %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #f59e0b; grid-column: span 2;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--spacing-md);">
Wspólnicy ({{ krs.wspolnicy|length }})
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--spacing-md);">
{% for wspolnik in krs.wspolnicy %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); background: var(--surface); border-radius: var(--radius);">
<div style="width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; font-size: var(--font-size-sm);">
{{ wspolnik.imie[0] if wspolnik.imie else '?' }}{{ wspolnik.nazwisko[0] if wspolnik.nazwisko else '' }}
</div>
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text-primary);">
{{ wspolnik.imie }} {{ wspolnik.nazwisko }}
</div>
{% if wspolnik.udzialy %}
<div style="font-size: var(--font-size-xs); color: var(--text-muted);">
Udziały: {{ wspolnik.udzialy }}
{% if wspolnik.calosc_udzialow %}<span style="color: #f59e0b; font-weight: 600;">(100%)</span>{% endif %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Przedmiot działalności (PKD) -->
{% if company.krs_activities and company.krs_activities|length > 0 %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #a855f7; grid-column: span 2;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--spacing-md);">
Przedmiot działalności z KRS ({{ company.krs_activities|length }} PKD)
</div>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm); max-height: 200px; overflow-y: auto;">
{% for pkd in company.krs_activities %}
<span style="background: var(--surface); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius); font-size: var(--font-size-sm); border: 1px solid var(--border);">
{{ pkd }}
</span>
{% endfor %}
</div>
</div>
{% elif krs.przedmiot_dzialalnosci and krs.przedmiot_dzialalnosci|length > 0 %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #a855f7; grid-column: span 2;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--spacing-md);">
Przedmiot działalności z KRS ({{ krs.przedmiot_dzialalnosci|length }} PKD)
</div>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm); max-height: 200px; overflow-y: auto;">
{% for pkd in krs.przedmiot_dzialalnosci %}
<span style="background: var(--surface); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius); font-size: var(--font-size-sm); border: 1px solid var(--border);">
{{ pkd }}
</span>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Adres z KRS -->
{% if krs.adres and (krs.adres.ulica or krs.adres.miejscowosc) %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #64748b;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--spacing-sm);">Adres siedziby z KRS</div>
<div style="font-size: var(--font-size-base); color: var(--text-primary);">
{% if krs.adres.ulica %}{{ krs.adres.ulica }}{% endif %}
{% if krs.adres.nr_domu %} {{ krs.adres.nr_domu }}{% endif %}
{% if krs.adres.nr_lokalu %}/{{ krs.adres.nr_lokalu }}{% endif %}<br>
{% if krs.adres.kod_pocztowy %}{{ krs.adres.kod_pocztowy }} {% endif %}{{ krs.adres.miejscowosc or '' }}
</div>
{% if krs.adres.powiat or krs.adres.wojewodztwo %}
<div style="font-size: var(--font-size-xs); color: var(--text-muted); margin-top: 4px;">
{% if krs.adres.powiat %}pow. {{ krs.adres.powiat }}, {% endif %}woj. {{ krs.adres.wojewodztwo or '' }}
</div>
{% endif %}
</div>
{% endif %}
<!-- OPP Status -->
{% if krs.czy_opp %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #ec4899;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Status OPP</div>
<div style="font-size: var(--font-size-lg); font-weight: 700; color: #ec4899; margin-top: 4px;">
Organizacja Pożytku Publicznego
</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted); margin-top: 4px;">Można przekazać 1,5% podatku</div>
</div>
{% endif %}
<!-- Metadane KRS -->
{% if krs.metadata or krs.daty %}
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border-left: 4px solid #94a3b8;">
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--spacing-sm);">Informacje o odpisie KRS</div>
<div style="font-size: var(--font-size-sm); color: var(--text-muted);">
{% if krs.metadata %}
{% if krs.metadata.stan_z_dnia %}<div>Stan na dzień: <strong>{{ krs.metadata.stan_z_dnia }}</strong></div>{% endif %}
{% if krs.metadata.data_odpisu %}<div>Data odpisu: <strong>{{ krs.metadata.data_odpisu }}</strong></div>{% endif %}
{% endif %}
{% if krs.daty %}
{% if krs.daty.ostatniego_wpisu %}<div>Ostatni wpis: <strong>{{ krs.daty.ostatniego_wpisu }}</strong>{% if krs.daty.numer_ostatniego_wpisu %} (nr {{ krs.daty.numer_ostatniego_wpisu }}){% endif %}</div>{% endif %}
{% endif %}
{% if company.krs_fetched_at %}
<div style="margin-top: var(--spacing-sm); padding-top: var(--spacing-sm); border-top: 1px solid var(--border);">
Pobrano z API: <strong>{{ company.krs_fetched_at.strftime('%d.%m.%Y %H:%M') }}</strong>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
<!-- ===== KONIEC DANE KRS ===== -->