Files
tilbudgivern/backend/pdf_generator.py
2026-07-07 21:30:34 +02:00

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']}", 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()