feat(company): Add registry data enrichment button for admins
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Adds "Pobierz dane urzędowe" button on company detail page (admin-only)
that fetches data from KRS, Biała Lista VAT, or CEIDG registries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-06 17:54:56 +01:00
parent d6bcf88f1d
commit 6d436bb0ae
3 changed files with 290 additions and 2 deletions

View File

@ -18,11 +18,12 @@ from flask import jsonify, request, current_app
from flask_login import current_user, login_required
from database import (
SessionLocal, Company, User, Person, CompanyPerson, CompanyAIInsights, AiEnrichmentProposal
SessionLocal, Company, User, Person, CompanyPerson, CompanyPKD, CompanyAIInsights, AiEnrichmentProposal
)
from datetime import timedelta
import gemini_service
import krs_api_service
from ceidg_api_service import fetch_ceidg_by_nip
from . import bp
logger = logging.getLogger(__name__)
@ -349,6 +350,162 @@ def api_refresh_company_krs(company_id):
db.close()
# ============================================================
# REGISTRY ENRICHMENT API ROUTE
# ============================================================
@bp.route('/company/<int:company_id>/enrich-registry', methods=['POST'])
@login_required
def api_enrich_company_registry(company_id):
"""
API: Enrich company data from official registries (KRS, CEIDG, Biała Lista VAT).
Only accessible by administrators.
"""
if not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Tylko administrator może pobierać dane z rejestrów'
}), 403
db = SessionLocal()
try:
company = db.query(Company).filter_by(id=company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie znaleziona'}), 404
source = None
message_parts = []
details = {}
# Strategy 1: Company has KRS — fetch directly
if company.krs:
from blueprints.admin.routes_membership import _enrich_company_from_krs
success = _enrich_company_from_krs(company, db)
if success:
source = 'KRS'
# Count imported data
people_count = db.query(CompanyPerson).filter_by(company_id=company.id).count()
pkd_count = db.query(CompanyPKD).filter_by(company_id=company.id).count()
details = {
'people_imported': people_count,
'pkd_codes': pkd_count,
'legal_form': company.legal_form or ''
}
message_parts.append(f'zarząd ({people_count} osób)')
message_parts.append(f'kody PKD ({pkd_count})')
if company.legal_form:
message_parts.append(f'forma prawna: {company.legal_form}')
else:
return jsonify({
'success': False,
'error': f'Nie udało się pobrać danych z KRS dla numeru {company.krs}'
}), 404
# Strategy 2: No KRS but has NIP — try Biała Lista to find KRS
elif company.nip:
krs_service = krs_api_service.KRSApiService()
biala_lista_result = krs_service.search_by_nip(company.nip)
if biala_lista_result and biala_lista_result.get('krs'):
# Found KRS via Biała Lista — save it and enrich
company.krs = biala_lista_result['krs']
db.flush()
from blueprints.admin.routes_membership import _enrich_company_from_krs
success = _enrich_company_from_krs(company, db)
if success:
source = 'KRS (via Biała Lista VAT)'
people_count = db.query(CompanyPerson).filter_by(company_id=company.id).count()
pkd_count = db.query(CompanyPKD).filter_by(company_id=company.id).count()
details = {
'krs_found': company.krs,
'people_imported': people_count,
'pkd_codes': pkd_count,
'legal_form': company.legal_form or ''
}
message_parts.append(f'znaleziono KRS: {company.krs}')
message_parts.append(f'zarząd ({people_count} osób)')
message_parts.append(f'kody PKD ({pkd_count})')
else:
source = 'Biała Lista VAT'
details = {'krs_found': company.krs}
message_parts.append(f'znaleziono KRS: {company.krs} (dane KRS niedostępne)')
else:
# No KRS found — try CEIDG (likely JDG)
ceidg_data = fetch_ceidg_by_nip(company.nip)
if ceidg_data:
source = 'CEIDG'
updated_fields = []
if ceidg_data.get('nazwa') and not company.legal_name:
company.legal_name = ceidg_data['nazwa']
updated_fields.append('nazwa pełna')
if ceidg_data.get('adres_ulica'):
company.address_street = ceidg_data['adres_ulica']
updated_fields.append('ulica')
if ceidg_data.get('adres_budynek'):
company.address_building = ceidg_data['adres_budynek']
updated_fields.append('nr budynku')
if ceidg_data.get('adres_lokal'):
company.address_apartment = ceidg_data['adres_lokal']
updated_fields.append('nr lokalu')
if ceidg_data.get('adres_kod'):
company.address_postal_code = ceidg_data['adres_kod']
updated_fields.append('kod pocztowy')
if ceidg_data.get('adres_miasto'):
company.address_city = ceidg_data['adres_miasto']
updated_fields.append('miasto')
if ceidg_data.get('email') and not company.email:
company.email = ceidg_data['email']
updated_fields.append('email')
if ceidg_data.get('www') and not company.website:
company.website = ceidg_data['www']
updated_fields.append('strona www')
if ceidg_data.get('telefon') and not company.phone:
company.phone = ceidg_data['telefon']
updated_fields.append('telefon')
if ceidg_data.get('regon') and not company.regon:
company.regon = ceidg_data['regon']
updated_fields.append('REGON')
details = {'updated_fields': updated_fields}
message_parts.append(f'zaktualizowano {len(updated_fields)} pól')
if updated_fields:
message_parts.append(', '.join(updated_fields))
else:
return jsonify({
'success': False,
'error': 'Nie znaleziono danych w rejestrach KRS ani CEIDG dla tego NIP'
}), 404
else:
return jsonify({
'success': False,
'error': 'Firma nie ma numeru NIP ani KRS — nie można pobrać danych z rejestrów'
}), 400
db.commit()
logger.info(f"Registry enrichment for company {company.id} ({company.name}) from {source} by {current_user.email}")
return jsonify({
'success': True,
'message': f'Pobrano dane z rejestru {source}: {", ".join(message_parts)}',
'source': source,
'details': details
})
except Exception as e:
db.rollback()
logger.error(f"Registry enrichment error for company {company_id}: {str(e)}")
return jsonify({
'success': False,
'error': f'Błąd podczas pobierania danych: {str(e)}'
}), 500
finally:
db.close()
# ============================================================
# AI ENRICHMENT HELPER FUNCTIONS
# ============================================================

View File

@ -241,7 +241,8 @@ def company_detail(company_id):
gbp_audit=gbp_audit,
it_audit=it_audit,
pkd_codes=pkd_codes,
can_enrich=can_enrich
can_enrich=can_enrich,
is_admin=current_user.is_authenticated and current_user.is_admin
)
finally:
db.close()

View File

@ -208,6 +208,46 @@
opacity: 0.7;
}
/* Registry Enrichment Button */
.registry-enrich-btn {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
background: linear-gradient(135deg, #0d9488 0%, #065f46 100%);
color: white;
border: none;
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.registry-enrich-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(13, 148, 136, 0.4);
}
.registry-enrich-btn:disabled {
background: #ccc;
cursor: not-allowed;
opacity: 0.7;
}
.registry-enrich-btn .spinner {
display: none;
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.registry-enrich-btn.loading .spinner { display: inline-block; }
.registry-enrich-btn.loading .btn-text { display: none; }
/* AI Progress Modal */
.ai-progress-modal {
display: none;
@ -597,6 +637,24 @@
</svg>
<span class="btn-text">Wzbogac dane AI</span>
</button>
{% if is_admin %}
<button
id="registryEnrichBtn"
class="registry-enrich-btn"
data-company-id="{{ company.id }}"
data-nip="{{ company.nip or '' }}"
data-krs="{{ company.krs or '' }}"
>
<span class="spinner"></span>
<svg class="btn-text" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 7V4h16v3"/>
<path d="M9 20h6"/>
<path d="M12 4v16"/>
<rect x="2" y="7" width="20" height="6" rx="1"/>
</svg>
<span class="btn-text">Pobierz dane urzędowe</span>
</button>
{% endif %}
{% if not can_enrich %}
<span style="font-size: var(--font-size-sm); color: var(--text-secondary); margin-left: var(--spacing-sm);">
(tylko admin lub wlasciciel)
@ -3946,6 +4004,78 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Registry enrichment button handler
const registryEnrichBtn = document.getElementById('registryEnrichBtn');
if (registryEnrichBtn) {
registryEnrichBtn.addEventListener('click', async function() {
const companyId = this.dataset.companyId;
const nip = this.dataset.nip;
const krs = this.dataset.krs;
// Show what will be fetched
let sourceInfo = '';
if (krs) {
sourceInfo = `KRS: ${krs}`;
} else if (nip) {
sourceInfo = `NIP: ${nip} (szukam w Białej Liście / CEIDG)`;
} else {
sourceInfo = 'brak NIP i KRS';
}
this.classList.add('loading');
this.disabled = true;
try {
const response = await fetch(`/api/company/${companyId}/enrich-registry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
});
const data = await response.json();
if (data.success) {
// Show success flash
const flash = document.createElement('div');
flash.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#065f46;color:white;padding:16px 24px;border-radius:8px;z-index:9999;font-size:14px;max-width:600px;box-shadow:0 4px 12px rgba(0,0,0,0.3);';
flash.innerHTML = `<strong>Pobrano dane z ${data.source}</strong><br>${data.message}`;
document.body.appendChild(flash);
setTimeout(() => {
flash.remove();
window.location.reload();
}, 3000);
} else {
// Show error flash
const flash = document.createElement('div');
flash.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#991b1b;color:white;padding:16px 24px;border-radius:8px;z-index:9999;font-size:14px;max-width:600px;box-shadow:0 4px 12px rgba(0,0,0,0.3);cursor:pointer;';
flash.innerHTML = `<strong>Błąd</strong><br>${data.error}`;
flash.onclick = () => flash.remove();
document.body.appendChild(flash);
setTimeout(() => flash.remove(), 5000);
this.classList.remove('loading');
this.disabled = false;
}
} catch (error) {
console.error('Registry enrichment error:', error);
const flash = document.createElement('div');
flash.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#991b1b;color:white;padding:16px 24px;border-radius:8px;z-index:9999;font-size:14px;max-width:600px;box-shadow:0 4px 12px rgba(0,0,0,0.3);cursor:pointer;';
flash.innerHTML = '<strong>Błąd połączenia</strong><br>Nie udało się połączyć z serwerem';
flash.onclick = () => flash.remove();
document.body.appendChild(flash);
setTimeout(() => flash.remove(), 5000);
this.classList.remove('loading');
this.disabled = false;
}
});
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}