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
- Add @role_required to 2 missing routes (krs_api PDF download, zopk milestones) - Add role-based menu visibility in admin bar (hide Users, Security, Benefits, Model Comparison, Debug from OFFICE_MANAGER users) - Inject SystemRole into Jinja2 context processor for template role checks - Replace is_admin checkbox with role select dropdown in user creation form - Migrate routes.py and routes_users_api.py from is_admin to SystemRole-based role assignment via set_role() - Add deprecation notice to is_admin database column - Add 23 RBAC unit tests (hierarchy, has_role, set_role, permissions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
469 lines
16 KiB
Python
469 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
|
|
@role_required(SystemRole.OFFICE_MANAGER)
|
|
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()
|