import os import markdown import datetime from bs4 import BeautifulSoup def export_to_docx( content: str, output_path: str, template: str = "standard", project_title: str = "Wniosek o Dofinansowanie", company_name: str = "Brak nazwy", version: str = "1.0", date_str: str = "", extra_context: dict = None, ): """ Eksportuje wygenerowany przez Wizarda wniosek do formatu Microsoft Word (DOCX). Wczytuje gotowy szablon docx z wymaganymi stalami i spisem treści. Integruje się z `docxtpl` by umożliwić elastyczne wstrzykiwanie zmiennych (np {{ beneficjent.krs }}). """ if extra_context is None: extra_context = {} try: from docxtpl import DocxTemplate template_name = ( template if template in ["standard", "official", "modern"] else "standard" ) template_path = os.path.join( os.path.dirname(__file__), "..", "templates", f"template_{template_name}.docx", ) if not os.path.exists(template_path): print(f"Brak pliku {template_path}, upewnij się, że wygenerowano szablony!") return False tpl = DocxTemplate(template_path) # Puste wartości dla formatowania markdown (usuwamy duplikujące title jeśli zaczyna się od H1) if content.startswith("# "): content = "\n".join(content.split("\n")[1:]) # Przygotowanie pełnego kontekstu dla DocxTemplate (Jinja2 tags) # Zostawiamy 'tresc_wniosku' puste, bo zastąpimy ten paragraf natywnym kodem python-docx render_context = { "tytul_projektu": project_title, "nazwa_firmy": company_name, "data_generowania": date_str, "wersja": version, "tresc_wniosku": "", } render_context.update(extra_context) tpl.render(render_context) # === Pełna integracja Świadectwa Zgodności w eksporcie (Cycle 4) === if extra_context and extra_context.get("include_grounding_certificate"): append_grounding_certificate_to_docx(tpl._document, { "title": project_title, "company_name": company_name, "version_hash": extra_context.get("version_hash"), "precise_regulation_url": extra_context.get("precise_regulation_url"), "regulation_link_quality": extra_context.get("regulation_link_quality"), "snapshot_data": extra_context.get("snapshot_data"), }) tpl.save(output_path) # 2. Otwieramy zapisany plik za pomocą natywnego python-docx import docx doc = docx.Document(output_path) # Usuwamy ostatni paragraf (który zawierał wyczyszczoną zmienną 'tresc_wniosku') if len(doc.paragraphs) > 0 and doc.paragraphs[-1].text.strip() == "": p = doc.paragraphs[-1] p._element.getparent().remove(p._element) # Konwersja MD do prostego HTML, a potem interpretacja BeautifulSoup html = markdown.markdown(content) soup = BeautifulSoup(html, "html.parser") for element in soup: if element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]: level = int(element.name[1]) try: doc.add_paragraph(element.text, style=f"Heading {level}") except KeyError: doc.add_heading(element.text, level=level) elif element.name == "p": # Złożona obsługa bold/italic p = doc.add_paragraph() if template == "official": p.paragraph_format.alignment = 3 # Justify for child in element.children: if child.name is None: p.add_run(child.string) elif child.name in ["strong", "b"]: p.add_run(child.text).bold = True elif child.name in ["em", "i"]: p.add_run(child.text).italic = True else: p.add_run(child.text) elif element.name in ["ul", "ol"]: for li in element.find_all("li"): style_name = ( "List Bullet" if element.name == "ul" else "List Number" ) try: p = doc.add_paragraph(style=style_name) except KeyError: p = doc.add_paragraph(style="Normal") p.add_run("• ") for child in li.children: if child.name is None: p.add_run(child.string) elif child.name in ["strong", "b"]: p.add_run(child.text).bold = True elif child.name in ["em", "i"]: p.add_run(child.text).italic = True else: p.add_run(child.text) elif element.name == "table": # Ulepszona obsługa tabel dla DOCX rows = element.find_all("tr") if rows: cols = max(len(row.find_all(["th", "td"])) for row in rows) table = doc.add_table(rows=0, cols=cols) try: table.style = "Light Shading Accent 1" except KeyError: table.style = "Table Grid" for idx_row, tr in enumerate(rows): row = table.add_row() cells = tr.find_all(["th", "td"]) for idx, cell in enumerate(cells): if idx < cols: p = row.cells[idx].paragraphs[0] p.text = "" for child in cell.children: if child.name is None: p.add_run(child.string.strip() if child.string else "") elif child.name in ["strong", "b"]: p.add_run(child.text.strip()).bold = True elif child.name in ["em", "i"]: p.add_run(child.text.strip()).italic = True else: p.add_run(child.text.strip()) # Zawsze pogrubiamy nagłówki ( lub pierwszy wiersz) if cell.name == "th" or idx_row == 0: for run in p.runs: run.bold = True doc.save(output_path) return True except Exception: import traceback print(f"Błąd eksportu do DOCX: {traceback.format_exc()}") return False def append_grounding_certificate_to_docx(doc, project_data: dict): """Dodaje stronę Świadectwa Zgodności na końcu dokumentu DOCX.""" from docx.shared import Pt, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH doc.add_page_break() # Nagłówek świadectwa p = doc.add_paragraph() run = p.add_run("ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE") run.bold = True run.font.size = Pt(14) run.font.color.rgb = RGBColor(26, 54, 93) p.alignment = WD_ALIGN_PARAGRAPH.CENTER p2 = doc.add_paragraph("GrantForge AI — Najwyższy poziom ugruntowania w regulaminach i prawie UE") p2.alignment = WD_ALIGN_PARAGRAPH.CENTER doc.add_paragraph() # Kluczowe dane cert_info = [ ("Projekt", project_data.get("title", "")), ("Beneficjent", project_data.get("company_name", "")), ("Data wygenerowania", datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")), ("Version Hash", project_data.get("version_hash", "N/A")), ("Precise Regulation URL", project_data.get("precise_regulation_url", "N/A")), ("Regulation Link Quality", project_data.get("regulation_link_quality", "N/A")), ] for label, value in cert_info: p = doc.add_paragraph() run = p.add_run(f"{label}: ") run.bold = True p.add_run(str(value)[:120]) # Snapshot summary jeśli dostępny if project_data.get("snapshot_data"): doc.add_paragraph() p = doc.add_paragraph() p.add_run("SNAPSHOT REGULAMINU (źródło prawdy)").bold = True for k, v in project_data["snapshot_data"].items(): if k in ["key_rules", "exclusions"]: continue doc.add_paragraph(f" {k}: {v}") doc.add_paragraph() p = doc.add_paragraph("Dokument ten potwierdza, że wszystkie wygenerowane treści opierają się na konkretnej, wersjonowanej wersji regulaminu oraz aktualnych aktach prawnych UE.") p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY p = doc.add_paragraph("https://grantforge.ai | Regulation Engine jako źródło prawdy") p.alignment = WD_ALIGN_PARAGRAPH.CENTER def export_full_application_with_certificate( output_path: str, project_title: str, company_name: str, main_content: str, certificate_data: dict, ) -> bool: """Eksportuje pełny wniosek + stronę Świadectwa Zgodności na końcu (PDF). Używane w cyklu finałowym.""" try: from reportlab.lib.pagesizes import A4 from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak from reportlab.lib.styles import getSampleStyleSheet doc = SimpleDocTemplate(output_path, pagesize=A4) styles = getSampleStyleSheet() story = [] # Główna treść wniosku (uproszczona) for line in (main_content or "").split("\n")[:80]: if line.strip(): story.append(Paragraph(line[:200], styles["Normal"])) story.append(PageBreak()) # Świadectwo cert = certificate_data or {} story.append(Paragraph("ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE", styles["Heading1"])) story.append(Spacer(1, 10)) story.append(Paragraph(f"Projekt: {project_title}", styles["Normal"])) story.append(Paragraph(f"Beneficjent: {company_name}", styles["Normal"])) story.append(Paragraph(f"Version Hash: {cert.get('version_hash', 'N/A')}", styles["Normal"])) story.append(Paragraph(f"Precise Regulation URL: {cert.get('precise_regulation_url', 'N/A')}", styles["Normal"])) story.append(Paragraph(f"Link Quality: {cert.get('regulation_link_quality', 'N/A')}", styles["Normal"])) doc.build(story) return True except Exception as e: print(f"Błąd eksportu PDF z certificate: {e}") return False def export_grounding_certificate_pdf( output_path: str, project_title: str, company_name: str, snapshot_data: dict, eurlex_links: list = None, pkd_list: list = None, msp_status: str = "", engine_checks: dict = None, version_hash: str = "", v5_certificate: dict = None, # v5.0 extended: citation_score, trap_risk, trust_score etc. ) -> bool: """ Generuje publiczne, profesjonalne Świadectwo Zgodności (Regulation Grounding Certificate) w formacie PDF. Zawiera wszystkie metadane snapshotu, hash, linki EUR-Lex, PKD, status MSP, wyniki silnika. To jest kluczowy element dla najwyższej wiarygodności — dokument można załączyć do wniosku lub pokazać instytucji. """ try: from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import cm from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable from reportlab.lib import colors from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY import datetime doc = SimpleDocTemplate( output_path, pagesize=A4, rightMargin=1.5*cm, leftMargin=1.5*cm, topMargin=1.5*cm, bottomMargin=1.5*cm ) styles = getSampleStyleSheet() title_style = ParagraphStyle( 'TitlePL', parent=styles['Heading1'], fontSize=16, alignment=TA_CENTER, spaceAfter=12, textColor=colors.HexColor('#1a365d') ) subtitle_style = ParagraphStyle( 'SubtitlePL', parent=styles['Normal'], fontSize=10, alignment=TA_CENTER, textColor=colors.HexColor('#2d3748') ) body_style = ParagraphStyle( 'BodyPL', parent=styles['Normal'], fontSize=9, alignment=TA_JUSTIFY, spaceAfter=6 ) small_style = ParagraphStyle( 'SmallPL', parent=styles['Normal'], fontSize=8, textColor=colors.gray ) story = [] # Header story.append(Paragraph("ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE", title_style)) story.append(Paragraph("GrantForge AI — Najwyższy poziom ugruntowania w aktualnych regulaminach i prawie UE", subtitle_style)) story.append(Spacer(1, 0.4*cm)) story.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor('#1a365d'))) story.append(Spacer(1, 0.3*cm)) # Basic info now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC") info_data = [ ["Projekt:", project_title], ["Beneficjent:", company_name], ["Data wygenerowania świadectwa:", now], ["Wersja silnika:", "Regulation Engine v3 + Live EUR-Lex"], ] info_table = Table(info_data, colWidths=[5*cm, 11*cm]) info_table.setStyle(TableStyle([ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 9), ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('BOTTOMPADDING', (0, 0), (-1, -1), 4), ])) story.append(info_table) story.append(Spacer(1, 0.3*cm)) # Snapshot section story.append(Paragraph("ŹRÓDŁO PRAWNE (SNAPSHOT REGULAMINU)", body_style)) if snapshot_data: snap_text = f""" Program: {snapshot_data.get('program', 'N/A')}
Snapshot ID: {snapshot_data.get('id', 'N/A')}
Version Hash (SHA256 skrót): {snapshot_data.get('version_hash') or version_hash or 'N/A'}
Data pobrania: {snapshot_data.get('fetched_at', 'N/A')}
Źródło: {snapshot_data.get('source_url', 'N/A')}
Wersja dokumentu: {snapshot_data.get('document_version', 'N/A')}
Obowiązuje od: {snapshot_data.get('effective_date', 'N/A')}
Instytucja: {snapshot_data.get('source_institution', 'N/A')}
Liczba kluczowych reguł: {len(snapshot_data.get('key_rules', []))}
Liczba wykluczeń: {len(snapshot_data.get('exclusions', []))} """ story.append(Paragraph(snap_text, body_style)) else: story.append(Paragraph("Brak snapshotu — dokument wygenerowany bez pełnego ugruntowania.", body_style)) story.append(Spacer(1, 0.3*cm)) # v5.0 Extended Grounding Certificate fields (citation/trap/trust) if v5_certificate: story.append(Paragraph("v5.0 MASTER ORCHESTRATOR GROUNDING CERTIFICATE (EXPORTABLE)", body_style)) v5_text = f""" Snapshot ID: {v5_certificate.get('snapshot_id', 'N/A')}
Version Hash: {v5_certificate.get('version_hash', version_hash or 'N/A')}
Effective Date: {v5_certificate.get('effective_date', 'N/A')}
Citation Score: {v5_certificate.get('citation_score', 0.0)}
Trap Risk (Kruczkowski): {v5_certificate.get('trap_risk', 'unknown')}
Trust Score: {v5_certificate.get('trust_score', 50)}/100
Data Quality Avg: {v5_certificate.get('data_quality_avg', 0.0)}
Timestamp: {v5_certificate.get('timestamp', '')}
Query Intent: {v5_certificate.get('query_intent', '')}
Synthesis: {str(v5_certificate.get('synthesis_summary', ''))[:180]} """ story.append(Paragraph(v5_text, body_style)) story.append(Spacer(1, 0.2*cm)) # EUR-Lex story.append(Paragraph("LIVE LEGAL CONTROL — EUR-LEX (ŹRÓDŁO PRAWY UE)", body_style)) if eurlex_links: for link in eurlex_links[:3]: story.append(Paragraph(f"• {link.get('title', '')} — {link.get('url', '')}", small_style)) else: story.append(Paragraph("Brak bezpośrednich linków EUR-Lex w tym świadectwie.", small_style)) story.append(Spacer(1, 0.3*cm)) # PKD + MSP story.append(Paragraph("UZIEMIENIE W DANYCH FIRMY (GUS + MSP)", body_style)) if pkd_list: story.append(Paragraph(f"Kody PKD wnioskodawcy (GUS): {', '.join(pkd_list[:5])}", body_style)) story.append(Paragraph(f"Status MŚP: {msp_status or 'Zweryfikowany przez GraphRAG'}", body_style)) story.append(Spacer(1, 0.3*cm)) # Engine checks if engine_checks: story.append(Paragraph("WYNIKI AKTYWNEJ WERYFIKACJI (REGULATION ENGINE)", body_style)) story.append(Paragraph(str(engine_checks), body_style)) story.append(Spacer(1, 0.5*cm)) story.append(HRFlowable(width="100%", thickness=1, color=colors.gray)) story.append(Paragraph( "To świadectwo potwierdza, że wszystkie wygenerowane treści opierają się na konkretnej, wersjonowanej wersji regulaminu + aktualnych aktach prawnych UE (EUR-Lex). " "Hash wersji pozwala na późniejszą weryfikację. Dokument jest integralną częścią wniosku i może być przedstawiony instytucji finansującej.", small_style )) story.append(Spacer(1, 0.3*cm)) story.append(Paragraph("GrantForge AI — Regulation Engine jako źródło prawdy | https://grantforge.ai", subtitle_style)) doc.build(story) return True except ImportError: print("Brak reportlab — zainstaluj: pip install reportlab") # Fallback: zapisz jako tekst with open(output_path.replace('.pdf', '.txt'), 'w', encoding='utf-8') as f: f.write(f"ŚWIADECTWO ZGODNOŚCI — {project_title}\nHash: {version_hash}\nSnapshot: {snapshot_data}\n") return False except Exception as e: print(f"Błąd generowania Świadectwa PDF: {e}") return False def get_pdf_css(template: str) -> str: if template == "official": return """ @page { size: A4; margin: 2.5cm; } body { font-family: "DejaVu Sans", "Arial", serif; font-size: 11pt; line-height: 1.5; text-align: justify; color: #000; } h1, h2, h3 { color: #000; page-break-after: avoid; font-family: "DejaVu Sans", "Arial", serif; } h1 { border-bottom: 2px solid #000; padding-bottom: 5px; text-transform: uppercase; text-align: center; font-size: 16pt; margin-top: 2em; } h2 { font-size: 14pt; margin-top: 1.5em; } h3 { font-size: 12pt; margin-top: 1.2em; font-style: italic; } table { width: 100%; border-collapse: collapse; margin: 1em 0; page-break-inside: avoid; } th, td { border: 1px solid #000; padding: 8px; text-align: left; } p { margin-bottom: 1em; orphans: 3; widows: 3; } a { color: #000; text-decoration: none; } .toc { page-break-after: always; } .toc ul { list-style-type: none; padding-left: 1.5em; } .toc > ul { padding-left: 0; } .toc a { text-decoration: none; color: #000; } """ elif template == "modern": return """ @page { size: A4; margin: 2.5cm; } body { font-family: "DejaVu Sans", "Arial", sans-serif; font-size: 11pt; line-height: 1.6; color: #1e293b; background: #fff; } h1, h2, h3 { page-break-after: avoid; color: #0f172a; font-family: "DejaVu Sans", "Arial", sans-serif; } h1 { border-bottom: 2px solid #3b82f6; padding-bottom: 0.5em; font-size: 24pt; margin-top: 1em; } h2 { border-bottom: 1px solid #e2e8f0; padding-bottom: 0.3em; font-size: 18pt; margin-top: 1.5em; color: #2563eb; } h3 { font-size: 14pt; margin-top: 1.2em; color: #334155; } p { margin-bottom: 1em; text-align: justify; orphans: 3; widows: 3; } table { width: 100%; border-collapse: collapse; margin: 1.5em 0; background: #f8fafc; font-size: 10pt; page-break-inside: avoid; } th, td { border: 1px solid #e2e8f0; padding: 10px 14px; text-align: left; } th { background-color: #e2e8f0; color: #1e293b; font-weight: bold; border-bottom: 2px solid #cbd5e1; } tr:nth-child(even) { background-color: #f1f5f9; } ul, ol { margin-bottom: 1em; padding-left: 2em; } li { margin-bottom: 0.5em; } .toc { page-break-after: always; padding: 2em; background: #f8fafc; border-radius: 8px; } .toc ul { list-style-type: none; padding-left: 1.5em; } .toc > ul { padding-left: 0; } .toc a { text-decoration: none; color: #4f46e5; border-bottom: 1px dotted #cbd5e1; display: block; padding-bottom: 5px; margin-bottom: 5px; } """ elif template == "enterprise": return """ @page { size: A4; margin: 2.5cm; } body { font-family: "DejaVu Sans", "Arial", sans-serif; font-size: 11pt; line-height: 1.6; color: #1f2937; background: #fff; } h1, h2, h3 { page-break-after: avoid; color: #1e3a8a; font-family: "DejaVu Sans", "Arial", sans-serif; } h1 { border-bottom: 2px solid #10b981; padding-bottom: 0.5em; font-size: 24pt; margin-top: 1em; } h2 { border-bottom: 1px solid #e5e7eb; padding-bottom: 0.3em; font-size: 18pt; margin-top: 1.5em; color: #1e40af; } h3 { font-size: 14pt; margin-top: 1.2em; color: #374151; } p { margin-bottom: 1em; text-align: justify; orphans: 3; widows: 3; } table { width: 100%; border-collapse: collapse; margin: 1.5em 0; background: #ffffff; } th, td { border: 1px solid #d1d5db; padding: 12px; text-align: left; } th { background-color: #f3f4f6; color: #1f2937; font-weight: bold; } ul, ol { margin-bottom: 1em; padding-left: 2em; } li { margin-bottom: 0.5em; } .toc { page-break-after: always; padding: 2em; background: #f9fafb; border-radius: 8px; border-left: 4px solid #10b981; } .toc ul { list-style-type: none; padding-left: 1.5em; } .toc > ul { padding-left: 0; } .toc a { text-decoration: none; color: #1e3a8a; border-bottom: 1px dotted #9ca3af; display: block; padding-bottom: 5px; margin-bottom: 5px; } """ else: # standard return """ @page { size: A4; margin: 2cm; } body { font-family: "DejaVu Sans", "Arial", sans-serif; font-size: 11pt; line-height: 1.6; color: #333; } h1, h2, h3 { page-break-after: avoid; } h1 { border-bottom: 2px solid #3498db; padding-bottom: 10px; color: #2c3e50; } h2 { color: #2980b9; margin-top: 1.5em; } table { width: 100%; border-collapse: collapse; margin: 1em 0; } th, td { border: 1px solid #bdc3c7; padding: 8px; text-align: left; } th { background-color: #ecf0f1; } p { margin-bottom: 1em; text-align: justify; orphans: 3; widows: 3; } .toc { page-break-after: always; margin-top: 2em; } .toc ul { list-style-type: none; padding-left: 1.5em; } .toc > ul { padding-left: 0; } .toc a { text-decoration: none; color: #2980b9; border-bottom: 1px dotted #bdc3c7; display: block; padding-bottom: 5px; margin-bottom: 5px; } """ def export_to_pdf( content: str, output_path: str, template: str = "standard", project_title: str = "Wniosek o Dofinansowanie", company_name: str = "Brak nazwy", version: str = "1.0", date_str: str = "", extra_context: dict = None, ): """ Eksportuje wniosek do PDF wykorzystując WeasyPrint. """ if extra_context is None: extra_context = {} try: from xhtml2pdf import pisa from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase import pdfmetrics import xhtml2pdf.default except ImportError: print("Nie mozna pobrac xhtml2pdf.") raise Exception("Należy zainstalować xhtml2pdf (pip install xhtml2pdf).") try: # Usuwamy ewentualny nadmiarowy title na poczatku markdown by nie dublowac cover page if content.startswith("# "): content = "\n".join(content.split("\n")[1:]) md_content = f"[TOC]\n\n{content}" html_body = markdown.markdown( md_content, extensions=["tables", "fenced_code", "toc"], extension_configs={'toc': {'title': 'Spis Treści'}} ) import urllib.request backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) font_dir = os.path.join(backend_dir, "assets") os.makedirs(font_dir, exist_ok=True) dejavu_path = os.path.join(backend_dir, "DejaVuSans.ttf") dejavu_bold_path = os.path.join(backend_dir, "DejaVuSans-Bold.ttf") if not os.path.exists(dejavu_path): try: urllib.request.urlretrieve( "https://cdn.jsdelivr.net/npm/@vintproykt/dejavu-fonts-ttf/ttf/DejaVuSans.ttf", dejavu_path, ) except Exception as e: print(f"Nie udało się pobrać DejaVuSans: {e}") if not os.path.exists(dejavu_bold_path): try: urllib.request.urlretrieve( "https://cdn.jsdelivr.net/npm/@vintproykt/dejavu-fonts-ttf/ttf/DejaVuSans-Bold.ttf", dejavu_bold_path, ) except Exception as e: print(f"Nie udało się pobrać DejaVuSans-Bold: {e}") font_face = "" if os.path.exists(dejavu_path): pdfmetrics.registerFont(TTFont("DejaVu Sans", dejavu_path)) if os.path.exists(dejavu_bold_path): pdfmetrics.registerFont(TTFont("DejaVu Sans-Bold", dejavu_bold_path)) xhtml2pdf.default.DEFAULT_FONT["helvetica"] = "DejaVu Sans" xhtml2pdf.default.DEFAULT_FONT["sans-serif"] = "DejaVu Sans" xhtml2pdf.default.DEFAULT_FONT["arial"] = "DejaVu Sans" font_face = f""" @font-face {{ font-family: "DejaVu Sans"; src: url("file://{dejavu_path}"); }} """ if os.path.exists(dejavu_bold_path): font_face += f""" @font-face {{ font-family: "DejaVu Sans"; font-weight: bold; src: url("file://{dejavu_bold_path}"); }} """ css_style = font_face + get_pdf_css(template) if not date_str: date_str = datetime.datetime.now().strftime("%d.%m.%Y") logo_html = "" if template == "enterprise": logo_html = '
♦ GrantForge
' # Cert logic cert_html = "" if extra_context and extra_context.get("include_grounding_certificate"): cert = extra_context snap = cert.get("snapshot_data", {}) cert_html = f"""

ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE

GrantForge AI — Najwyższy poziom ugruntowania w regulaminach i prawie UE


Projekt: {project_title}

Beneficjent: {company_name}

Data wygenerowania: {datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")}

Version Hash: {cert.get("version_hash", "N/A")}

Precise Regulation URL: {cert.get("precise_regulation_url", "N/A")}

Link Quality: {cert.get("regulation_link_quality", "N/A")}

""" if snap: cert_html += "

SNAPSHOT REGULAMINU (źródło prawdy)

" cert_html += """

To świadectwo potwierdza, że wszystkie wygenerowane treści opierają się na konkretnej, wersjonowanej wersji regulaminu oraz aktualnych aktach prawnych UE.

https://grantforge.ai | Regulation Engine jako źródło prawdy

""" html_content = f"""
{logo_html}

{project_title}

{company_name}

Wygenerowano z użyciem systemu wsparcia DotacjeAI

Dokument utworzony: {date_str} | Wersja: {version}

{html_body} {cert_html} """ with open(output_path, "wb") as pdf_file: pisa_status = pisa.CreatePDF(html_content.encode("utf-8"), dest=pdf_file, encoding='utf-8') if pisa_status.err: print(f"Błąd pisa: {pisa_status.err}") return False return True except Exception: import traceback print(f"Błąd eksportu do PDF: {traceback.format_exc()}") return False def localize_markdown_headings(content: str, lang: str = "pl") -> str: """Map English section headings to Polish (lang=pl).""" if lang != "pl" or not content: return content mapping = { "Executive Summary": "Streszczenie wykonawcze", "Project Description": "Opis projektu", "Budget": "Budżet", "Timeline": "Harmonogram", "Risk Analysis": "Analiza ryzyka", "Expected Results": "Oczekiwane rezultaty", } lines = content.splitlines() out: list[str] = [] for line in lines: new_line = line for en, pl in mapping.items(): if line.strip() == f"# {en}": new_line = f"# {pl}" break if line.strip() == f"## {en}": new_line = f"## {pl}" break if line.strip() == en: new_line = pl break out.append(new_line) return "\n".join(out)