nordabiz/blueprints/admin/routes_krs_api.py
Maciej Pienczyn 4181a2e760 refactor: Migrate access control from is_admin to role-based system
Replace ~170 manual `if not current_user.is_admin` checks with:
- @role_required(SystemRole.ADMIN) for user management, security, ZOPK
- @role_required(SystemRole.OFFICE_MANAGER) for content management
- current_user.can_access_admin_panel() for admin UI access
- current_user.can_moderate_forum() for forum moderation
- current_user.can_edit_company(id) for company permissions

Add @office_manager_required decorator shortcut.
Add SQL migration to sync existing users' role field.

Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:05:22 +01:00

468 lines
16 KiB
Python

"""
KRS API Routes - Admin blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains API routes for KRS (National Court Register) audits.
"""
import logging
from datetime import datetime, date
from pathlib import Path
from flask import jsonify, request, send_file, current_app
from flask_login import current_user, login_required
from database import (
SessionLocal,
Company,
Person,
CompanyPerson,
CompanyPKD,
CompanyFinancialReport,
KRSAudit,
SystemRole
)
from utils.decorators import role_required
from . import bp
logger = logging.getLogger(__name__)
# Import limiter from app - will be initialized when app starts
def get_limiter():
"""Get rate limiter from current app."""
return current_app.extensions.get('limiter')
def is_krs_audit_available():
"""Check if KRS audit service is available."""
try:
from krs_audit_service import parse_krs_pdf
return True
except ImportError:
return False
def parse_date_str(date_val):
"""Helper to parse date from string or return date object as-is"""
if date_val is None:
return None
if isinstance(date_val, date):
return date_val
if isinstance(date_val, str):
try:
return datetime.strptime(date_val, '%Y-%m-%d').date()
except Exception:
return None
return None
def _import_krs_person(db, company_id, person_data, role_category, source_document):
"""Helper to import a person from KRS data"""
pesel = person_data.get('pesel')
nazwisko = person_data.get('nazwisko', '')
imiona = person_data.get('imiona', '')
rola = person_data.get('rola', '')
# Find or create Person
person = None
if pesel:
person = db.query(Person).filter_by(pesel=pesel).first()
if not person:
# Try to find by name
person = db.query(Person).filter_by(
nazwisko=nazwisko,
imiona=imiona
).first()
if not person:
person = Person(
pesel=pesel,
nazwisko=nazwisko,
imiona=imiona
)
db.add(person)
db.flush()
# Check if relation already exists
existing_rel = db.query(CompanyPerson).filter_by(
company_id=company_id,
person_id=person.id,
role_category=role_category
).first()
if not existing_rel:
cp = CompanyPerson(
company_id=company_id,
person_id=person.id,
role=rola,
role_category=role_category,
source='ekrs.ms.gov.pl',
source_document=source_document,
fetched_at=datetime.now()
)
# Add shares info for shareholders
if role_category == 'wspolnik':
cp.shares_count = person_data.get('udzialy_liczba')
if person_data.get('udzialy_wartosc'):
cp.shares_value = person_data['udzialy_wartosc']
if person_data.get('udzialy_procent'):
cp.shares_percent = person_data['udzialy_procent']
db.add(cp)
# ============================================================
# KRS AUDIT API ROUTES
# ============================================================
@bp.route('/krs-api/audit', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_trigger():
"""
API: Trigger KRS audit for a company (admin-only).
Parses KRS PDF file and extracts all available data:
- Basic info (KRS, NIP, REGON, name, legal form)
- Capital and shares
- Management board, shareholders, procurators
- PKD codes
- Financial reports
Request JSON body:
- company_id: Company ID (integer)
Returns:
- Success: Audit results saved to database
- Error: Error message with status code
"""
if not is_krs_audit_available():
return jsonify({
'success': False,
'error': 'Usługa audytu KRS jest niedostępna.'
}), 503
from krs_audit_service import parse_krs_pdf
data = request.get_json()
if not data or not data.get('company_id'):
return jsonify({
'success': False,
'error': 'Podaj company_id firmy do audytu.'
}), 400
company_id = data['company_id']
db = SessionLocal()
try:
company = db.query(Company).filter_by(id=company_id, status='active').first()
if not company:
return jsonify({
'success': False,
'error': 'Firma nie znaleziona.'
}), 404
if not company.krs:
return jsonify({
'success': False,
'error': f'Firma "{company.name}" nie ma numeru KRS.'
}), 400
# Find PDF file
pdf_dir = Path('data/krs_pdfs')
pdf_files = list(pdf_dir.glob(f'*{company.krs}*.pdf'))
if not pdf_files:
return jsonify({
'success': False,
'error': f'Nie znaleziono pliku PDF dla KRS {company.krs}. '
f'Pobierz odpis z ekrs.ms.gov.pl i umieść w data/krs_pdfs/'
}), 404
pdf_path = pdf_files[0]
# Create audit record
audit = KRSAudit(
company_id=company.id,
status='parsing',
progress_percent=10,
progress_message='Parsowanie pliku PDF...',
pdf_filename=pdf_path.name,
pdf_path=str(pdf_path)
)
db.add(audit)
db.commit()
# Parse PDF
try:
parsed_data = parse_krs_pdf(str(pdf_path), verbose=True)
# Update audit with parsed data
audit.status = 'completed'
audit.progress_percent = 100
audit.progress_message = 'Audyt zakończony pomyślnie'
audit.extracted_krs = parsed_data.get('krs')
audit.extracted_nazwa = parsed_data.get('nazwa')
audit.extracted_nip = parsed_data.get('nip')
audit.extracted_regon = parsed_data.get('regon')
audit.extracted_forma_prawna = parsed_data.get('forma_prawna')
audit.extracted_data_rejestracji = parse_date_str(parsed_data.get('data_rejestracji'))
audit.extracted_kapital_zakladowy = parsed_data.get('kapital_zakladowy')
audit.extracted_liczba_udzialow = parsed_data.get('liczba_udzialow')
audit.extracted_sposob_reprezentacji = parsed_data.get('sposob_reprezentacji')
audit.zarzad_count = len(parsed_data.get('zarzad', []))
audit.wspolnicy_count = len(parsed_data.get('wspolnicy', []))
audit.prokurenci_count = len(parsed_data.get('prokurenci', []))
audit.pkd_count = 1 if parsed_data.get('pkd_przewazajacy') else 0
audit.pkd_count += len(parsed_data.get('pkd_pozostale', []))
# Convert non-JSON-serializable values for JSONB storage
def make_json_serializable(obj):
from decimal import Decimal
if isinstance(obj, Decimal):
return float(obj)
elif isinstance(obj, (datetime, date)):
return obj.isoformat()
elif isinstance(obj, dict):
return {k: make_json_serializable(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [make_json_serializable(i) for i in obj]
return obj
audit.parsed_data = make_json_serializable(parsed_data)
audit.pdf_downloaded_at = datetime.now()
# Update company with parsed data
if parsed_data.get('kapital_zakladowy'):
company.capital_amount = parsed_data['kapital_zakladowy']
if parsed_data.get('liczba_udzialow'):
company.capital_shares_count = parsed_data['liczba_udzialow']
if parsed_data.get('wartosc_nominalna_udzialu'):
company.capital_share_value = parsed_data['wartosc_nominalna_udzialu']
if parsed_data.get('data_rejestracji'):
company.krs_registration_date = parse_date_str(parsed_data['data_rejestracji'])
if parsed_data.get('sposob_reprezentacji'):
company.krs_representation_rules = parsed_data['sposob_reprezentacji']
if parsed_data.get('czas_trwania'):
company.krs_duration = parsed_data['czas_trwania']
company.krs_last_audit_at = datetime.now()
company.krs_pdf_path = str(pdf_path)
# Import PKD codes
pkd_main = parsed_data.get('pkd_przewazajacy')
if pkd_main:
existing = db.query(CompanyPKD).filter_by(
company_id=company.id,
pkd_code=pkd_main['kod']
).first()
if not existing:
db.add(CompanyPKD(
company_id=company.id,
pkd_code=pkd_main['kod'],
pkd_description=pkd_main['opis'],
is_primary=True,
source='ekrs'
))
# Also update Company.pkd_code
company.pkd_code = pkd_main['kod']
company.pkd_description = pkd_main['opis']
for pkd in parsed_data.get('pkd_pozostale', []):
existing = db.query(CompanyPKD).filter_by(
company_id=company.id,
pkd_code=pkd['kod']
).first()
if not existing:
db.add(CompanyPKD(
company_id=company.id,
pkd_code=pkd['kod'],
pkd_description=pkd['opis'],
is_primary=False,
source='ekrs'
))
# Import people (zarząd, wspólnicy)
for person_data in parsed_data.get('zarzad', []):
_import_krs_person(db, company.id, person_data, 'zarzad', pdf_path.name)
for person_data in parsed_data.get('wspolnicy', []):
_import_krs_person(db, company.id, person_data, 'wspolnik', pdf_path.name)
for person_data in parsed_data.get('prokurenci', []):
_import_krs_person(db, company.id, person_data, 'prokurent', pdf_path.name)
# Import financial reports
for report in parsed_data.get('sprawozdania_finansowe', []):
existing = db.query(CompanyFinancialReport).filter_by(
company_id=company.id,
period_start=parse_date_str(report.get('okres_od')),
period_end=parse_date_str(report.get('okres_do'))
).first()
if not existing:
db.add(CompanyFinancialReport(
company_id=company.id,
period_start=parse_date_str(report.get('okres_od')),
period_end=parse_date_str(report.get('okres_do')),
filed_at=parse_date_str(report.get('data_zlozenia')),
source='ekrs'
))
db.commit()
logger.info(f"KRS audit completed for {company.name} (KRS: {company.krs})")
return jsonify({
'success': True,
'message': f'Audyt KRS zakończony dla {company.name}',
'company_id': company.id,
'data': {
'krs': parsed_data.get('krs'),
'nazwa': parsed_data.get('nazwa'),
'nip': parsed_data.get('nip'),
'regon': parsed_data.get('regon'),
'kapital': float(parsed_data.get('kapital_zakladowy', 0) or 0),
'liczba_udzialow': parsed_data.get('liczba_udzialow'),
'zarzad_count': len(parsed_data.get('zarzad', [])),
'wspolnicy_count': len(parsed_data.get('wspolnicy', [])),
'prokurenci_count': len(parsed_data.get('prokurenci', [])),
'pkd_count': audit.pkd_count
}
})
except Exception as e:
audit.status = 'error'
audit.progress_percent = 0
audit.error_message = str(e)
db.commit()
logger.error(f"KRS audit failed for {company.name}: {e}")
return jsonify({
'success': False,
'error': f'Błąd parsowania PDF: {str(e)}'
}), 500
finally:
db.close()
@bp.route('/krs-api/audit/batch', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_krs_audit_batch():
"""
API: Trigger batch KRS audit for all companies with KRS numbers.
This runs audits sequentially to avoid overloading the system.
Returns progress updates via the response.
"""
if not is_krs_audit_available():
return jsonify({
'success': False,
'error': 'Usługa audytu KRS jest niedostępna.'
}), 503
from krs_audit_service import parse_krs_pdf
db = SessionLocal()
try:
# Get companies with KRS that haven't been audited recently
companies = db.query(Company).filter(
Company.status == 'active',
Company.krs.isnot(None),
Company.krs != ''
).order_by(Company.name).all()
results = {
'total': len(companies),
'success': 0,
'failed': 0,
'skipped': 0,
'details': []
}
pdf_dir = Path('data/krs_pdfs')
for company in companies:
# Find PDF file
pdf_files = list(pdf_dir.glob(f'*{company.krs}*.pdf'))
if not pdf_files:
results['skipped'] += 1
results['details'].append({
'company': company.name,
'krs': company.krs,
'status': 'skipped',
'reason': 'Brak pliku PDF'
})
continue
pdf_path = pdf_files[0]
try:
parsed_data = parse_krs_pdf(str(pdf_path))
# Update company
if parsed_data.get('kapital_zakladowy'):
company.capital_amount = parsed_data['kapital_zakladowy']
if parsed_data.get('liczba_udzialow'):
company.capital_shares_count = parsed_data['liczba_udzialow']
company.krs_last_audit_at = datetime.now()
company.krs_pdf_path = str(pdf_path)
results['success'] += 1
results['details'].append({
'company': company.name,
'krs': company.krs,
'status': 'success'
})
except Exception as e:
results['failed'] += 1
results['details'].append({
'company': company.name,
'krs': company.krs,
'status': 'error',
'reason': str(e)
})
db.commit()
return jsonify({
'success': True,
'message': f'Audyt zakończony: {results["success"]} sukces, '
f'{results["failed"]} błędów, {results["skipped"]} pominiętych',
'results': results
})
finally:
db.close()
@bp.route('/krs-api/pdf/<int:company_id>')
@login_required
def api_krs_pdf_download(company_id):
"""
API: Download/serve KRS PDF file for a company.
"""
db = SessionLocal()
try:
company = db.query(Company).filter_by(id=company_id).first()
if not company:
return jsonify({'error': 'Firma nie znaleziona'}), 404
if not company.krs_pdf_path:
return jsonify({'error': 'Brak pliku PDF'}), 404
pdf_path = Path(company.krs_pdf_path)
if not pdf_path.exists():
return jsonify({'error': 'Plik PDF nie istnieje'}), 404
return send_file(
str(pdf_path),
mimetype='application/pdf',
as_attachment=False,
download_name=pdf_path.name
)
finally:
db.close()