invoice_service.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import os
  2. from fpdf import FPDF
  3. import config
  4. from datetime import datetime
  5. import qrcode
  6. import io
  7. from services.fiscal_service import FiscalService
  8. class PDFGenerator(FPDF):
  9. def __init__(self, **kwargs):
  10. super().__init__(**kwargs)
  11. self.set_auto_page_break(True, margin=15)
  12. def header_logo(self):
  13. # Placeholder for a logo if needed
  14. self.set_font("helvetica", "B", 20)
  15. self.set_text_color(31, 41, 55) # text-gray-800
  16. self.cell(0, 10, config.COMPANY_NAME, ln=True, align='L')
  17. self.set_font("helvetica", "", 8)
  18. self.set_text_color(107, 114, 128) # text-gray-500
  19. self.cell(0, 4, config.COMPANY_ADDRESS, ln=True)
  20. self.cell(0, 4, f"PIB: {config.COMPANY_PIB} | Ziro racun: {config.ZIRO_RACUN}", ln=True)
  21. self.ln(10)
  22. def draw_table_header(self, columns):
  23. self.set_fill_color(249, 250, 251) # gray-50
  24. self.set_text_color(55, 65, 81) # gray-700
  25. self.set_font("helvetica", "B", 9)
  26. for col in columns:
  27. self.cell(col['w'], 8, col['label'], border=1, fill=True, align=col['align'])
  28. self.ln()
  29. def format_money(self, amount):
  30. return f"{amount:.2f}".replace(".", ",")
  31. class InvoiceService:
  32. @staticmethod
  33. def _get_output_path(filename):
  34. pdf_dir = os.path.join(config.UPLOAD_DIR, "invoices")
  35. os.makedirs(pdf_dir, exist_ok=True)
  36. filepath = os.path.join(pdf_dir, filename)
  37. return filepath, os.path.join("uploads", "invoices", filename).replace("\\", "/")
  38. @staticmethod
  39. def generate_uplatnica(order_id, payer_name, payer_address, amount):
  40. """Generates a standard Payment Slip (Uplatnica) for individuals"""
  41. pdf = FPDF(orientation='P', unit='mm', format='A4')
  42. pdf.set_auto_page_break(False)
  43. pdf.add_page()
  44. pdf.set_font("helvetica", size=7)
  45. # Draw the structure (as in previous version but can be polished)
  46. pdf.rect(0, 0, 210, 99)
  47. pdf.line(105, 0, 105, 99)
  48. # ... (transferring the logic from uplatnica_generator.py) ...
  49. # I will keep the existing precise layout for uplatnica since it's a standard form
  50. labels = ["Hitnost", "Prenos", "Uplata", "Isplata"]
  51. for i, label in enumerate(labels):
  52. x = 110 + i * 22
  53. pdf.set_xy(x, 2)
  54. pdf.cell(14, 4, label)
  55. pdf.rect(x + 14, 2, 4, 4)
  56. if label == "Uplata":
  57. pdf.line(x + 14, 2, x + 18, 6); pdf.line(x + 18, 2, x + 14, 6)
  58. pdf.set_font("helvetica", "B", 8)
  59. pdf.set_xy(5, 5); pdf.cell(95, 5, "NALOG PLATIOCA", align="C")
  60. pdf.rect(5, 12, 95, 14)
  61. pdf.set_font("helvetica", size=9)
  62. pdf.set_xy(7, 14); pdf.cell(90, 4, payer_name[:40])
  63. pdf.set_xy(7, 18); pdf.cell(90, 4, payer_address[:40])
  64. pdf.set_font("helvetica", size=6)
  65. pdf.set_xy(5, 26); pdf.cell(95, 4, "(Naziv platioca)", align="C")
  66. pdf.rect(5, 30, 95, 14)
  67. pdf.set_font("helvetica", size=9)
  68. pdf.set_xy(7, 32); pdf.cell(90, 4, "Usluge 3D stampe")
  69. pdf.set_xy(7, 36); pdf.cell(90, 4, f"Narudzba {order_id}")
  70. pdf.set_font("helvetica", size=6)
  71. pdf.set_xy(5, 44); pdf.cell(95, 4, "(Svrha placanja)", align="C")
  72. pdf.rect(5, 48, 95, 14)
  73. pdf.set_font("helvetica", size=9)
  74. pdf.set_xy(7, 50); pdf.cell(90, 4, config.COMPANY_NAME)
  75. pdf.set_font("helvetica", size=6)
  76. pdf.set_xy(5, 62); pdf.cell(95, 4, "(Naziv primaoca)", align="C")
  77. pdf.set_font("helvetica", size=7)
  78. pdf.set_xy(110, 22); pdf.cell(10, 4, "EUR")
  79. pdf.rect(120, 22, 50, 10); pdf.rect(175, 22, 30, 10)
  80. pdf.set_font("helvetica", "B", 11)
  81. pdf.set_xy(120, 24); pdf.cell(50, 6, f"{amount:.2f}".replace(".", ","), align="C")
  82. pdf.set_xy(175, 24); pdf.cell(30, 6, "121", align="C")
  83. pdf.rect(110, 36, 95, 10)
  84. pdf.set_font("helvetica", size=10)
  85. pdf.set_xy(110, 38); pdf.cell(95, 6, config.ZIRO_RACUN, align="C")
  86. pdf.rect(110, 50, 20, 8); pdf.rect(135, 50, 70, 8)
  87. pdf.set_xy(110, 52); pdf.cell(20, 4, "00", align="C")
  88. pdf.set_xy(135, 52); pdf.cell(70, 4, str(order_id), align="C")
  89. filename = f"uplatnica_order_{order_id}.pdf"
  90. filepath, web_path = InvoiceService._get_output_path(filename)
  91. pdf.output(filepath)
  92. return web_path
  93. @staticmethod
  94. def generate_document(order_data, doc_type="predracun", override_price=None):
  95. """
  96. Generic generator for Predracun (Proforma) or Faktura (Invoice)
  97. Compliant with Montenegro VAT laws.
  98. """
  99. pdf = PDFGenerator()
  100. pdf.add_page()
  101. pdf.header_logo()
  102. title = "PREDRACUN (Proforma Invoice / Avansni racun)" if doc_type == "predracun" else "FAKTURA / RACUN (Invoice)"
  103. doc_id = f"{order_data['id']}/{datetime.now().year}"
  104. now_str = datetime.now().strftime('%d.%m.%Y.')
  105. pdf.set_font("helvetica", "B", 14)
  106. pdf.cell(0, 10, f"{title} #{doc_id}", ln=True)
  107. pdf.set_font("helvetica", "", 9)
  108. pdf.cell(0, 5, f"Mjesto izdavanja: {config.COMPANY_CITY}", ln=True)
  109. pdf.cell(0, 5, f"Datum izdavanja: {now_str}", ln=True)
  110. pdf.cell(0, 5, f"Datum prometa: {now_str}", ln=True)
  111. pdf.ln(10)
  112. # Buyer and Seller horizontal layout
  113. pdf.set_font("helvetica", "B", 10)
  114. pdf.cell(95, 5, "PRODAVAC / SUPPLIER:", ln=False)
  115. pdf.cell(95, 5, "KUPAC / CUSTOMER:", ln=True)
  116. pdf.set_font("helvetica", "", 9)
  117. # Supplier Details
  118. current_y = pdf.get_y()
  119. pdf.multi_cell(90, 4, f"{config.COMPANY_NAME}\n{config.COMPANY_ADDRESS}\nPIB: {config.COMPANY_PIB}\nZiro racun: {config.ZIRO_RACUN}")
  120. # Customer Details
  121. pdf.set_xy(105, current_y)
  122. is_company = bool(order_data.get('is_company'))
  123. buyer_name = order_data.get('company_name') or f"{order_data.get('first_name', '')} {order_data.get('last_name', '')}"
  124. buyer_address = order_data.get('company_address') or order_data.get('shipping_address', 'N/A')
  125. buyer_pib = order_data.get('company_pib') or ""
  126. # Note: Buyer's bank account isn't collected, so we leave it as N/A or empty
  127. pdf.multi_cell(90, 4, f"{buyer_name}\nAddress: {buyer_address}\nPIB/JMBG: {buyer_pib}\nZiro racun: N/A")
  128. pdf.ln(10)
  129. pdf.set_xy(10, pdf.get_y() + 5)
  130. # Items Table
  131. columns = [
  132. {'w': 10, 'label': '#', 'align': 'C'},
  133. {'w': 90, 'label': 'Opis / Description', 'align': 'L'},
  134. {'w': 20, 'label': 'Kol / Qty', 'align': 'C'},
  135. {'w': 35, 'label': 'Iznos / Total', 'align': 'R'},
  136. ]
  137. pdf.draw_table_header(columns)
  138. pdf.set_font("helvetica", "", 9)
  139. # Fetch actual fixed items from DB
  140. items = db.execute_query("SELECT * FROM order_items WHERE order_id = %s", (order_data['id'],))
  141. if not items:
  142. # Fallback to legacy single row if no items found (for old orders)
  143. total_amount = float(override_price if override_price is not None else (order_data.get('total_price') or 0))
  144. pdf.cell(10, 10, "1", border=1, align='C')
  145. pdf.cell(90, 10, f"Usluge 3D stampe (Order #{order_data['id']})", border=1)
  146. pdf.cell(20, 10, "1", border=1, align='C')
  147. pdf.cell(35, 10, pdf.format_money(total_amount), border=1, align='R', ln=True)
  148. else:
  149. total_amount = 0
  150. for idx, item in enumerate(items, 1):
  151. item_total = float(item['total_price'])
  152. total_amount += item_total
  153. pdf.cell(10, 8, str(idx), border=1, align='C')
  154. pdf.cell(90, 8, str(item['description']), border=1)
  155. pdf.cell(20, 8, str(item['quantity']), border=1, align='C')
  156. pdf.cell(35, 8, pdf.format_money(item_total), border=1, align='R', ln=True)
  157. # Summary
  158. pdv_rate = config.PDV_RATE
  159. pdf.ln(5)
  160. pdf.set_font("helvetica", "", 10)
  161. base_amount = total_amount / (1 + pdv_rate/100)
  162. pdv_amount = total_amount - base_amount
  163. pdf.cell(155, 6, "Osnovica / Base Amount (bez PDV):", align='R')
  164. pdf.cell(35, 6, pdf.format_money(base_amount), align='R', ln=True)
  165. pdf.cell(155, 6, f"PDV {pdv_rate}% / VAT Amount:", align='R')
  166. pdf.cell(35, 6, pdf.format_money(pdv_amount), align='R', ln=True)
  167. pdf.set_font("helvetica", "B", 12)
  168. pdf.cell(155, 12, "UKUPNO / TOTAL (EUR):", align='R')
  169. pdf.cell(35, 12, pdf.format_money(total_amount), align='R', ln=True)
  170. pdf.ln(20)
  171. # --- FISCALIZATION BLOCK ---
  172. print(f"DEBUG: Generating document type: {doc_type}")
  173. manual_qr = order_data.get('fiscal_qr_url')
  174. manual_ikof = order_data.get('ikof')
  175. manual_jikr = order_data.get('jikr')
  176. manual_ts = order_data.get('fiscalized_at')
  177. if doc_type == "faktura" or manual_qr:
  178. print("DEBUG: Entering Fiscalization Block")
  179. # Use manual data if available, otherwise generate mock/active
  180. ikof = manual_ikof or FiscalService.generate_ikof(order_data['id'], total_amount)[0]
  181. ts_obj = manual_ts or datetime.now()
  182. ts = ts_obj.strftime("%Y-%m-%dT%H:%M:%S+02:00") if hasattr(ts_obj, 'strftime') else str(ts_obj)
  183. jikr = manual_jikr or "MOCK-JIKR-EFI-STAGING-123"
  184. qr_url = manual_qr or FiscalService.generate_qr_url(jikr, ikof, ts, total_amount)
  185. print(f"DEBUG: QR URL: {qr_url}")
  186. # 4. Create QR Image
  187. qr = qrcode.QRCode(box_size=10, border=0)
  188. qr.add_data(qr_url)
  189. qr.make(fit=True)
  190. img_qr = qr.make_image(fill_color="black", back_color="white")
  191. # Convert to bytes for FPDF
  192. img_byte_arr = io.BytesIO()
  193. img_qr.save(img_byte_arr, format='PNG')
  194. img_byte_arr.seek(0)
  195. # 5. Place QR and Fiscal Info
  196. try:
  197. pdf.image(img_byte_arr, x=10, y=pdf.get_y(), w=30, h=30, type='PNG')
  198. except Exception as e:
  199. print(f"DEBUG: Failed to embed QR image: {e}")
  200. pdf.set_xy(45, pdf.get_y() + 5)
  201. pdf.set_font("helvetica", "B", 8)
  202. pdf.cell(0, 4, f"IKOF: {ikof}", ln=True)
  203. pdf.set_xy(45, pdf.get_y())
  204. pdf.cell(0, 4, f"JIKR: {jikr}", ln=True)
  205. pdf.set_xy(45, pdf.get_y())
  206. pdf.set_font("helvetica", "", 7)
  207. pdf.cell(0, 4, f"Fiskalizovano: {ts}", ln=True)
  208. pdf.set_y(pdf.get_y() + 15)
  209. # ---------------------------
  210. pdf.set_font("helvetica", "", 9)
  211. pdf.cell(95, 10, "Fakturisao (Izdavalac):", ln=False)
  212. pdf.cell(95, 10, "Primio (Kupac):", ln=True)
  213. pdf.ln(5)
  214. pdf.line(10, pdf.get_y(), 80, pdf.get_y())
  215. pdf.line(110, pdf.get_y(), 180, pdf.get_y())
  216. # Footer
  217. pdf.ln(15)
  218. pdf.set_font("helvetica", "I", 8)
  219. if doc_type == "predracun":
  220. 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."
  221. else:
  222. 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."
  223. pdf.multi_cell(0, 5, msg)
  224. filename = f"{doc_type}_order_{order_data['id']}.pdf"
  225. filepath, web_path = InvoiceService._get_output_path(filename)
  226. print(f"DEBUG: Saving PDF to absolute path: {os.path.abspath(filepath)}")
  227. pdf.output(filepath)
  228. return web_path