611 lines
32 KiB
Python
Executable File
611 lines
32 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
PDF Generator for Tilbudgivern
|
|
Uses ReportLab for reliable PDF generation on ARM/Raspberry Pi systems
|
|
"""
|
|
|
|
import sys
|
|
import json
|
|
import os
|
|
from datetime import datetime
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.units import mm, cm
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, PageBreak
|
|
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.utils import ImageReader
|
|
from io import BytesIO
|
|
import base64
|
|
|
|
class TilbudgivernPDFGenerator:
|
|
def __init__(self):
|
|
self.styles = getSampleStyleSheet()
|
|
self.setup_custom_styles()
|
|
|
|
# Company information
|
|
self.company_info = {
|
|
'name': 'Tømrer- og Snedkermester Mikael Holck ApS',
|
|
'address': 'Hornumvej 7',
|
|
'city': '4600 Køge',
|
|
'phone': '42 46 81 10',
|
|
'email': 'mail@mikaelholck.dk',
|
|
'website': 'www.mikaelholck.dk',
|
|
'cvr': '38178059',
|
|
'bank': 'Møns Bank',
|
|
'account': '6140 0002083754'
|
|
}
|
|
|
|
# Logo path
|
|
self.logo_path = '/mnt/HC_Volume_103713257/tilbudgivern/frontend/firmadata/firmabilleder/MIKHOL_logo.png'
|
|
|
|
def setup_custom_styles(self):
|
|
"""Setup custom styles for the PDF matching Mikael Holck professional format"""
|
|
# Company header - larger and centered
|
|
self.styles.add(ParagraphStyle(
|
|
name='CompanyHeader',
|
|
parent=self.styles['Heading1'],
|
|
fontSize=18,
|
|
spaceAfter=8,
|
|
spaceBefore=0,
|
|
textColor=colors.HexColor('#1a3a5c'),
|
|
alignment=TA_CENTER,
|
|
fontName='Helvetica-Bold'
|
|
))
|
|
|
|
# Section headers (ARBEJDE DER UDFØRES, MATERIALER, etc.)
|
|
self.styles.add(ParagraphStyle(
|
|
name='SectionHeader',
|
|
parent=self.styles['Heading2'],
|
|
fontSize=13,
|
|
spaceAfter=8,
|
|
spaceBefore=12,
|
|
textColor=colors.black,
|
|
alignment=TA_LEFT,
|
|
fontName='Helvetica-Bold',
|
|
underline=True
|
|
))
|
|
|
|
# Main task header (Skorsten, Udvidelse af værelse, etc.)
|
|
self.styles.add(ParagraphStyle(
|
|
name='MainTaskHeader',
|
|
parent=self.styles['Heading3'],
|
|
fontSize=12,
|
|
spaceAfter=6,
|
|
spaceBefore=8,
|
|
textColor=colors.black,
|
|
alignment=TA_LEFT,
|
|
fontName='Helvetica-Bold'
|
|
))
|
|
|
|
# Sub task header (Sikring omkring skorsten, Ny konstruktion, etc.)
|
|
self.styles.add(ParagraphStyle(
|
|
name='SubTaskHeader',
|
|
parent=self.styles['Normal'],
|
|
fontSize=11,
|
|
spaceAfter=4,
|
|
spaceBefore=6,
|
|
textColor=colors.black,
|
|
alignment=TA_LEFT,
|
|
fontName='Helvetica-Bold',
|
|
leftIndent=10
|
|
))
|
|
|
|
# Body text with spacing
|
|
self.styles.add(ParagraphStyle(
|
|
name='BodySpaced',
|
|
parent=self.styles['Normal'],
|
|
fontSize=10,
|
|
spaceAfter=5,
|
|
leading=14,
|
|
leftIndent=0
|
|
))
|
|
|
|
# Bullet point text (under subtasks)
|
|
self.styles.add(ParagraphStyle(
|
|
name='BulletText',
|
|
parent=self.styles['Normal'],
|
|
fontSize=10,
|
|
spaceAfter=3,
|
|
leading=13,
|
|
leftIndent=20,
|
|
bulletIndent=10
|
|
))
|
|
|
|
# Price text - bold and prominent
|
|
self.styles.add(ParagraphStyle(
|
|
name='PriceText',
|
|
parent=self.styles['Normal'],
|
|
fontSize=11,
|
|
spaceAfter=4,
|
|
fontName='Helvetica-Bold'
|
|
))
|
|
|
|
# Total price - larger and very prominent
|
|
self.styles.add(ParagraphStyle(
|
|
name='TotalPrice',
|
|
parent=self.styles['Normal'],
|
|
fontSize=13,
|
|
spaceAfter=6,
|
|
spaceBefore=4,
|
|
fontName='Helvetica-Bold',
|
|
textColor=colors.HexColor('#1a3a5c')
|
|
))
|
|
|
|
# Customer info style
|
|
self.styles.add(ParagraphStyle(
|
|
name='CustomerInfo',
|
|
parent=self.styles['Normal'],
|
|
fontSize=11,
|
|
spaceAfter=4,
|
|
leading=14
|
|
))
|
|
|
|
# Small note text (forbehold, noter)
|
|
self.styles.add(ParagraphStyle(
|
|
name='NoteText',
|
|
parent=self.styles['Normal'],
|
|
fontSize=9,
|
|
spaceAfter=3,
|
|
leading=11,
|
|
textColor=colors.grey,
|
|
fontName='Helvetica-Oblique'
|
|
))
|
|
|
|
def load_logo(self, logo_path=None):
|
|
"""Load company logo if available"""
|
|
try:
|
|
path = logo_path or self.logo_path
|
|
if os.path.exists(path):
|
|
return Image(path, width=50*mm, height=25*mm)
|
|
else:
|
|
print(f"Warning: Logo not found at {path}")
|
|
return None
|
|
except Exception as e:
|
|
print(f"Warning: Could not load logo from {path}: {e}")
|
|
return None
|
|
|
|
def safe_float(self, value, default=0):
|
|
"""Safely convert value to float"""
|
|
try:
|
|
if value is None:
|
|
return default
|
|
return float(value)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
def safe_format_price(self, value, default=0):
|
|
"""Safely format value as price"""
|
|
try:
|
|
return f"{self.safe_float(value, default):.2f}"
|
|
except:
|
|
return f"{default:.2f}"
|
|
|
|
def generate_pdf(self, quote_data, output_path):
|
|
"""Generate PDF from quote data matching Mikael Holck professional format with logos"""
|
|
try:
|
|
# Create document with proper margins
|
|
doc = SimpleDocTemplate(
|
|
output_path,
|
|
pagesize=A4,
|
|
rightMargin=20*mm,
|
|
leftMargin=20*mm,
|
|
topMargin=20*mm,
|
|
bottomMargin=20*mm
|
|
)
|
|
|
|
# Build content
|
|
story = []
|
|
|
|
# Extract data
|
|
project = quote_data.get('project', {})
|
|
geometry = quote_data.get('geometry', {})
|
|
labor = quote_data.get('labor', {})
|
|
materials = quote_data.get('materials', [])
|
|
calculation = quote_data.get('calculation', {})
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# HEADER SECTION WITH LOGOS
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
# Load logos
|
|
logo_mikhol = self.load_logo()
|
|
logo_byggaranti = self.load_logo('/mnt/HC_Volume_103713257/tilbudgivern/frontend/firmadata/firmabilleder/byggarenti_logo.png')
|
|
|
|
# Create header with both logos
|
|
if logo_mikhol and logo_byggaranti:
|
|
header_table = Table(
|
|
[[logo_mikhol, Paragraph("<b>TILBUD</b>", self.styles['CompanyHeader']), logo_byggaranti]],
|
|
colWidths=[50*mm, 60*mm, 50*mm]
|
|
)
|
|
header_table.setStyle(TableStyle([
|
|
('ALIGN', (0, 0), (0, 0), 'LEFT'),
|
|
('ALIGN', (1, 0), (1, 0), 'CENTER'),
|
|
('ALIGN', (2, 0), (2, 0), 'RIGHT'),
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
|
]))
|
|
story.append(header_table)
|
|
elif logo_mikhol:
|
|
header_table = Table(
|
|
[[logo_mikhol, Paragraph("<b>TILBUD</b>", self.styles['CompanyHeader'])]],
|
|
colWidths=[60*mm, 100*mm]
|
|
)
|
|
header_table.setStyle(TableStyle([
|
|
('ALIGN', (0, 0), (0, 0), 'LEFT'),
|
|
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
|
]))
|
|
story.append(header_table)
|
|
else:
|
|
story.append(Paragraph("<b>TILBUD</b>", self.styles['CompanyHeader']))
|
|
|
|
story.append(Spacer(1, 10*mm))
|
|
|
|
# Customer information box
|
|
story.append(Paragraph(f"<b>{project.get('customer_name', 'N/A')}</b>", self.styles['CustomerInfo']))
|
|
story.append(Paragraph(project.get('customer_address', ''), self.styles['CustomerInfo']))
|
|
story.append(Spacer(1, 8*mm))
|
|
|
|
# Quote details aligned right
|
|
quote_details_data = [
|
|
['', f"Tilbudnr: {quote_data.get('quote_number', 'XXXX')}"],
|
|
['', f"Tilbudsdato: {datetime.now().strftime('%d-%m-%Y')}"],
|
|
['', f"Kundenr: {project.get('id', 'N/A')}"],
|
|
['', 'Side: 1/2']
|
|
]
|
|
quote_details = Table(quote_details_data, colWidths=[110*mm, 50*mm])
|
|
quote_details.setStyle(TableStyle([
|
|
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 11),
|
|
]))
|
|
story.append(quote_details)
|
|
story.append(Spacer(1, 8*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# INTRODUCTION
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph(f"<b>Tilbud på {project.get('project_description', 'arbejder')}.</b>",
|
|
self.styles['MainTaskHeader']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# MAIN WORK SECTION - Hierarchical structure
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("<b>ARBEJDE DER UDFØRES:</b>", self.styles['SectionHeader']))
|
|
story.append(Spacer(1, 3*mm))
|
|
|
|
# Main task description with hierarchical bullets
|
|
if geometry.get('roof_type'):
|
|
story.append(Paragraph(f"<b>{geometry['roof_type']}</b>", self.styles['MainTaskHeader']))
|
|
story.append(Spacer(1, 2*mm))
|
|
|
|
# Work details in hierarchical structure
|
|
story.append(Paragraph("Opmåling og forberedelse", self.styles['SubTaskHeader']))
|
|
if geometry.get('total_area'):
|
|
story.append(Paragraph(f"• Samlet areal: {geometry['total_area']} m²", self.styles['BulletText']))
|
|
if geometry.get('roof_pitch'):
|
|
story.append(Paragraph(f"• Taghældning: {geometry['roof_pitch']}°", self.styles['BulletText']))
|
|
story.append(Spacer(1, 2*mm))
|
|
|
|
story.append(Paragraph("Arbejdsudførelse", self.styles['SubTaskHeader']))
|
|
if labor.get('total_work_hours'):
|
|
carpenter_count = labor.get('carpenter_count', 1)
|
|
hourly_rate = self.safe_float(labor.get('hourly_rate'), 580)
|
|
story.append(Paragraph(f"• Arbejdstimer: {labor['total_work_hours']} timer med {carpenter_count} tømrer{'e' if carpenter_count > 1 else ''}",
|
|
self.styles['BulletText']))
|
|
story.append(Paragraph(f"• Timepris: {self.safe_format_price(hourly_rate)} kr/time",
|
|
self.styles['BulletText']))
|
|
story.append(Paragraph("• Alt affald bortkøres", self.styles['BulletText']))
|
|
story.append(Paragraph("• Området ryddes og efterlades rent", self.styles['BulletText']))
|
|
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# MATERIALS SECTION
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("<b>MATERIALER:</b>", self.styles['SectionHeader']))
|
|
story.append(Spacer(1, 3*mm))
|
|
|
|
# Materials list with proper formatting
|
|
if materials:
|
|
for material in materials:
|
|
material_category = material.get('material_category', 'materialer')
|
|
quantity = material.get('quantity', 1)
|
|
total_price = self.safe_float(material.get('total_price'), 0)
|
|
story.append(Paragraph(f"• {material_category}: {quantity} stk. - {self.safe_format_price(total_price)} kr",
|
|
self.styles['BulletText']))
|
|
else:
|
|
story.append(Paragraph("• Materialer specificeres efter endelig opmåling", self.styles['BulletText']))
|
|
|
|
story.append(Spacer(1, 3*mm))
|
|
story.append(Paragraph("Vi indhenter tilbud fra flere leverandører, så du får den bedste balance mellem pris, kvalitet og holdbarhed. Alle materialer er godkendte og lever op til gældende byggestandarder.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# PRICE SECTION - Clean and clear (UDEN overhead/fortjeneste til kunde)
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("<b>PRISSPECIFIKATION:</b>", self.styles['SectionHeader']))
|
|
story.append(Spacer(1, 3*mm))
|
|
|
|
# Calculate values
|
|
try:
|
|
labor_cost = self.safe_float(calculation.get('total_labor_cost'), 0)
|
|
material_cost = self.safe_float(calculation.get('total_material_cost'), 0)
|
|
subtotal = labor_cost + material_cost
|
|
overhead_pct = self.safe_float(calculation.get('overhead_percentage'), 15.0)
|
|
profit_pct = self.safe_float(calculation.get('profit_percentage'), 20.0)
|
|
vat_pct = self.safe_float(calculation.get('vat_percentage'), 25.0)
|
|
|
|
# Internal calculations. The customer-facing subtotal must be
|
|
# the same amount VAT is calculated from.
|
|
overhead = subtotal * (overhead_pct / 100)
|
|
profit = subtotal * (profit_pct / 100)
|
|
project_costs = overhead + profit
|
|
before_vat = subtotal + overhead + profit
|
|
vat = before_vat * (vat_pct / 100)
|
|
total_with_vat = before_vat + vat
|
|
|
|
# Format prices with Danish thousand separator
|
|
def format_dk_price(amount):
|
|
"""Format price with Danish thousand separator and decimal comma."""
|
|
formatted = f"{amount:,.2f}"
|
|
return formatted.replace(",", "X").replace(".", ",").replace("X", ".")
|
|
|
|
# Price table - customer-facing amounts must add up visibly.
|
|
price_data = [
|
|
['Arbejdsløn:', f"{format_dk_price(labor_cost)} kr"],
|
|
['Materialer:', f"{format_dk_price(material_cost)} kr"],
|
|
['Projektomkostninger og dækningsbidrag:', f"{format_dk_price(project_costs)} kr"],
|
|
['', ''], # Spacer
|
|
['Subtotal ekskl. moms:', f"{format_dk_price(before_vat)} kr"],
|
|
[f'Moms ({vat_pct:.0f}%):', f"{format_dk_price(vat)} kr"],
|
|
]
|
|
|
|
price_table = Table(price_data, colWidths=[100*mm, 50*mm])
|
|
price_table.setStyle(TableStyle([
|
|
('ALIGN', (0, 0), (0, -1), 'LEFT'),
|
|
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
|
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
|
('FONTSIZE', (0, 0), (-1, -1), 11),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
|
|
('TOPPADDING', (0, 0), (-1, -1), 5),
|
|
('LINEABOVE', (0, 4), (-1, 4), 1, colors.grey), # Line before subtotal
|
|
]))
|
|
story.append(price_table)
|
|
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# Total with visual separation
|
|
separator_line = '─' * 60
|
|
story.append(Paragraph(separator_line, self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 2*mm))
|
|
|
|
story.append(Paragraph(f"<b>SAMLET PRIS EKS. MOMS: {format_dk_price(before_vat)} kr</b>",
|
|
self.styles['TotalPrice']))
|
|
|
|
story.append(Spacer(1, 3*mm))
|
|
story.append(Paragraph(f"<b>TOTAL DKK INKL. MOMS ({vat_pct:.0f}%): {format_dk_price(total_with_vat)} kr</b>",
|
|
self.styles['TotalPrice']))
|
|
|
|
except Exception as e:
|
|
print(f"Error in price calculation: {e}")
|
|
story.append(Paragraph("Priser beregnes efter aftale", self.styles['BodySpaced']))
|
|
|
|
story.append(Spacer(1, 8*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# DISCLAIMERS AND NOTES
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("*Hvis der bruges kortere tid på opgaven end beregnet, gives der et fradrag på prisen.",
|
|
self.styles['NoteText']))
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# COMPANY PROMISES
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("<b>VORES LØFTE TIL DIG:</b>", self.styles['SectionHeader']))
|
|
story.append(Paragraph("• Præcision og ordentlighed i alt hvad vi laver", self.styles['BulletText']))
|
|
story.append(Paragraph("• Tradition og transformation går hånd i hånd", self.styles['BulletText']))
|
|
story.append(Paragraph("• Meningsfuldt, ordentligt og bæredygtigt håndværk", self.styles['BulletText']))
|
|
story.append(Paragraph("• Vi kommer til tiden og står inde for vores arbejde", self.styles['BulletText']))
|
|
story.append(Spacer(1, 3*mm))
|
|
story.append(Paragraph("Vi leverer altid det aftalte til tiden og med kvaliteten i top. Vores erfaring sikrer en flot og holdbar løsning, der øger både komforten og værdien af dit hjem.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# GUARANTEES
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("<b>GARANTIER OG SERVICE:</b>", self.styles['SectionHeader']))
|
|
story.append(Paragraph("• Gratis tagtjek tilbydes", self.styles['BulletText']))
|
|
story.append(Paragraph("• Professionel rådgivning i valg af løsninger", self.styles['BulletText']))
|
|
story.append(Paragraph("• Korrekt dokumentation og tryghed", self.styles['BulletText']))
|
|
story.append(Paragraph("• Kvalitet der holder i mange år", self.styles['BulletText']))
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# VALIDITY AND DISCLAIMERS
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("<b>TILBUDDET ER GYLDIGT I 30 DAGE</b>", self.styles['PriceText']))
|
|
story.append(Spacer(1, 4*mm))
|
|
story.append(Paragraph("Der tages forbehold for rød og svamp i eksisterende trækonstruktion.",
|
|
self.styles['NoteText']))
|
|
story.append(Spacer(1, 8*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# CLOSING
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("Med venlig hilsen", self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 10*mm))
|
|
story.append(Paragraph(f"<b>{self.company_info['name']}</b>", self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 2*mm))
|
|
story.append(Paragraph(f"{self.company_info['address']}, {self.company_info['city']}", self.styles['BodySpaced']))
|
|
story.append(Paragraph(f"CVR: {self.company_info['cvr']}", self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# CONTACT INFO
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("<b>Kontakt:</b>", self.styles['SubTaskHeader']))
|
|
story.append(Paragraph(f"Telefon: {self.company_info['phone']}", self.styles['BulletText']))
|
|
story.append(Paragraph(f"Email: {self.company_info['email']}", self.styles['BulletText']))
|
|
story.append(Paragraph(f"Website: {self.company_info['website']}", self.styles['BulletText']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
story.append(Paragraph(f"Bank: {self.company_info['bank']} - Kontonr.: {self.company_info['account']}", self.styles['NoteText']))
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# FOOTER TEXT
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(Paragraph("Med fokus på kvalitet, præcision og tryghed leverer vi et resultat, du kan stole på. Vi vurderer altid byggeteknisk korrekte løsninger og sikrer at arbejdet udføres efter gældende regler.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 8*mm))
|
|
|
|
# Company footer
|
|
company_footer_data = [
|
|
[
|
|
Paragraph(f"<b>{self.company_info['name']}</b><br/>{self.company_info['address']}<br/>{self.company_info['city']}", self.styles['NoteText']),
|
|
Paragraph(f"Tlf: {self.company_info['phone']}<br/>Bank: {self.company_info['bank']}<br/>Kontonr: {self.company_info['account']}", self.styles['NoteText']),
|
|
Paragraph(f"CVR: {self.company_info['cvr']}<br/>{self.company_info['email']}<br/>{self.company_info['website']}", self.styles['NoteText'])
|
|
]
|
|
]
|
|
footer_table = Table(company_footer_data, colWidths=[53*mm, 53*mm, 54*mm])
|
|
footer_table.setStyle(TableStyle([
|
|
('ALIGN', (0, 0), (0, 0), 'LEFT'),
|
|
('ALIGN', (1, 0), (1, 0), 'CENTER'),
|
|
('ALIGN', (2, 0), (2, 0), 'RIGHT'),
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
]))
|
|
story.append(footer_table)
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# PAGE 2 - FIRMABESKRIVELSE
|
|
# ═══════════════════════════════════════════════════════════
|
|
|
|
story.append(PageBreak())
|
|
|
|
# Page 2 header with logos
|
|
if logo_mikhol and logo_byggaranti:
|
|
header_table2 = Table(
|
|
[[logo_mikhol, '', logo_byggaranti]],
|
|
colWidths=[50*mm, 60*mm, 50*mm]
|
|
)
|
|
header_table2.setStyle(TableStyle([
|
|
('ALIGN', (0, 0), (0, 0), 'LEFT'),
|
|
('ALIGN', (2, 0), (2, 0), 'RIGHT'),
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
|
]))
|
|
story.append(header_table2)
|
|
story.append(Spacer(1, 10*mm))
|
|
|
|
# Company description
|
|
story.append(Paragraph("Med mange års erfaring inden for snedker- og tømrerarbejde kan vi udføre en bred vifte af opgaver — Store som små. Vi brænder for at realisere drømme, og har gennem årene hjulpet privatpersoner, virksomheder og offentlige institutioner i mål med deres drømme.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
story.append(Paragraph("Som professionelt tømrerfirma værner vi omkring høj kvalitet og god service. Vi er af den overbevisning at de bedste resultater skabes gennem tæt kunde. Derfor bliver dine ønsker og behov altid hørt hos os. Ud fra disse, og med vores brede faglighed, finder vi sammen den bedste løsning til den absolut bedste pris.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
story.append(Paragraph(f"Hos {self.company_info['name']} går vi aldrig på kompromis med kvaliteten af vores arbejde. Hertil er vi medlem af Byg Garanti, hvilket er din garanti for veludført håndværk. Hos os er du en prioritet, og ikke bare endnu en ordre i kalenderen. Vi kommer derfor altid til tiden, og rydder selvfølgelig op efter os selv, når opgaven er udført.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# Section: Vores tid er vigtig
|
|
story.append(Paragraph("<b>Vores tid er vigtig</b>", self.styles['MainTaskHeader']))
|
|
story.append(Paragraph("Vi bruger hver dag mange timer på at regne tilbud, og derfor vil vi sætte stor pris på at du vender tilbage med feedback på det tilbud vi har givet dig.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
story.append(Paragraph("Det er vigtigt for os med din feedback, og derfor vender vi også selv tilbage hvis vi ikke har hørt noget fra dig. Dette gør vi for at yde den bedste service til alle vi er i dialog med.",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
story.append(Paragraph('Husk at det "bedste tilbud" ikke altid er billigste. Det skal udføres korrekt og til den rigtige tid. Du skal kunne stole på din tømrer såvel som snedker — Og det kan du hos os.',
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 6*mm))
|
|
|
|
# Section: Anmeld Håndværker
|
|
story.append(Paragraph("<b>Anmeld Håndværker</b>", self.styles['MainTaskHeader']))
|
|
story.append(Paragraph('Vi er medlem af Anmeld Håndværker, og der har vi mærket "ELITE HÅNDVÆRKER" på baggrund af vores mange gode anmeldelser.',
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 4*mm))
|
|
|
|
story.append(Paragraph("På Anmeld håndværker har du mulighed for at se anmeldelser på opgaver vi har udført.<br/>Læs vores anmeldelser her: Anmeld Håndværker",
|
|
self.styles['BodySpaced']))
|
|
story.append(Spacer(1, 8*mm))
|
|
|
|
# Elite Håndværker logo
|
|
logo_elite = self.load_logo('/mnt/HC_Volume_103713257/tilbudgivern/frontend/firmadata/firmabilleder/elitehaandvaerker_logo.png')
|
|
if logo_elite:
|
|
elite_table = Table([[logo_elite]], colWidths=[60*mm])
|
|
elite_table.setStyle(TableStyle([
|
|
('ALIGN', (0, 0), (0, 0), 'CENTER'),
|
|
]))
|
|
story.append(elite_table)
|
|
story.append(Spacer(1, 10*mm))
|
|
|
|
# Footer on page 2
|
|
story.append(footer_table)
|
|
|
|
# Build PDF
|
|
doc.build(story)
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Error generating PDF: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
def main():
|
|
"""Main function to handle command line arguments"""
|
|
if len(sys.argv) != 3:
|
|
print("Usage: python3 pdf_generator.py <json_data_file> <output_pdf_path>")
|
|
sys.exit(1)
|
|
|
|
json_file = sys.argv[1]
|
|
output_path = sys.argv[2]
|
|
|
|
try:
|
|
# Read quote data from JSON file
|
|
with open(json_file, 'r', encoding='utf-8') as f:
|
|
quote_data = json.load(f)
|
|
|
|
# Generate PDF
|
|
generator = TilbudgivernPDFGenerator()
|
|
success = generator.generate_pdf(quote_data, output_path)
|
|
|
|
if success:
|
|
print(f"SUCCESS: PDF generated at {output_path}")
|
|
sys.exit(0)
|
|
else:
|
|
print("ERROR: Failed to generate PDF")
|
|
sys.exit(1)
|
|
|
|
except FileNotFoundError:
|
|
print(f"ERROR: JSON file not found: {json_file}")
|
|
sys.exit(1)
|
|
except json.JSONDecodeError as e:
|
|
print(f"ERROR: Invalid JSON in file {json_file}: {e}")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"ERROR: {e}")
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|