import os from fpdf import FPDF import config from datetime import datetime import qrcode import io from services.fiscal_service import FiscalService class PDFGenerator(FPDF): def __init__(self, **kwargs): super().__init__(**kwargs) self.set_auto_page_break(True, margin=15) def header_logo(self): # Placeholder for a logo if needed self.set_font("helvetica", "B", 20) self.set_text_color(31, 41, 55) # text-gray-800 self.cell(0, 10, config.COMPANY_NAME, ln=True, align='L') self.set_font("helvetica", "", 8) self.set_text_color(107, 114, 128) # text-gray-500 self.cell(0, 4, config.COMPANY_ADDRESS, ln=True) self.cell(0, 4, f"PIB: {config.COMPANY_PIB} | Ziro racun: {config.ZIRO_RACUN}", ln=True) self.ln(10) def draw_table_header(self, columns): self.set_fill_color(249, 250, 251) # gray-50 self.set_text_color(55, 65, 81) # gray-700 self.set_font("helvetica", "B", 9) for col in columns: self.cell(col['w'], 8, col['label'], border=1, fill=True, align=col['align']) self.ln() def format_money(self, amount): return f"{amount:.2f}".replace(".", ",") class InvoiceService: @staticmethod def _get_output_path(filename): pdf_dir = os.path.join(config.UPLOAD_DIR, "invoices") os.makedirs(pdf_dir, exist_ok=True) filepath = os.path.join(pdf_dir, filename) return filepath, os.path.join("uploads", "invoices", filename).replace("\\", "/") @staticmethod def generate_uplatnica(order_id, payer_name, payer_address, amount): """Generates a standard Payment Slip (Uplatnica) for individuals""" pdf = FPDF(orientation='P', unit='mm', format='A4') pdf.set_auto_page_break(False) pdf.add_page() pdf.set_font("helvetica", size=7) # Draw the structure (as in previous version but can be polished) pdf.rect(0, 0, 210, 99) pdf.line(105, 0, 105, 99) # ... (transferring the logic from uplatnica_generator.py) ... # I will keep the existing precise layout for uplatnica since it's a standard form labels = ["Hitnost", "Prenos", "Uplata", "Isplata"] for i, label in enumerate(labels): x = 110 + i * 22 pdf.set_xy(x, 2) pdf.cell(14, 4, label) pdf.rect(x + 14, 2, 4, 4) if label == "Uplata": pdf.line(x + 14, 2, x + 18, 6); pdf.line(x + 18, 2, x + 14, 6) pdf.set_font("helvetica", "B", 8) pdf.set_xy(5, 5); pdf.cell(95, 5, "NALOG PLATIOCA", align="C") pdf.rect(5, 12, 95, 14) pdf.set_font("helvetica", size=9) pdf.set_xy(7, 14); pdf.cell(90, 4, payer_name[:40]) pdf.set_xy(7, 18); pdf.cell(90, 4, payer_address[:40]) pdf.set_font("helvetica", size=6) pdf.set_xy(5, 26); pdf.cell(95, 4, "(Naziv platioca)", align="C") pdf.rect(5, 30, 95, 14) pdf.set_font("helvetica", size=9) pdf.set_xy(7, 32); pdf.cell(90, 4, "Usluge 3D stampe") pdf.set_xy(7, 36); pdf.cell(90, 4, f"Narudzba {order_id}") pdf.set_font("helvetica", size=6) pdf.set_xy(5, 44); pdf.cell(95, 4, "(Svrha placanja)", align="C") pdf.rect(5, 48, 95, 14) pdf.set_font("helvetica", size=9) pdf.set_xy(7, 50); pdf.cell(90, 4, config.COMPANY_NAME) pdf.set_font("helvetica", size=6) pdf.set_xy(5, 62); pdf.cell(95, 4, "(Naziv primaoca)", align="C") pdf.set_font("helvetica", size=7) pdf.set_xy(110, 22); pdf.cell(10, 4, "EUR") pdf.rect(120, 22, 50, 10); pdf.rect(175, 22, 30, 10) pdf.set_font("helvetica", "B", 11) pdf.set_xy(120, 24); pdf.cell(50, 6, f"{amount:.2f}".replace(".", ","), align="C") pdf.set_xy(175, 24); pdf.cell(30, 6, "121", align="C") pdf.rect(110, 36, 95, 10) pdf.set_font("helvetica", size=10) pdf.set_xy(110, 38); pdf.cell(95, 6, config.ZIRO_RACUN, align="C") pdf.rect(110, 50, 20, 8); pdf.rect(135, 50, 70, 8) pdf.set_xy(110, 52); pdf.cell(20, 4, "00", align="C") pdf.set_xy(135, 52); pdf.cell(70, 4, str(order_id), align="C") filename = f"uplatnica_order_{order_id}.pdf" filepath, web_path = InvoiceService._get_output_path(filename) pdf.output(filepath) return web_path @staticmethod def generate_document(order_data, doc_type="predracun", override_price=None): """ Generic generator for Predracun (Proforma) or Faktura (Invoice) Compliant with Montenegro VAT laws. """ pdf = PDFGenerator() pdf.add_page() pdf.header_logo() title = "PREDRACUN (Proforma Invoice / Avansni racun)" if doc_type == "predracun" else "FAKTURA / RACUN (Invoice)" doc_id = f"{order_data['id']}/{datetime.now().year}" now_str = datetime.now().strftime('%d.%m.%Y.') pdf.set_font("helvetica", "B", 14) pdf.cell(0, 10, f"{title} #{doc_id}", ln=True) pdf.set_font("helvetica", "", 9) pdf.cell(0, 5, f"Mjesto izdavanja: {config.COMPANY_CITY}", ln=True) pdf.cell(0, 5, f"Datum izdavanja: {now_str}", ln=True) pdf.cell(0, 5, f"Datum prometa: {now_str}", ln=True) pdf.ln(10) # Buyer and Seller horizontal layout pdf.set_font("helvetica", "B", 10) pdf.cell(95, 5, "PRODAVAC / SUPPLIER:", ln=False) pdf.cell(95, 5, "KUPAC / CUSTOMER:", ln=True) pdf.set_font("helvetica", "", 9) # Supplier Details current_y = pdf.get_y() pdf.multi_cell(90, 4, f"{config.COMPANY_NAME}\n{config.COMPANY_ADDRESS}\nPIB: {config.COMPANY_PIB}\nZiro racun: {config.ZIRO_RACUN}") # Customer Details pdf.set_xy(105, current_y) is_company = bool(order_data.get('is_company')) buyer_name = order_data.get('company_name') or f"{order_data.get('first_name', '')} {order_data.get('last_name', '')}" buyer_address = order_data.get('company_address') or order_data.get('shipping_address', 'N/A') buyer_pib = order_data.get('company_pib') or "" # Note: Buyer's bank account isn't collected, so we leave it as N/A or empty pdf.multi_cell(90, 4, f"{buyer_name}\nAddress: {buyer_address}\nPIB/JMBG: {buyer_pib}\nZiro racun: N/A") pdf.ln(10) pdf.set_xy(10, pdf.get_y() + 5) # Items Table columns = [ {'w': 10, 'label': '#', 'align': 'C'}, {'w': 90, 'label': 'Opis / Description', 'align': 'L'}, {'w': 20, 'label': 'Kol / Qty', 'align': 'C'}, {'w': 35, 'label': 'Iznos / Total', 'align': 'R'}, ] pdf.draw_table_header(columns) pdf.set_font("helvetica", "", 9) # Fetch actual fixed items from DB items = db.execute_query("SELECT * FROM order_items WHERE order_id = %s", (order_data['id'],)) if not items: # Fallback to legacy single row if no items found (for old orders) total_amount = float(override_price if override_price is not None else (order_data.get('total_price') or 0)) pdf.cell(10, 10, "1", border=1, align='C') pdf.cell(90, 10, f"Usluge 3D stampe (Order #{order_data['id']})", border=1) pdf.cell(20, 10, "1", border=1, align='C') pdf.cell(35, 10, pdf.format_money(total_amount), border=1, align='R', ln=True) else: total_amount = 0 for idx, item in enumerate(items, 1): item_total = float(item['total_price']) total_amount += item_total pdf.cell(10, 8, str(idx), border=1, align='C') pdf.cell(90, 8, str(item['description']), border=1) pdf.cell(20, 8, str(item['quantity']), border=1, align='C') pdf.cell(35, 8, pdf.format_money(item_total), border=1, align='R', ln=True) # Summary pdv_rate = config.PDV_RATE pdf.ln(5) pdf.set_font("helvetica", "", 10) base_amount = total_amount / (1 + pdv_rate/100) pdv_amount = total_amount - base_amount pdf.cell(155, 6, "Osnovica / Base Amount (bez PDV):", align='R') pdf.cell(35, 6, pdf.format_money(base_amount), align='R', ln=True) pdf.cell(155, 6, f"PDV {pdv_rate}% / VAT Amount:", align='R') pdf.cell(35, 6, pdf.format_money(pdv_amount), align='R', ln=True) pdf.set_font("helvetica", "B", 12) pdf.cell(155, 12, "UKUPNO / TOTAL (EUR):", align='R') pdf.cell(35, 12, pdf.format_money(total_amount), align='R', ln=True) pdf.ln(20) # --- FISCALIZATION BLOCK --- print(f"DEBUG: Generating document type: {doc_type}") manual_qr = order_data.get('fiscal_qr_url') manual_ikof = order_data.get('ikof') manual_jikr = order_data.get('jikr') manual_ts = order_data.get('fiscalized_at') if doc_type == "faktura" or manual_qr: print("DEBUG: Entering Fiscalization Block") # Use manual data if available, otherwise generate mock/active ikof = manual_ikof or FiscalService.generate_ikof(order_data['id'], total_amount)[0] ts_obj = manual_ts or datetime.now() ts = ts_obj.strftime("%Y-%m-%dT%H:%M:%S+02:00") if hasattr(ts_obj, 'strftime') else str(ts_obj) jikr = manual_jikr or "MOCK-JIKR-EFI-STAGING-123" qr_url = manual_qr or FiscalService.generate_qr_url(jikr, ikof, ts, total_amount) print(f"DEBUG: QR URL: {qr_url}") # 4. Create QR Image qr = qrcode.QRCode(box_size=10, border=0) qr.add_data(qr_url) qr.make(fit=True) img_qr = qr.make_image(fill_color="black", back_color="white") # Convert to bytes for FPDF img_byte_arr = io.BytesIO() img_qr.save(img_byte_arr, format='PNG') img_byte_arr.seek(0) # 5. Place QR and Fiscal Info try: pdf.image(img_byte_arr, x=10, y=pdf.get_y(), w=30, h=30, type='PNG') except Exception as e: print(f"DEBUG: Failed to embed QR image: {e}") pdf.set_xy(45, pdf.get_y() + 5) pdf.set_font("helvetica", "B", 8) pdf.cell(0, 4, f"IKOF: {ikof}", ln=True) pdf.set_xy(45, pdf.get_y()) pdf.cell(0, 4, f"JIKR: {jikr}", ln=True) pdf.set_xy(45, pdf.get_y()) pdf.set_font("helvetica", "", 7) pdf.cell(0, 4, f"Fiskalizovano: {ts}", ln=True) pdf.set_y(pdf.get_y() + 15) # --------------------------- pdf.set_font("helvetica", "", 9) pdf.cell(95, 10, "Fakturisao (Izdavalac):", ln=False) pdf.cell(95, 10, "Primio (Kupac):", ln=True) pdf.ln(5) pdf.line(10, pdf.get_y(), 80, pdf.get_y()) pdf.line(110, pdf.get_y(), 180, pdf.get_y()) # Footer pdf.ln(15) pdf.set_font("helvetica", "I", 8) if doc_type == "predracun": msg = "Predracun je validan bez pecata i potpisa shodno clanu 31 Zakona o PDV. Placanje se vrsi u roku od 3 dana na navedeni ziro racun.\nThis is a proforma invoice and is valid without stamp and signature." else: msg = "Faktura je validna bez pecata i potpisa shodno clanu 31 Zakona o PDV. Roba/usluga je isporucena u skladu sa ugovorom.\nThis is a final invoice and is valid without stamp and signature." pdf.multi_cell(0, 5, msg) filename = f"{doc_type}_order_{order_data['id']}.pdf" filepath, web_path = InvoiceService._get_output_path(filename) print(f"DEBUG: Saving PDF to absolute path: {os.path.abspath(filepath)}") pdf.output(filepath) return web_path