feat: Add KRS Audit panel with full PDF parsing
- New admin panel /admin/krs-audit for KRS data extraction - Full PDF parser extracting: company data, capital, shares, PKD codes, management board, shareholders, procurators, financial reports - API endpoints for single/batch audits and PDF download - Company profile shows "Odpis PDF" button and last audit date - Database migration for krs_audits, company_pkd, company_financial_reports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
11c49ad937
commit
de52e6991c
527
app.py
527
app.py
@ -22,8 +22,8 @@ import secrets
|
||||
import re
|
||||
import json
|
||||
from collections import deque
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session, Response
|
||||
from datetime import datetime, timedelta, date
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session, Response, send_file
|
||||
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from flask_limiter import Limiter
|
||||
@ -109,7 +109,10 @@ from database import (
|
||||
Person,
|
||||
CompanyPerson,
|
||||
GBPAudit,
|
||||
ITAudit
|
||||
ITAudit,
|
||||
KRSAudit,
|
||||
CompanyPKD,
|
||||
CompanyFinancialReport
|
||||
)
|
||||
|
||||
# Import services
|
||||
@ -155,6 +158,16 @@ except ImportError as e:
|
||||
GBP_AUDIT_VERSION = None
|
||||
logger.warning(f"GBP audit service not available: {e}")
|
||||
|
||||
# KRS (Krajowy Rejestr Sądowy) audit service
|
||||
try:
|
||||
from krs_audit_service import parse_krs_pdf, parse_krs_pdf_full
|
||||
KRS_AUDIT_AVAILABLE = True
|
||||
KRS_AUDIT_VERSION = '1.0'
|
||||
except ImportError as e:
|
||||
KRS_AUDIT_AVAILABLE = False
|
||||
KRS_AUDIT_VERSION = None
|
||||
logger.warning(f"KRS audit service not available: {e}")
|
||||
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__)
|
||||
|
||||
@ -8566,6 +8579,514 @@ def api_zopk_search_news():
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# KRS AUDIT (Krajowy Rejestr Sądowy)
|
||||
# ============================================================
|
||||
|
||||
@app.route('/admin/krs-audit')
|
||||
@login_required
|
||||
def admin_krs_audit():
|
||||
"""
|
||||
Admin dashboard for KRS (Krajowy Rejestr Sądowy) audit.
|
||||
|
||||
Displays:
|
||||
- Summary stats (with KRS, audited count, data extraction status)
|
||||
- List of companies with KRS numbers
|
||||
- Audit progress and status for each company
|
||||
- Links to source PDF files
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
from sqlalchemy import func
|
||||
|
||||
# Get all active companies with KRS numbers
|
||||
companies_query = db.query(Company).filter(
|
||||
Company.status == 'active',
|
||||
Company.krs.isnot(None),
|
||||
Company.krs != ''
|
||||
).order_by(Company.name).all()
|
||||
|
||||
# Get latest audit for each company
|
||||
companies = []
|
||||
for company in companies_query:
|
||||
# Get latest audit
|
||||
latest_audit = db.query(KRSAudit).filter(
|
||||
KRSAudit.company_id == company.id
|
||||
).order_by(KRSAudit.audit_date.desc()).first()
|
||||
|
||||
# Get PKD codes count
|
||||
pkd_count = db.query(CompanyPKD).filter(
|
||||
CompanyPKD.company_id == company.id
|
||||
).count()
|
||||
|
||||
# Get people count
|
||||
people_count = db.query(CompanyPerson).filter(
|
||||
CompanyPerson.company_id == company.id
|
||||
).count()
|
||||
|
||||
companies.append({
|
||||
'id': company.id,
|
||||
'name': company.name,
|
||||
'slug': company.slug,
|
||||
'krs': company.krs,
|
||||
'nip': company.nip,
|
||||
'capital_amount': company.capital_amount,
|
||||
'krs_last_audit_at': company.krs_last_audit_at,
|
||||
'krs_pdf_path': company.krs_pdf_path,
|
||||
'audit': latest_audit,
|
||||
'pkd_count': pkd_count,
|
||||
'people_count': people_count,
|
||||
'capital_shares_count': company.capital_shares_count
|
||||
})
|
||||
|
||||
# Calculate stats
|
||||
total_with_krs = len(companies)
|
||||
audited_count = len([c for c in companies if c['krs_last_audit_at']])
|
||||
not_audited_count = total_with_krs - audited_count
|
||||
with_capital = len([c for c in companies if c['capital_amount']])
|
||||
with_people = len([c for c in companies if c['people_count'] > 0])
|
||||
with_pkd = len([c for c in companies if c['pkd_count'] > 0])
|
||||
|
||||
# Companies without KRS
|
||||
no_krs_count = db.query(Company).filter(
|
||||
Company.status == 'active',
|
||||
(Company.krs.is_(None)) | (Company.krs == '')
|
||||
).count()
|
||||
|
||||
stats = {
|
||||
'total_with_krs': total_with_krs,
|
||||
'audited_count': audited_count,
|
||||
'not_audited_count': not_audited_count,
|
||||
'no_krs_count': no_krs_count,
|
||||
'with_capital': with_capital,
|
||||
'with_people': with_people,
|
||||
'with_pkd': with_pkd
|
||||
}
|
||||
|
||||
return render_template('admin/krs_audit_dashboard.html',
|
||||
companies=companies,
|
||||
stats=stats,
|
||||
krs_audit_available=KRS_AUDIT_AVAILABLE,
|
||||
now=datetime.now()
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/krs/audit', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("20 per hour")
|
||||
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 current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Brak uprawnień. Tylko administrator może uruchamiać audyty KRS.'
|
||||
}), 403
|
||||
|
||||
if not KRS_AUDIT_AVAILABLE:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Usługa audytu KRS jest niedostępna.'
|
||||
}), 503
|
||||
|
||||
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', []))
|
||||
audit.parsed_data = 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'),
|
||||
'kapital': float(parsed_data.get('kapital_zakladowy', 0) or 0),
|
||||
'zarzad_count': len(parsed_data.get('zarzad', [])),
|
||||
'wspolnicy_count': len(parsed_data.get('wspolnicy', [])),
|
||||
'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()
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
@app.route('/api/krs/audit/batch', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit("5 per hour")
|
||||
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 current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Brak uprawnień.'
|
||||
}), 403
|
||||
|
||||
if not KRS_AUDIT_AVAILABLE:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Usługa audytu KRS jest niedostępna.'
|
||||
}), 503
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@app.route('/api/krs/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()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ERROR HANDLERS
|
||||
# ============================================================
|
||||
|
||||
147
database.py
147
database.py
@ -334,6 +334,18 @@ class Company(Base):
|
||||
website_status = Column(String(20)) # 'active', 'broken', 'no_website'
|
||||
website_quality_score = Column(Integer) # 0-100
|
||||
|
||||
# === KRS DATA (added 2026-01-13) ===
|
||||
krs_registration_date = Column(Date) # Data wpisu do KRS
|
||||
krs_company_agreement_date = Column(Date) # Data umowy spółki
|
||||
krs_duration = Column(String(100)) # Czas trwania (NIEOZNACZONY lub data)
|
||||
krs_representation_rules = Column(Text) # Sposób reprezentacji
|
||||
capital_currency = Column(String(3), default='PLN')
|
||||
capital_shares_count = Column(Integer) # Liczba udziałów
|
||||
capital_share_value = Column(Numeric(15, 2)) # Wartość nominalna udziału
|
||||
is_opp = Column(Boolean, default=False) # Czy OPP
|
||||
krs_last_audit_at = Column(DateTime) # Data ostatniego audytu KRS
|
||||
krs_pdf_path = Column(Text) # Ścieżka do pliku PDF
|
||||
|
||||
# Relationships
|
||||
category = relationship('Category', back_populates='companies')
|
||||
services = relationship('CompanyService', back_populates='company', cascade='all, delete-orphan')
|
||||
@ -2202,6 +2214,141 @@ class CompanyPerson(Base):
|
||||
return f"<CompanyPerson {self.person.full_name()} - {self.role} @ {self.company.name}>"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# KRS AUDIT
|
||||
# ============================================================
|
||||
|
||||
class KRSAudit(Base):
|
||||
"""
|
||||
KRS audit history - tracks PDF downloads and data extraction.
|
||||
Each audit represents one extraction run from EKRS.
|
||||
"""
|
||||
__tablename__ = 'krs_audits'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
|
||||
# Audit timing
|
||||
audit_date = Column(DateTime, default=datetime.now, nullable=False, index=True)
|
||||
|
||||
# PDF source info
|
||||
pdf_filename = Column(String(255)) # np. "odpis_pelny_0000882964.pdf"
|
||||
pdf_path = Column(Text) # full path to stored PDF
|
||||
pdf_downloaded_at = Column(DateTime)
|
||||
|
||||
# Extraction status
|
||||
status = Column(String(20), default='pending', index=True) # pending, downloading, parsing, completed, error
|
||||
progress_percent = Column(Integer, default=0)
|
||||
progress_message = Column(Text)
|
||||
error_message = Column(Text)
|
||||
|
||||
# Extracted data summary
|
||||
extracted_krs = Column(String(10))
|
||||
extracted_nazwa = Column(Text)
|
||||
extracted_nip = Column(String(10))
|
||||
extracted_regon = Column(String(14))
|
||||
extracted_forma_prawna = Column(String(255))
|
||||
extracted_data_rejestracji = Column(Date)
|
||||
extracted_kapital_zakladowy = Column(Numeric(15, 2))
|
||||
extracted_waluta = Column(String(3), default='PLN')
|
||||
extracted_liczba_udzialow = Column(Integer)
|
||||
extracted_sposob_reprezentacji = Column(Text)
|
||||
|
||||
# Counts for quick stats
|
||||
zarzad_count = Column(Integer, default=0)
|
||||
wspolnicy_count = Column(Integer, default=0)
|
||||
prokurenci_count = Column(Integer, default=0)
|
||||
pkd_count = Column(Integer, default=0)
|
||||
|
||||
# Full parsed data as JSON
|
||||
parsed_data = Column(JSONB)
|
||||
|
||||
# Audit metadata
|
||||
audit_version = Column(String(20), default='1.0')
|
||||
audit_source = Column(String(50), default='ekrs.ms.gov.pl')
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# Relationship
|
||||
company = relationship('Company', backref='krs_audits')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<KRSAudit company_id={self.company_id} status={self.status}>'
|
||||
|
||||
@property
|
||||
def status_label(self):
|
||||
"""Human-readable status label in Polish"""
|
||||
labels = {
|
||||
'pending': 'Oczekuje',
|
||||
'downloading': 'Pobieranie PDF',
|
||||
'parsing': 'Przetwarzanie',
|
||||
'completed': 'Ukończony',
|
||||
'error': 'Błąd'
|
||||
}
|
||||
return labels.get(self.status, self.status)
|
||||
|
||||
|
||||
class CompanyPKD(Base):
|
||||
"""
|
||||
PKD codes for companies (Polska Klasyfikacja Działalności).
|
||||
Multiple PKD codes per company allowed - one is marked as primary.
|
||||
"""
|
||||
__tablename__ = 'company_pkd'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
pkd_code = Column(String(10), nullable=False, index=True) # np. "62.03.Z"
|
||||
pkd_description = Column(Text)
|
||||
is_primary = Column(Boolean, default=False) # przeważający PKD
|
||||
source = Column(String(50), default='ekrs') # ekrs, ceidg
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# Relationship
|
||||
company = relationship('Company', backref='pkd_codes')
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('company_id', 'pkd_code', name='uq_company_pkd'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
primary = ' (główny)' if self.is_primary else ''
|
||||
return f'<CompanyPKD {self.pkd_code}{primary}>'
|
||||
|
||||
|
||||
class CompanyFinancialReport(Base):
|
||||
"""
|
||||
Financial reports (sprawozdania finansowe) filed with KRS.
|
||||
Tracks report periods and filing dates.
|
||||
"""
|
||||
__tablename__ = 'company_financial_reports'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
period_start = Column(Date)
|
||||
period_end = Column(Date)
|
||||
filed_at = Column(Date)
|
||||
report_type = Column(String(50), default='annual') # annual, quarterly
|
||||
source = Column(String(50), default='ekrs')
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
# Relationship
|
||||
company = relationship('Company', backref='financial_reports')
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('company_id', 'period_start', 'period_end', 'report_type', name='uq_company_financial_report'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<CompanyFinancialReport {self.period_start} - {self.period_end}>'
|
||||
|
||||
|
||||
# ============================================================
|
||||
# DATABASE INITIALIZATION
|
||||
# ============================================================
|
||||
|
||||
136
database/migrations/013_krs_audit.sql
Normal file
136
database/migrations/013_krs_audit.sql
Normal file
@ -0,0 +1,136 @@
|
||||
-- ============================================================
|
||||
-- Migration: 013_krs_audit.sql
|
||||
-- Date: 2026-01-13
|
||||
-- Description: Add KRS Audit table for tracking KRS data extracts
|
||||
-- ============================================================
|
||||
|
||||
-- KRS Audit table - tracks audit history and PDF sources
|
||||
CREATE TABLE IF NOT EXISTS krs_audits (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company_id INTEGER REFERENCES companies(id) ON DELETE CASCADE NOT NULL,
|
||||
|
||||
-- Audit timing
|
||||
audit_date TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- PDF source info
|
||||
pdf_filename VARCHAR(255), -- np. "odpis_pelny_0000882964.pdf"
|
||||
pdf_path TEXT, -- full path to stored PDF
|
||||
pdf_downloaded_at TIMESTAMP, -- when PDF was fetched from EKRS
|
||||
|
||||
-- Extraction status
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, downloading, parsing, completed, error
|
||||
progress_percent INTEGER DEFAULT 0,
|
||||
progress_message TEXT,
|
||||
error_message TEXT,
|
||||
|
||||
-- Extracted data summary (for quick display)
|
||||
extracted_krs VARCHAR(10),
|
||||
extracted_nazwa TEXT,
|
||||
extracted_nip VARCHAR(10),
|
||||
extracted_regon VARCHAR(14),
|
||||
extracted_forma_prawna VARCHAR(255),
|
||||
extracted_data_rejestracji DATE,
|
||||
extracted_kapital_zakladowy NUMERIC(15, 2),
|
||||
extracted_waluta VARCHAR(3) DEFAULT 'PLN',
|
||||
extracted_liczba_udzialow INTEGER,
|
||||
extracted_sposob_reprezentacji TEXT,
|
||||
|
||||
-- Counts for quick stats
|
||||
zarzad_count INTEGER DEFAULT 0,
|
||||
wspolnicy_count INTEGER DEFAULT 0,
|
||||
prokurenci_count INTEGER DEFAULT 0,
|
||||
pkd_count INTEGER DEFAULT 0,
|
||||
|
||||
-- Full parsed data as JSON (for reference)
|
||||
parsed_data JSONB,
|
||||
|
||||
-- Audit metadata
|
||||
audit_version VARCHAR(20) DEFAULT '1.0',
|
||||
audit_source VARCHAR(50) DEFAULT 'ekrs.ms.gov.pl',
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_krs_audits_company_id ON krs_audits(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_krs_audits_audit_date ON krs_audits(audit_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_krs_audits_status ON krs_audits(status);
|
||||
|
||||
-- Company table extensions for KRS data
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_registration_date DATE;
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_company_agreement_date DATE;
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_duration VARCHAR(100);
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_representation_rules TEXT;
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS capital_currency VARCHAR(3) DEFAULT 'PLN';
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS capital_shares_count INTEGER;
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS capital_share_value NUMERIC(15, 2);
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS is_opp BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_last_audit_at TIMESTAMP;
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS krs_pdf_path TEXT;
|
||||
|
||||
-- PKD codes table (multiple PKD codes per company)
|
||||
CREATE TABLE IF NOT EXISTS company_pkd (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company_id INTEGER REFERENCES companies(id) ON DELETE CASCADE NOT NULL,
|
||||
pkd_code VARCHAR(10) NOT NULL, -- np. "62.03.Z"
|
||||
pkd_description TEXT,
|
||||
is_primary BOOLEAN DEFAULT FALSE, -- przeważający PKD
|
||||
source VARCHAR(50) DEFAULT 'ekrs', -- ekrs, ceidg
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(company_id, pkd_code)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_company_pkd_company_id ON company_pkd(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_company_pkd_code ON company_pkd(pkd_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_company_pkd_primary ON company_pkd(company_id, is_primary);
|
||||
|
||||
-- Financial reports table (sprawozdania finansowe)
|
||||
CREATE TABLE IF NOT EXISTS company_financial_reports (
|
||||
id SERIAL PRIMARY KEY,
|
||||
company_id INTEGER REFERENCES companies(id) ON DELETE CASCADE NOT NULL,
|
||||
period_start DATE,
|
||||
period_end DATE,
|
||||
filed_at DATE,
|
||||
report_type VARCHAR(50) DEFAULT 'annual', -- annual, quarterly
|
||||
source VARCHAR(50) DEFAULT 'ekrs',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(company_id, period_start, period_end, report_type)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_company_financial_reports_company_id ON company_financial_reports(company_id);
|
||||
|
||||
-- Add comments
|
||||
COMMENT ON TABLE krs_audits IS 'KRS audit history - tracks PDF downloads and data extraction';
|
||||
COMMENT ON COLUMN krs_audits.pdf_path IS 'Full path to stored PDF file on server';
|
||||
COMMENT ON COLUMN krs_audits.status IS 'Audit status: pending, downloading, parsing, completed, error';
|
||||
COMMENT ON COLUMN krs_audits.parsed_data IS 'Full extracted data in JSON format';
|
||||
|
||||
COMMENT ON TABLE company_pkd IS 'PKD codes for companies - multiple codes per company allowed';
|
||||
COMMENT ON COLUMN company_pkd.is_primary IS 'TRUE for main business activity (PKD przeważający)';
|
||||
|
||||
COMMENT ON TABLE company_financial_reports IS 'Financial reports (sprawozdania finansowe) filed with KRS';
|
||||
|
||||
COMMENT ON COLUMN companies.krs_registration_date IS 'Date of first entry in KRS registry';
|
||||
COMMENT ON COLUMN companies.krs_representation_rules IS 'Rules for company representation from KRS';
|
||||
COMMENT ON COLUMN companies.krs_last_audit_at IS 'Date of last KRS audit';
|
||||
COMMENT ON COLUMN companies.krs_pdf_path IS 'Path to latest KRS PDF file';
|
||||
|
||||
-- Grant permissions
|
||||
GRANT ALL ON TABLE krs_audits TO nordabiz_app;
|
||||
GRANT ALL ON TABLE company_pkd TO nordabiz_app;
|
||||
GRANT ALL ON TABLE company_financial_reports TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE krs_audits_id_seq TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE company_pkd_id_seq TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE company_financial_reports_id_seq TO nordabiz_app;
|
||||
|
||||
-- ============================================================
|
||||
-- Verification query (run after migration):
|
||||
-- SELECT c.name, ka.audit_date, ka.status, ka.extracted_kapital_zakladowy
|
||||
-- FROM companies c
|
||||
-- LEFT JOIN krs_audits ka ON c.id = ka.company_id
|
||||
-- WHERE c.krs IS NOT NULL
|
||||
-- LIMIT 10;
|
||||
-- ============================================================
|
||||
685
krs_audit_service.py
Normal file
685
krs_audit_service.py
Normal file
@ -0,0 +1,685 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
KRS Audit Service - Full data extraction from KRS PDF files.
|
||||
|
||||
Downloads PDF documents from EKRS and extracts complete company data:
|
||||
- Basic info: KRS, NIP, REGON, company name, legal form
|
||||
- Address: full address with email, website
|
||||
- Capital: share capital amount, shares count, nominal value
|
||||
- People: management board, shareholders, procurators
|
||||
- PKD codes: main and secondary business activities
|
||||
- Financial reports: filing dates
|
||||
- Representation rules
|
||||
|
||||
Author: Norda Biznes Development Team
|
||||
Created: 2026-01-13
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime, date
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
from decimal import Decimal
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import pdfplumber
|
||||
except ImportError:
|
||||
logger.error("Required library pdfplumber. Install: pip install pdfplumber")
|
||||
raise
|
||||
|
||||
|
||||
# ============================================================
|
||||
# DATA CLASSES
|
||||
# ============================================================
|
||||
|
||||
@dataclass
|
||||
class KRSPerson:
|
||||
"""Person from KRS (board member, shareholder, procurator)"""
|
||||
nazwisko: str
|
||||
imiona: str
|
||||
pesel: Optional[str] = None
|
||||
rola: str = "" # PREZES ZARZĄDU, CZŁONEK ZARZĄDU, WSPÓLNIK, PROKURENT
|
||||
rola_kategoria: str = "" # zarzad, wspolnik, prokurent
|
||||
|
||||
# For shareholders
|
||||
udzialy_liczba: Optional[int] = None
|
||||
udzialy_wartosc: Optional[Decimal] = None
|
||||
udzialy_procent: Optional[Decimal] = None
|
||||
|
||||
def full_name(self) -> str:
|
||||
return f"{self.imiona} {self.nazwisko}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class KRSPKD:
|
||||
"""PKD code from KRS"""
|
||||
kod: str # e.g., "62.03.Z"
|
||||
opis: str
|
||||
jest_przewazajacy: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class KRSFinancialReport:
|
||||
"""Financial report filing info from KRS"""
|
||||
okres_od: Optional[date] = None
|
||||
okres_do: Optional[date] = None
|
||||
data_zlozenia: Optional[date] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class KRSAddress:
|
||||
"""Company address from KRS"""
|
||||
ulica: Optional[str] = None
|
||||
numer_domu: Optional[str] = None
|
||||
numer_lokalu: Optional[str] = None
|
||||
miejscowosc: Optional[str] = None
|
||||
kod_pocztowy: Optional[str] = None
|
||||
poczta: Optional[str] = None
|
||||
wojewodztwo: Optional[str] = None
|
||||
powiat: Optional[str] = None
|
||||
gmina: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
www: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class KRSFullData:
|
||||
"""Complete data extracted from KRS PDF"""
|
||||
# Identifiers
|
||||
krs: str
|
||||
nip: Optional[str] = None
|
||||
regon: Optional[str] = None
|
||||
|
||||
# Basic info
|
||||
nazwa: str = ""
|
||||
nazwa_skrocona: Optional[str] = None
|
||||
forma_prawna: Optional[str] = None
|
||||
|
||||
# Dates
|
||||
data_rejestracji: Optional[date] = None
|
||||
data_umowy_spolki: Optional[date] = None
|
||||
czas_trwania: Optional[str] = None # "NIEOZNACZONY" or date
|
||||
|
||||
# Address
|
||||
siedziba: Optional[KRSAddress] = None
|
||||
|
||||
# Capital
|
||||
kapital_zakladowy: Optional[Decimal] = None
|
||||
waluta: str = "PLN"
|
||||
liczba_udzialow: Optional[int] = None
|
||||
wartosc_nominalna_udzialu: Optional[Decimal] = None
|
||||
|
||||
# Representation
|
||||
sposob_reprezentacji: Optional[str] = None
|
||||
|
||||
# PKD codes
|
||||
pkd_przewazajacy: Optional[KRSPKD] = None
|
||||
pkd_pozostale: List[KRSPKD] = field(default_factory=list)
|
||||
|
||||
# People
|
||||
zarzad: List[KRSPerson] = field(default_factory=list)
|
||||
wspolnicy: List[KRSPerson] = field(default_factory=list)
|
||||
prokurenci: List[KRSPerson] = field(default_factory=list)
|
||||
rada_nadzorcza: List[KRSPerson] = field(default_factory=list)
|
||||
|
||||
# Status
|
||||
czy_opp: bool = False
|
||||
zaleglosci: Optional[str] = None
|
||||
wierzytelnosci: Optional[str] = None
|
||||
|
||||
# Financial reports
|
||||
sprawozdania_finansowe: List[KRSFinancialReport] = field(default_factory=list)
|
||||
|
||||
# Metadata
|
||||
zrodlo: str = "ekrs.ms.gov.pl"
|
||||
data_pobrania: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for JSON serialization"""
|
||||
def convert_value(v):
|
||||
if isinstance(v, (date, datetime)):
|
||||
return v.isoformat()
|
||||
if isinstance(v, Decimal):
|
||||
return float(v)
|
||||
if hasattr(v, '__dict__'):
|
||||
return {k: convert_value(val) for k, val in asdict(v).items()}
|
||||
if isinstance(v, list):
|
||||
return [convert_value(item) for item in v]
|
||||
return v
|
||||
|
||||
return {k: convert_value(v) for k, v in asdict(self).items()}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# PDF PARSING FUNCTIONS
|
||||
# ============================================================
|
||||
|
||||
def extract_text_from_pdf(pdf_path: str) -> str:
|
||||
"""Extract full text from PDF"""
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
text = ""
|
||||
for page in pdf.pages:
|
||||
page_text = page.extract_text()
|
||||
if page_text:
|
||||
text += page_text + "\n"
|
||||
return text
|
||||
|
||||
|
||||
def parse_date(date_str: str) -> Optional[date]:
|
||||
"""Parse date from various formats"""
|
||||
if not date_str:
|
||||
return None
|
||||
|
||||
# Try DD.MM.YYYY format
|
||||
match = re.search(r'(\d{2})\.(\d{2})\.(\d{4})', date_str)
|
||||
if match:
|
||||
try:
|
||||
return date(int(match.group(3)), int(match.group(2)), int(match.group(1)))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Try YYYY-MM-DD format
|
||||
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', date_str)
|
||||
if match:
|
||||
try:
|
||||
return date(int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_money(money_str: str) -> Optional[Decimal]:
|
||||
"""Parse money value from Polish format (e.g., '4.000,00' or '5 000,00')"""
|
||||
if not money_str:
|
||||
return None
|
||||
|
||||
# Remove spaces
|
||||
cleaned = money_str.replace(' ', '')
|
||||
|
||||
# Handle Polish format: dot as thousands separator, comma as decimal separator
|
||||
# e.g., "4.000,00" should become "4000.00"
|
||||
# First, check if we have Polish format (comma present)
|
||||
if ',' in cleaned:
|
||||
# Remove dots (thousands separators) and replace comma with dot (decimal separator)
|
||||
cleaned = cleaned.replace('.', '').replace(',', '.')
|
||||
else:
|
||||
# US format or simple integer - dots are decimal separators
|
||||
pass
|
||||
|
||||
# Extract number
|
||||
match = re.search(r'([\d\.]+)', cleaned)
|
||||
if match:
|
||||
try:
|
||||
return Decimal(match.group(1))
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_shares_info(shares_str: str) -> Tuple[Optional[int], Optional[Decimal]]:
|
||||
"""Parse shares count and value from string like '80 UDZIAŁÓW O ŁĄCZNEJ WARTOŚCI 4.000,00 ZŁ'"""
|
||||
count = None
|
||||
value = None
|
||||
|
||||
# Count pattern
|
||||
count_match = re.search(r'(\d+)\s+UDZIAŁ', shares_str, re.IGNORECASE)
|
||||
if count_match:
|
||||
count = int(count_match.group(1))
|
||||
|
||||
# Value pattern
|
||||
value_match = re.search(r'WARTOŚCI\s+([\d\s,\.]+)\s*ZŁ', shares_str, re.IGNORECASE)
|
||||
if value_match:
|
||||
value = parse_money(value_match.group(1))
|
||||
|
||||
return count, value
|
||||
|
||||
|
||||
def extract_person_block(lines: List[str], start_idx: int, role_category: str) -> Optional[KRSPerson]:
|
||||
"""Extract person data from PDF text block"""
|
||||
person = KRSPerson(nazwisko="", imiona="", rola_kategoria=role_category)
|
||||
found_nazwisko = False
|
||||
found_imiona = False
|
||||
|
||||
for i in range(start_idx, min(start_idx + 15, len(lines))):
|
||||
line = lines[i].strip()
|
||||
|
||||
# Stop at next person block
|
||||
if i > start_idx and ('1.Nazwisko' in line or 'Nazwisko / Nazwa' in line):
|
||||
break
|
||||
|
||||
# Nazwisko
|
||||
if not found_nazwisko and 'Nazwisko' in line and ' - ' in line:
|
||||
match = re.search(r' - ([A-ZĄĆĘŁŃÓŚŹŻ\-]+)$', line)
|
||||
if match:
|
||||
person.nazwisko = match.group(1)
|
||||
found_nazwisko = True
|
||||
|
||||
# Imiona
|
||||
if not found_imiona and 'Imiona' in line and ' - ' in line:
|
||||
match = re.search(r' - ([A-ZĄĆĘŁŃÓŚŹŻ ]+)$', line)
|
||||
if match:
|
||||
person.imiona = match.group(1).strip()
|
||||
found_imiona = True
|
||||
|
||||
# PESEL
|
||||
if 'PESEL' in line and ' - ' in line:
|
||||
match = re.search(r' - (\d{11})', line)
|
||||
if match:
|
||||
person.pesel = match.group(1)
|
||||
|
||||
# Function (for board members)
|
||||
if 'Funkcja' in line and ' - ' in line:
|
||||
match = re.search(r' - ([A-ZĄĆĘŁŃÓŚŹŻ ]+)$', line)
|
||||
if match:
|
||||
person.rola = match.group(1).strip()
|
||||
|
||||
# Shares (for shareholders)
|
||||
if 'udziały' in line.lower() and ' - ' in line:
|
||||
match = re.search(r' - (.+)$', line)
|
||||
if match:
|
||||
shares_str = match.group(1)
|
||||
count, value = parse_shares_info(shares_str)
|
||||
person.udzialy_liczba = count
|
||||
person.udzialy_wartosc = value
|
||||
|
||||
if person.nazwisko and person.imiona:
|
||||
return person
|
||||
return None
|
||||
|
||||
|
||||
def parse_krs_pdf_full(pdf_path: str) -> KRSFullData:
|
||||
"""
|
||||
Parse KRS PDF and extract all available data.
|
||||
|
||||
Args:
|
||||
pdf_path: Path to KRS PDF file
|
||||
|
||||
Returns:
|
||||
KRSFullData object with all extracted data
|
||||
"""
|
||||
text = extract_text_from_pdf(pdf_path)
|
||||
lines = text.split('\n')
|
||||
|
||||
data = KRSFullData(krs="", data_pobrania=datetime.now())
|
||||
|
||||
# === Basic identifiers ===
|
||||
|
||||
# KRS number
|
||||
krs_match = re.search(r'Numer KRS:\s*(\d{10})', text)
|
||||
if krs_match:
|
||||
data.krs = krs_match.group(1)
|
||||
|
||||
# NIP and REGON
|
||||
nip_match = re.search(r'NIP:\s*(\d{10})', text)
|
||||
if nip_match:
|
||||
data.nip = nip_match.group(1)
|
||||
|
||||
regon_match = re.search(r'REGON:\s*(\d{9,14})', text)
|
||||
if regon_match:
|
||||
data.regon = regon_match.group(1)
|
||||
|
||||
# === Company name and form ===
|
||||
|
||||
# Company name
|
||||
nazwa_match = re.search(r'3\.Firma,?\s+pod którą spółka działa\s+\d+\s+-\s+([^\n]+)', text)
|
||||
if nazwa_match:
|
||||
data.nazwa = nazwa_match.group(1).strip()
|
||||
|
||||
# Legal form
|
||||
forma_match = re.search(r'1\.Oznaczenie formy prawnej\s+\d+\s+-\s+([^\n]+)', text)
|
||||
if forma_match:
|
||||
data.forma_prawna = forma_match.group(1).strip()
|
||||
|
||||
# OPP status
|
||||
opp_match = re.search(r'status organizacji\s*pożytku publicznego\?\s+\d+\s+-\s+(TAK|NIE)', text, re.IGNORECASE)
|
||||
if opp_match:
|
||||
data.czy_opp = opp_match.group(1).upper() == 'TAK'
|
||||
|
||||
# === Registration date ===
|
||||
|
||||
# Find first entry date
|
||||
reg_match = re.search(r'Nr wpisu 1 Data dokonania wpisu (\d{2}\.\d{2}\.\d{4})', text)
|
||||
if reg_match:
|
||||
data.data_rejestracji = parse_date(reg_match.group(1))
|
||||
|
||||
# Company agreement date
|
||||
umowa_match = re.search(r'Informacja o zawarciu.+?(\d{2}\.\d{2}\.\d{4})', text, re.DOTALL)
|
||||
if umowa_match:
|
||||
data.data_umowy_spolki = parse_date(umowa_match.group(1))
|
||||
|
||||
# Duration
|
||||
czas_match = re.search(r'Czas,?\s+na jaki została utworzona spółka\s+\d+\s+-\s+([^\n]+)', text)
|
||||
if czas_match:
|
||||
data.czas_trwania = czas_match.group(1).strip()
|
||||
|
||||
# === Address ===
|
||||
|
||||
data.siedziba = KRSAddress()
|
||||
|
||||
# Full address line
|
||||
adres_match = re.search(
|
||||
r'2\.Adres\s+\d+\s+-\s+ul\.\s*([^,]+),\s*nr\s*(\d+\w*)'
|
||||
r'(?:,\s*lok\.\s*([^,]+))?'
|
||||
r',\s*miejsc\.\s*([^,]+)'
|
||||
r',\s*kod\s*(\d{2}-\d{3})'
|
||||
r',\s*poczta\s*([^,]+)',
|
||||
text, re.IGNORECASE
|
||||
)
|
||||
if adres_match:
|
||||
data.siedziba.ulica = adres_match.group(1).strip()
|
||||
data.siedziba.numer_domu = adres_match.group(2).strip()
|
||||
if adres_match.group(3):
|
||||
data.siedziba.numer_lokalu = adres_match.group(3).strip()
|
||||
data.siedziba.miejscowosc = adres_match.group(4).strip()
|
||||
data.siedziba.kod_pocztowy = adres_match.group(5).strip()
|
||||
data.siedziba.poczta = adres_match.group(6).strip()
|
||||
|
||||
# Siedziba (province info)
|
||||
siedziba_match = re.search(
|
||||
r'1\.Siedziba\s+\d+\s+-\s+kraj\s+POLSKA,\s*woj\.\s*([^,]+),\s*powiat\s*([^,]+),\s*gmina\s*([^,]+)',
|
||||
text, re.IGNORECASE
|
||||
)
|
||||
if siedziba_match:
|
||||
data.siedziba.wojewodztwo = siedziba_match.group(1).strip()
|
||||
data.siedziba.powiat = siedziba_match.group(2).strip()
|
||||
data.siedziba.gmina = siedziba_match.group(3).strip()
|
||||
|
||||
# Email
|
||||
email_match = re.search(r'3\.Adres poczty elektronicznej\s+\d+\s+-\s+([^\n]+)', text)
|
||||
if email_match:
|
||||
data.siedziba.email = email_match.group(1).strip()
|
||||
|
||||
# Website
|
||||
www_match = re.search(r'4\.Adres strony internetowej\s+\d+\s+-\s+([^\n]+)', text)
|
||||
if www_match:
|
||||
data.siedziba.www = www_match.group(1).strip()
|
||||
|
||||
# === Capital ===
|
||||
|
||||
kapital_match = re.search(r'1\.Wysokość kapitału zakładowego\s+\d+\s+-\s+([\d\s,\.]+)\s*ZŁ', text, re.IGNORECASE)
|
||||
if kapital_match:
|
||||
data.kapital_zakladowy = parse_money(kapital_match.group(1))
|
||||
|
||||
# === Representation rules ===
|
||||
|
||||
repr_match = re.search(r'2\.Sposób reprezentacji podmiotu\s+\d+\s+-\s+([^\n]+(?:\n[^\n]*)?)', text)
|
||||
if repr_match:
|
||||
# Clean up multiline representation
|
||||
repr_text = repr_match.group(1).strip()
|
||||
# Remove line breaks and extra spaces
|
||||
repr_text = ' '.join(repr_text.split())
|
||||
data.sposob_reprezentacji = repr_text
|
||||
|
||||
# === PKD codes ===
|
||||
|
||||
# Main PKD
|
||||
pkd_glowny_match = re.search(
|
||||
r'1\.Przedmiot przeważającej\s+\d+\s+\d+\s+-\s+(\d+),\s*(\d+),\s*([A-Z]),\s*([^\n]+)',
|
||||
text
|
||||
)
|
||||
if pkd_glowny_match:
|
||||
kod = f"{pkd_glowny_match.group(1)}.{pkd_glowny_match.group(2)}.{pkd_glowny_match.group(3)}"
|
||||
opis = pkd_glowny_match.group(4).strip()
|
||||
data.pkd_przewazajacy = KRSPKD(kod=kod, opis=opis, jest_przewazajacy=True)
|
||||
|
||||
# Secondary PKDs
|
||||
pkd_pozostale = re.findall(
|
||||
r'2\.Przedmiot pozostałej działalności\s+\d+\s+\d+\s+-\s+(\d+),\s*(\d+),\s*([A-Z]),\s*([^\n]+)',
|
||||
text
|
||||
)
|
||||
for match in pkd_pozostale:
|
||||
kod = f"{match[0]}.{match[1]}.{match[2]}"
|
||||
opis = match[3].strip()
|
||||
data.pkd_pozostale.append(KRSPKD(kod=kod, opis=opis, jest_przewazajacy=False))
|
||||
|
||||
# === People ===
|
||||
|
||||
in_zarzad = False
|
||||
in_wspolnicy = False
|
||||
in_prokurenci = False
|
||||
in_rada = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
line_stripped = line.strip()
|
||||
|
||||
# Section detection
|
||||
if 'ZARZĄD' in line_stripped.upper() and 'Nazwa organu' in line_stripped:
|
||||
in_zarzad = True
|
||||
in_wspolnicy = False
|
||||
in_prokurenci = False
|
||||
in_rada = False
|
||||
continue
|
||||
|
||||
if 'Dane wspólników' in line_stripped or ('Rubryka 7' in line_stripped and 'wspólników' in line_stripped.lower()):
|
||||
in_wspolnicy = True
|
||||
in_zarzad = False
|
||||
in_prokurenci = False
|
||||
in_rada = False
|
||||
continue
|
||||
|
||||
if 'Prokurenci' in line_stripped:
|
||||
in_prokurenci = True
|
||||
in_zarzad = False
|
||||
in_wspolnicy = False
|
||||
in_rada = False
|
||||
continue
|
||||
|
||||
if 'Organ nadzoru' in line_stripped:
|
||||
in_rada = True
|
||||
in_zarzad = False
|
||||
in_wspolnicy = False
|
||||
in_prokurenci = False
|
||||
continue
|
||||
|
||||
# New section - reset
|
||||
if 'Dział' in line_stripped and re.match(r'Dział \d+', line_stripped):
|
||||
if 'Dział 2' not in line_stripped and 'Dział 1' not in line_stripped:
|
||||
in_zarzad = False
|
||||
in_wspolnicy = False
|
||||
in_prokurenci = False
|
||||
in_rada = False
|
||||
|
||||
# Parse person when we find "Nazwisko"
|
||||
if '1.Nazwisko' in line_stripped or 'Nazwisko / Nazwa' in line_stripped:
|
||||
if in_zarzad:
|
||||
person = extract_person_block(lines, i, 'zarzad')
|
||||
if person:
|
||||
if not person.rola:
|
||||
person.rola = "CZŁONEK ZARZĄDU"
|
||||
data.zarzad.append(person)
|
||||
elif in_wspolnicy:
|
||||
person = extract_person_block(lines, i, 'wspolnik')
|
||||
if person:
|
||||
person.rola = "WSPÓLNIK"
|
||||
data.wspolnicy.append(person)
|
||||
elif in_prokurenci:
|
||||
person = extract_person_block(lines, i, 'prokurent')
|
||||
if person:
|
||||
person.rola = "PROKURENT"
|
||||
data.prokurenci.append(person)
|
||||
elif in_rada:
|
||||
person = extract_person_block(lines, i, 'rada_nadzorcza')
|
||||
if person:
|
||||
person.rola = "CZŁONEK RADY NADZORCZEJ"
|
||||
data.rada_nadzorcza.append(person)
|
||||
|
||||
# Calculate share percentage for shareholders
|
||||
if data.kapital_zakladowy and data.wspolnicy:
|
||||
for wspolnik in data.wspolnicy:
|
||||
if wspolnik.udzialy_wartosc:
|
||||
wspolnik.udzialy_procent = Decimal(str(
|
||||
round(float(wspolnik.udzialy_wartosc) / float(data.kapital_zakladowy) * 100, 2)
|
||||
))
|
||||
|
||||
# Calculate total shares
|
||||
total_shares = sum(w.udzialy_liczba or 0 for w in data.wspolnicy)
|
||||
if total_shares > 0:
|
||||
data.liczba_udzialow = total_shares
|
||||
if data.kapital_zakladowy:
|
||||
data.wartosc_nominalna_udzialu = Decimal(str(
|
||||
round(float(data.kapital_zakladowy) / total_shares, 2)
|
||||
))
|
||||
|
||||
# === Financial reports ===
|
||||
|
||||
# Parse financial report filings - multiple patterns needed
|
||||
# Format 1: "1.Wzmianka o złożeniu rocznego 1 4 - 22.06.2022 OD 05.02.2021 DO 31.12.2021"
|
||||
# Format 2: "2 6 - 20.06.2023 OD 01.01.2022 DO 31.12.2022" (continuation lines)
|
||||
report_matches = re.findall(
|
||||
r'(\d{2}\.\d{2}\.\d{4})\s+OD\s+(\d{2}\.\d{2}\.\d{4})\s+DO\s+(\d{2}\.\d{2}\.\d{4})',
|
||||
text
|
||||
)
|
||||
seen_reports = set()
|
||||
for match in report_matches:
|
||||
# Deduplicate by period
|
||||
key = (match[1], match[2])
|
||||
if key not in seen_reports:
|
||||
seen_reports.add(key)
|
||||
report = KRSFinancialReport(
|
||||
data_zlozenia=parse_date(match[0]),
|
||||
okres_od=parse_date(match[1]),
|
||||
okres_do=parse_date(match[2])
|
||||
)
|
||||
data.sprawozdania_finansowe.append(report)
|
||||
|
||||
# Sort by period end date
|
||||
data.sprawozdania_finansowe.sort(key=lambda r: r.okres_do or date.min)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MAIN ENTRY POINT
|
||||
# ============================================================
|
||||
|
||||
def parse_krs_pdf(pdf_path: str, verbose: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse KRS PDF and return dictionary with all data.
|
||||
|
||||
This is the main entry point for external use.
|
||||
|
||||
Args:
|
||||
pdf_path: Path to KRS PDF file
|
||||
verbose: Print debug information
|
||||
|
||||
Returns:
|
||||
Dictionary with all extracted data
|
||||
"""
|
||||
if verbose:
|
||||
logger.info(f"Parsing: {pdf_path}")
|
||||
|
||||
data = parse_krs_pdf_full(pdf_path)
|
||||
|
||||
if verbose:
|
||||
logger.info(f" KRS: {data.krs}")
|
||||
logger.info(f" Nazwa: {data.nazwa}")
|
||||
logger.info(f" NIP: {data.nip}, REGON: {data.regon}")
|
||||
logger.info(f" Kapitał: {data.kapital_zakladowy} {data.waluta}")
|
||||
logger.info(f" Zarząd: {len(data.zarzad)} osób")
|
||||
logger.info(f" Wspólnicy: {len(data.wspolnicy)} osób")
|
||||
if data.pkd_przewazajacy:
|
||||
logger.info(f" PKD główny: {data.pkd_przewazajacy.kod}")
|
||||
|
||||
return data.to_dict()
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point for testing"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Parse KRS PDF files (full data extraction)")
|
||||
parser.add_argument("--file", type=str, help="Single PDF file to parse")
|
||||
parser.add_argument("--dir", type=str, help="Directory with PDF files")
|
||||
parser.add_argument("--output", type=str, help="Output JSON file")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
||||
args = parser.parse_args()
|
||||
|
||||
results = []
|
||||
|
||||
if args.file:
|
||||
data = parse_krs_pdf(args.file, verbose=args.verbose)
|
||||
results.append(data)
|
||||
|
||||
# Print summary
|
||||
print(f"\n{'='*60}")
|
||||
print(f"{data['nazwa']} (KRS: {data['krs']})")
|
||||
print(f"{'='*60}")
|
||||
print(f"NIP: {data['nip']}, REGON: {data['regon']}")
|
||||
print(f"Forma prawna: {data['forma_prawna']}")
|
||||
print(f"Data rejestracji: {data['data_rejestracji']}")
|
||||
|
||||
if data['siedziba']:
|
||||
addr = data['siedziba']
|
||||
print(f"Adres: {addr.get('ulica', '')} {addr.get('numer_domu', '')}, {addr.get('kod_pocztowy', '')} {addr.get('miejscowosc', '')}")
|
||||
if addr.get('email'):
|
||||
print(f"Email: {addr['email']}")
|
||||
if addr.get('www'):
|
||||
print(f"WWW: {addr['www']}")
|
||||
|
||||
print(f"\nKapitał zakładowy: {data['kapital_zakladowy']} {data['waluta']}")
|
||||
print(f"Liczba udziałów: {data['liczba_udzialow']}")
|
||||
print(f"Wartość nominalna: {data['wartosc_nominalna_udzialu']} {data['waluta']}")
|
||||
|
||||
print(f"\nSposób reprezentacji: {data['sposob_reprezentacji']}")
|
||||
|
||||
if data['pkd_przewazajacy']:
|
||||
print(f"\nPKD przeważający: {data['pkd_przewazajacy']['kod']} - {data['pkd_przewazajacy']['opis']}")
|
||||
|
||||
if data['pkd_pozostale']:
|
||||
print("PKD pozostałe:")
|
||||
for pkd in data['pkd_pozostale']:
|
||||
print(f" - {pkd['kod']}: {pkd['opis']}")
|
||||
|
||||
print(f"\nZarząd ({len(data['zarzad'])} osób):")
|
||||
for p in data['zarzad']:
|
||||
print(f" - {p['imiona']} {p['nazwisko']} - {p['rola']}")
|
||||
|
||||
print(f"\nWspólnicy ({len(data['wspolnicy'])} osób):")
|
||||
for p in data['wspolnicy']:
|
||||
shares_info = ""
|
||||
if p.get('udzialy_liczba'):
|
||||
shares_info = f" ({p['udzialy_liczba']} udziałów, {p.get('udzialy_procent', '?')}%)"
|
||||
print(f" - {p['imiona']} {p['nazwisko']}{shares_info}")
|
||||
|
||||
if data['prokurenci']:
|
||||
print(f"\nProkurenci ({len(data['prokurenci'])} osób):")
|
||||
for p in data['prokurenci']:
|
||||
print(f" - {p['imiona']} {p['nazwisko']}")
|
||||
|
||||
if data['sprawozdania_finansowe']:
|
||||
print(f"\nSprawozdania finansowe ({len(data['sprawozdania_finansowe'])}):")
|
||||
for sf in data['sprawozdania_finansowe']:
|
||||
print(f" - {sf['okres_od']} do {sf['okres_do']} (złożone: {sf['data_zlozenia']})")
|
||||
|
||||
elif args.dir:
|
||||
pdf_dir = Path(args.dir)
|
||||
pdf_files = list(pdf_dir.glob("*.pdf"))
|
||||
print(f"Found {len(pdf_files)} PDF files")
|
||||
|
||||
for pdf_file in pdf_files:
|
||||
try:
|
||||
data = parse_krs_pdf(str(pdf_file), verbose=args.verbose)
|
||||
results.append(data)
|
||||
print(f" ✓ {data['nazwa']} (KRS: {data['krs']})")
|
||||
except Exception as e:
|
||||
print(f" ✗ {pdf_file.name}: {e}")
|
||||
|
||||
# Save results
|
||||
if args.output and results:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
print(f"\nResults saved to: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
869
templates/admin/krs_audit_dashboard.html
Normal file
869
templates/admin/krs_audit_dashboard.html
Normal file
@ -0,0 +1,869 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Panel Audyt KRS - Norda Biznes Hub{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.admin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.admin-header h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.data-source-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--info-light, #e0f2fe);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--info, #0284c7);
|
||||
}
|
||||
|
||||
.data-source-info svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.data-source-info a {
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Summary Cards */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-number.green { color: var(--success); }
|
||||
.stat-number.yellow { color: var(--warning); }
|
||||
.stat-number.red { color: var(--error); }
|
||||
.stat-number.gray { color: var(--secondary); }
|
||||
.stat-number.blue { color: var(--primary); }
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Progress Section */
|
||||
.progress-section {
|
||||
background: white;
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 24px;
|
||||
background: var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
border-radius: 12px;
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.progress-log {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--background);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.progress-log-entry {
|
||||
padding: var(--spacing-xs) 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.progress-log-entry.success { color: var(--success); }
|
||||
.progress-log-entry.error { color: var(--error); }
|
||||
.progress-log-entry.skip { color: var(--warning); }
|
||||
|
||||
/* Filters */
|
||||
.filters-bar {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
background: white;
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-group select,
|
||||
.filter-group input {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Table Container */
|
||||
.table-container {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.krs-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.krs-table th,
|
||||
.krs-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.krs-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.krs-table tbody tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.company-name-cell {
|
||||
font-weight: 500;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.company-name-cell a {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.company-name-cell a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.krs-number {
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.audited {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
/* Data cell */
|
||||
.data-cell {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.capital-value {
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Date cell */
|
||||
.date-cell {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-never {
|
||||
color: var(--error);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--background);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-icon.audit {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.btn-icon.audit:hover {
|
||||
background: #dcfce7;
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.btn-icon.pdf {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.btn-icon.pdf:hover {
|
||||
background: #fee2e2;
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.btn-icon:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-icon.warning {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.modal-icon.success {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.krs-table {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
.hide-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="admin-header">
|
||||
<div>
|
||||
<h1>Panel Audyt KRS</h1>
|
||||
<p class="text-muted">Ekstrakcja danych z odpisow KRS (Krajowy Rejestr Sadowy)</p>
|
||||
<div class="data-source-info">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Dane z <a href="https://ekrs.ms.gov.pl/" target="_blank" rel="noopener">eKRS (ekrs.ms.gov.pl)</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{% if krs_audit_available %}
|
||||
<button class="btn btn-primary btn-sm" onclick="runBatchAudit()" id="batchAuditBtn">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Uruchom audyt wszystkich
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-muted">Usluga audytu niedostepna</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-number blue">{{ stats.total_with_krs }}</span>
|
||||
<span class="stat-label">Firm z KRS</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number green">{{ stats.audited_count }}</span>
|
||||
<span class="stat-label">Przeaudytowane</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number yellow">{{ stats.not_audited_count }}</span>
|
||||
<span class="stat-label">Oczekujace</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number gray">{{ stats.no_krs_count }}</span>
|
||||
<span class="stat-label">Bez KRS (JDG)</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{{ stats.with_capital }}</span>
|
||||
<span class="stat-label">Z kapitalem</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{{ stats.with_people }}</span>
|
||||
<span class="stat-label">Z zarzadem</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{{ stats.with_pkd }}</span>
|
||||
<span class="stat-label">Z PKD</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Section (hidden by default) -->
|
||||
<div class="progress-section" id="progressSection">
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">Audyt w toku...</span>
|
||||
<button class="btn btn-sm btn-outline" onclick="cancelAudit()">Anuluj</button>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar-fill" id="progressBar" style="width: 0%">0%</div>
|
||||
</div>
|
||||
<div class="progress-message" id="progressMessage">Przygotowywanie...</div>
|
||||
<div class="progress-log" id="progressLog"></div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label for="filterStatus">Status:</label>
|
||||
<select id="filterStatus" onchange="applyFilters()">
|
||||
<option value="">Wszystkie</option>
|
||||
<option value="audited">Przeaudytowane</option>
|
||||
<option value="pending">Oczekujace</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="filterSearch">Szukaj:</label>
|
||||
<input type="text" id="filterSearch" placeholder="Nazwa lub KRS..." oninput="applyFilters()">
|
||||
</div>
|
||||
<div class="filter-group" style="margin-left: auto;">
|
||||
<button class="btn btn-sm btn-outline" onclick="resetFilters()">Resetuj filtry</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
{% if companies %}
|
||||
<div class="table-container">
|
||||
<table class="krs-table" id="krsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Firma</th>
|
||||
<th>KRS</th>
|
||||
<th class="hide-mobile">Kapital</th>
|
||||
<th class="hide-mobile">Zarzad</th>
|
||||
<th class="hide-mobile">PKD</th>
|
||||
<th>Status</th>
|
||||
<th>Ostatni audyt</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="krsTableBody">
|
||||
{% for company in companies %}
|
||||
<tr data-name="{{ company.name|lower }}"
|
||||
data-krs="{{ company.krs }}"
|
||||
data-status="{{ 'audited' if company.krs_last_audit_at else 'pending' }}">
|
||||
<td class="company-name-cell">
|
||||
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="krs-number">{{ company.krs }}</span>
|
||||
</td>
|
||||
<td class="data-cell hide-mobile">
|
||||
{% if company.capital_amount %}
|
||||
<span class="capital-value">{{ "{:,.0f}".format(company.capital_amount|float).replace(",", " ") }} PLN</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="data-cell hide-mobile">
|
||||
{% if company.people_count > 0 %}
|
||||
<span class="data-value">{{ company.people_count }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="data-cell hide-mobile">
|
||||
{% if company.pkd_count > 0 %}
|
||||
<span class="data-value">{{ company.pkd_count }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if company.krs_last_audit_at %}
|
||||
<span class="status-badge audited">
|
||||
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
OK
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="status-badge pending">
|
||||
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Oczekuje
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="date-cell">
|
||||
{% if company.krs_last_audit_at %}
|
||||
<span title="{{ company.krs_last_audit_at.strftime('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.krs_last_audit_at.strftime('%d.%m.%Y') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="date-never">Nigdy</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn-icon" title="Zobacz profil">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% if company.krs_pdf_path %}
|
||||
<a href="{{ url_for('api_krs_pdf_download', company_id=company.id) }}" class="btn-icon pdf" title="Pobierz PDF" target="_blank">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if krs_audit_available %}
|
||||
<button class="btn-icon audit" onclick="runSingleAudit({{ company.id }}, '{{ company.name }}')" title="Uruchom audyt">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<h3>Brak firm z KRS</h3>
|
||||
<p>Nie znaleziono firm z numerem KRS do audytu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<div class="modal" id="confirmModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-icon warning">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="modal-title" id="modalTitle">Potwierdz operacje</div>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
Czy na pewno chcesz wykonac te operacje?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeModal()">Anuluj</button>
|
||||
<button class="btn btn-primary" onclick="confirmModalAction()">Potwierdz</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Modal -->
|
||||
<div class="modal" id="resultModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-icon success" id="resultIcon">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="modal-title" id="resultTitle">Sukces</div>
|
||||
</div>
|
||||
<div class="modal-body" id="resultBody">
|
||||
Operacja zakonczona pomyslnie.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="closeResultModal()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
const csrfToken = '{{ csrf_token() }}';
|
||||
let pendingModalAction = null;
|
||||
let auditInProgress = false;
|
||||
|
||||
// Modal functions
|
||||
function showModal(title, body, onConfirm) {
|
||||
document.getElementById('modalTitle').textContent = title;
|
||||
document.getElementById('modalBody').textContent = body;
|
||||
pendingModalAction = onConfirm;
|
||||
document.getElementById('confirmModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('confirmModal').classList.remove('active');
|
||||
pendingModalAction = null;
|
||||
}
|
||||
|
||||
function confirmModalAction() {
|
||||
if (pendingModalAction) {
|
||||
pendingModalAction();
|
||||
}
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function showResultModal(title, body, success = true) {
|
||||
document.getElementById('resultTitle').textContent = title;
|
||||
document.getElementById('resultBody').textContent = body;
|
||||
const icon = document.getElementById('resultIcon');
|
||||
icon.className = 'modal-icon ' + (success ? 'success' : 'warning');
|
||||
document.getElementById('resultModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeResultModal() {
|
||||
document.getElementById('resultModal').classList.remove('active');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// Close modal on backdrop click
|
||||
document.getElementById('confirmModal')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'confirmModal') closeModal();
|
||||
});
|
||||
document.getElementById('resultModal')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'resultModal') closeResultModal();
|
||||
});
|
||||
|
||||
// Filter functions
|
||||
function applyFilters() {
|
||||
const status = document.getElementById('filterStatus').value;
|
||||
const search = document.getElementById('filterSearch').value.toLowerCase();
|
||||
|
||||
const rows = document.querySelectorAll('#krsTableBody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
let show = true;
|
||||
|
||||
// Status filter
|
||||
if (status && row.dataset.status !== status) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (search && show) {
|
||||
const name = row.dataset.name || '';
|
||||
const krs = row.dataset.krs || '';
|
||||
if (!name.includes(search) && !krs.includes(search)) {
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
|
||||
row.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
document.getElementById('filterStatus').value = '';
|
||||
document.getElementById('filterSearch').value = '';
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Audit functions
|
||||
async function runSingleAudit(companyId, companyName) {
|
||||
showModal(
|
||||
'Uruchom audyt KRS',
|
||||
`Czy chcesz uruchomic audyt KRS dla firmy "${companyName}"? Plik PDF musi byc dostepny w katalogu data/krs_pdfs/.`,
|
||||
async () => {
|
||||
try {
|
||||
const response = await fetch('/api/krs/audit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ company_id: companyId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
showResultModal(
|
||||
'Audyt zakonczon',
|
||||
`Pomyslnie wyciagnieto dane dla ${companyName}. ` +
|
||||
`Kapital: ${data.data?.kapital?.toLocaleString() || '-'} PLN, ` +
|
||||
`Zarzad: ${data.data?.zarzad_count || 0} osob, ` +
|
||||
`PKD: ${data.data?.pkd_count || 0}`,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
showResultModal('Blad', data.error || 'Wystapil nieznany blad', false);
|
||||
}
|
||||
} catch (error) {
|
||||
showResultModal('Blad polaczenia', 'Nie udalo sie polaczyc z serwerem: ' + error.message, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function runBatchAudit() {
|
||||
showModal(
|
||||
'Uruchom audyt wszystkich firm',
|
||||
'Czy chcesz uruchomic audyt KRS dla wszystkich firm? To moze potrwac kilka minut.',
|
||||
async () => {
|
||||
auditInProgress = true;
|
||||
const btn = document.getElementById('batchAuditBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>Audyt w toku...</span>';
|
||||
|
||||
const progressSection = document.getElementById('progressSection');
|
||||
progressSection.classList.add('active');
|
||||
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressMessage = document.getElementById('progressMessage');
|
||||
const progressLog = document.getElementById('progressLog');
|
||||
|
||||
progressBar.style.width = '5%';
|
||||
progressBar.textContent = '5%';
|
||||
progressMessage.textContent = 'Rozpoczynanie audytu...';
|
||||
progressLog.innerHTML = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/krs/audit/batch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = '100%';
|
||||
progressMessage.textContent = data.message;
|
||||
|
||||
// Show details in log
|
||||
if (data.results && data.results.details) {
|
||||
data.results.details.forEach(item => {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'progress-log-entry ' + item.status;
|
||||
entry.textContent = `${item.company} (${item.krs}): ${item.status}${item.reason ? ' - ' + item.reason : ''}`;
|
||||
progressLog.appendChild(entry);
|
||||
});
|
||||
}
|
||||
|
||||
showResultModal(
|
||||
'Audyt zakonczony',
|
||||
`Sukces: ${data.results?.success || 0}, Bledy: ${data.results?.failed || 0}, Pominiete: ${data.results?.skipped || 0}`,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
showResultModal('Blad', data.error || 'Wystapil nieznany blad', false);
|
||||
}
|
||||
} catch (error) {
|
||||
showResultModal('Blad polaczenia', 'Nie udalo sie polaczyc z serwerem: ' + error.message, false);
|
||||
} finally {
|
||||
auditInProgress = false;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Uruchom audyt wszystkich
|
||||
`;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function cancelAudit() {
|
||||
// Note: Currently can't cancel - just hide progress section
|
||||
document.getElementById('progressSection').classList.remove('active');
|
||||
}
|
||||
{% endblock %}
|
||||
@ -1031,7 +1031,15 @@
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">KRS</div>
|
||||
<div style="font-size: var(--font-size-xl); font-weight: 700; color: #ef4444; font-family: monospace;">{{ company.krs }}</div>
|
||||
</div>
|
||||
<div style="margin-left: auto;">
|
||||
<div style="margin-left: auto; display: flex; gap: var(--spacing-sm); flex-wrap: wrap;">
|
||||
{% if company.krs_pdf_path %}
|
||||
<a href="/api/krs/pdf/{{ company.id }}" target="_blank" rel="noopener noreferrer"
|
||||
style="padding: 8px 16px; background: #ef4444; color: white; border-radius: var(--radius); text-decoration: none; font-size: var(--font-size-sm); font-weight: 600; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;"
|
||||
title="Pobierz odpis pełny KRS w formacie PDF">
|
||||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M10,13H7V11H10V13M14,13H11V11H14V13M10,16H7V14H10V16M14,16H11V14H14V16Z"/></svg>
|
||||
Odpis PDF
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="https://rejestr.io/krs/{{ company.krs|int }}" target="_blank" rel="noopener noreferrer"
|
||||
style="padding: 8px 16px; background: #10b981; color: white; border-radius: var(--radius); text-decoration: none; font-size: var(--font-size-sm); font-weight: 600; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px;"
|
||||
title="Powiązania osobowe, władze, beneficjenci, pomoc publiczna">
|
||||
@ -1042,6 +1050,12 @@
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); padding-left: 60px; margin-top: var(--spacing-sm);">
|
||||
Źródło: <strong style="color: #10b981;">Krajowy Rejestr Sądowy</strong>
|
||||
{% if company.krs_last_audit_at %}
|
||||
<span style="margin-left: var(--spacing-md); color: #6366f1;">
|
||||
<svg width="12" height="12" fill="currentColor" viewBox="0 0 24 24" style="vertical-align: middle;"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
|
||||
Audyt KRS: {{ company.krs_last_audit_at.strftime('%d.%m.%Y') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user