| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- 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
|