package controllers import ( "encoding/json" "fmt" "io" "net/http" "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/jung-kurt/gofpdf" "gorm.io/gorm" "fotbal-club/internal/models" ) type InvoiceController struct { db *gorm.DB } func NewInvoiceController(db *gorm.DB) *InvoiceController { return &InvoiceController{db: db} } // Invoice Management // GetInvoices retrieves all invoices with filtering and pagination func (ic *InvoiceController) GetInvoices(c *gin.Context) { var invoices []models.Invoice query := ic.db.Preload("Items").Preload("Payments").Preload("Customer") // Filter by status if provided if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } // Filter by payment status if provided if paymentStatus := c.Query("payment_status"); paymentStatus != "" { query = query.Where("payment_status = ?", paymentStatus) } // Filter by customer if provided if customerID := c.Query("customer_id"); customerID != "" { query = query.Where("customer_id = ?", customerID) } // Filter by date range if provided if startDate := c.Query("start_date"); startDate != "" { if date, err := time.Parse("2006-01-02", startDate); err == nil { query = query.Where("issue_date >= ?", date) } } if endDate := c.Query("end_date"); endDate != "" { if date, err := time.Parse("2006-01-02", endDate); err == nil { query = query.Where("issue_date <= ?", date) } } // Search by invoice number or customer name if search := c.Query("search"); search != "" { query = query.Where("invoice_number ILIKE ? OR customer_name ILIKE ?", "%"+search+"%", "%"+search+"%") } // Pagination page := 1 if p := c.Query("page"); p != "" { if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { page = parsed } } limit := 20 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { limit = parsed } } offset := (page - 1) * limit var total int64 query.Model(&models.Invoice{}).Count(&total) if err := query.Order("issue_date DESC, created_at DESC").Offset(offset).Limit(limit).Find(&invoices).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve invoices"}) return } c.JSON(http.StatusOK, gin.H{ "invoices": invoices, "total": total, "page": page, "limit": limit, }) } // GetInvoice retrieves a single invoice by ID func (ic *InvoiceController) GetInvoice(c *gin.Context) { id := c.Param("id") var invoice models.Invoice if err := ic.db.Preload("Items").Preload("Payments").Preload("Customer").First(&invoice, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Invoice not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve invoice"}) return } c.JSON(http.StatusOK, invoice) } // CreateInvoice creates a new invoice func (ic *InvoiceController) CreateInvoice(c *gin.Context) { var invoice models.Invoice if err := c.ShouldBindJSON(&invoice); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Generate invoice number invoiceNumber, err := ic.generateInvoiceNumber(invoice.InvoiceType) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate invoice number"}) return } invoice.InvoiceNumber = invoiceNumber // Set default values if invoice.IssueDate.IsZero() { invoice.IssueDate = time.Now() } if invoice.DueDate.IsZero() { invoice.DueDate = invoice.IssueDate.AddDate(0, 0, 14) // Default 14 days } if invoice.TaxableSupplyDate.IsZero() { invoice.TaxableSupplyDate = invoice.IssueDate } if invoice.Status == "" { invoice.Status = "draft" } if invoice.PaymentStatus == "" { invoice.PaymentStatus = "unpaid" } if invoice.Currency == "" { invoice.Currency = "CZK" } // Auto-fill supplier information from settings if err := ic.fillSupplierInfo(&invoice); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fill supplier information"}) return } // Auto-fill customer information if customer ID provided if invoice.CustomerID != nil { if err := ic.fillCustomerInfo(&invoice, *invoice.CustomerID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fill customer information"}) return } } // Calculate totals if err := ic.calculateInvoiceTotals(&invoice); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to calculate invoice totals"}) return } // Get current user ID from context userID, exists := c.Get("user_id") if exists { invoice.CreatedBy = userID.(uint) invoice.UpdatedBy = userID.(uint) } // Create invoice with items if err := ic.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&invoice).Error; err != nil { return err } // Create invoice items for i := range invoice.Items { invoice.Items[i].InvoiceID = invoice.ID if err := tx.Create(&invoice.Items[i]).Error; err != nil { return err } } return nil }); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create invoice"}) return } c.JSON(http.StatusCreated, invoice) } // UpdateInvoice updates an existing invoice func (ic *InvoiceController) UpdateInvoice(c *gin.Context) { id := c.Param("id") var invoice models.Invoice if err := ic.db.Preload("Items").Preload("Payments").First(&invoice, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Invoice not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve invoice"}) return } var updateData models.Invoice if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Don't allow editing of sent/paid invoices if invoice.Status == "sent" || invoice.Status == "paid" { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot edit sent or paid invoices"}) return } // Update fields invoice.InvoiceType = updateData.InvoiceType invoice.VariableSymbol = updateData.VariableSymbol invoice.ConstantSymbol = updateData.ConstantSymbol invoice.SpecificSymbol = updateData.SpecificSymbol invoice.IssueDate = updateData.IssueDate invoice.DueDate = updateData.DueDate invoice.TaxableSupplyDate = updateData.TaxableSupplyDate invoice.CustomerName = updateData.CustomerName invoice.CustomerICO = updateData.CustomerICO invoice.CustomerDIC = updateData.CustomerDIC invoice.CustomerAddress = updateData.CustomerAddress invoice.CustomerCity = updateData.CustomerCity invoice.CustomerZIP = updateData.CustomerZIP invoice.CustomerCountry = updateData.CustomerCountry invoice.CustomerEmail = updateData.CustomerEmail invoice.CustomerPhone = updateData.CustomerPhone invoice.Note = updateData.Note invoice.PaymentNote = updateData.PaymentNote invoice.InternalNote = updateData.InternalNote // Update items if err := ic.db.Transaction(func(tx *gorm.DB) error { // Delete existing items if err := tx.Where("invoice_id = ?", invoice.ID).Delete(&models.InvoiceItem{}).Error; err != nil { return err } // Create new items for i := range updateData.Items { updateData.Items[i].InvoiceID = invoice.ID if err := tx.Create(&updateData.Items[i]).Error; err != nil { return err } } return nil }); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update invoice items"}) return } // Recalculate totals invoice.Items = updateData.Items if err := ic.calculateInvoiceTotals(&invoice); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to calculate invoice totals"}) return } // Get current user ID from context userID, exists := c.Get("user_id") if exists { invoice.UpdatedBy = userID.(uint) } if err := ic.db.Save(&invoice).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update invoice"}) return } c.JSON(http.StatusOK, invoice) } // DeleteInvoice deletes an invoice func (ic *InvoiceController) DeleteInvoice(c *gin.Context) { id := c.Param("id") var invoice models.Invoice if err := ic.db.First(&invoice, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Invoice not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve invoice"}) return } // Don't allow deletion of sent/paid invoices if invoice.Status == "sent" || invoice.Status == "paid" { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete sent or paid invoices"}) return } if err := ic.db.Delete(&invoice).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete invoice"}) return } c.JSON(http.StatusOK, gin.H{"message": "Invoice deleted successfully"}) } // GenerateInvoicePDF generates PDF for an invoice func (ic *InvoiceController) GenerateInvoicePDF(c *gin.Context) { id := c.Param("id") var invoice models.Invoice if err := ic.db.Preload("Items").Preload("Customer").First(&invoice, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Invoice not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve invoice"}) return } // Generate PDF pdfPath, err := ic.generatePDF(&invoice) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate PDF"}) return } // Update invoice with PDF path invoice.PDFPath = pdfPath now := time.Now() invoice.PDFGeneratedAt = &now ic.db.Save(&invoice) c.JSON(http.StatusOK, gin.H{ "pdf_path": pdfPath, "message": "PDF generated successfully", }) } // SendInvoice sends invoice via email func (ic *InvoiceController) SendInvoice(c *gin.Context) { id := c.Param("id") var request struct { Emails []string `json:"emails" binding:"required"` Subject string `json:"subject"` Message string `json:"message"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var invoice models.Invoice if err := ic.db.Preload("Items").Preload("Customer").First(&invoice, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Invoice not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve invoice"}) return } // Generate PDF if not exists if invoice.PDFPath == "" { pdfPath, err := ic.generatePDF(&invoice) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate PDF"}) return } invoice.PDFPath = pdfPath } // TODO: Implement email sending functionality // For now, just update the sent status sentToJSON := fmt.Sprintf(`["%s"]`, strings.Join(request.Emails, `","`)) invoice.SentTo = sentToJSON now := time.Now() invoice.SentAt = &now invoice.Status = "sent" if err := ic.db.Save(&invoice).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update invoice status"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Invoice sent successfully", "sent_to": request.Emails, }) } // Customer Management // GetCustomers retrieves all customers func (ic *InvoiceController) GetCustomers(c *gin.Context) { var customers []models.InvoiceCustomer query := ic.db.Where("active = ?", true) // Search by name or ICO if search := c.Query("search"); search != "" { query = query.Where("name ILIKE ? OR ico ILIKE ?", "%"+search+"%", "%"+search+"%") } if err := query.Order("name").Find(&customers).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve customers"}) return } c.JSON(http.StatusOK, customers) } // GetCustomer retrieves a single customer by ID func (ic *InvoiceController) GetCustomer(c *gin.Context) { id := c.Param("id") var customer models.InvoiceCustomer if err := ic.db.First(&customer, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Customer not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve customer"}) return } c.JSON(http.StatusOK, customer) } // CreateCustomer creates a new customer func (ic *InvoiceController) CreateCustomer(c *gin.Context) { var customer models.InvoiceCustomer if err := c.ShouldBindJSON(&customer); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Set default values if customer.Country == "" { customer.Country = "Česká republika" } customer.Active = true // Get current user ID from context userID, exists := c.Get("user_id") if exists { customer.CreatedBy = userID.(uint) customer.UpdatedBy = userID.(uint) } if err := ic.db.Create(&customer).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create customer"}) return } c.JSON(http.StatusCreated, customer) } // UpdateCustomer updates an existing customer func (ic *InvoiceController) UpdateCustomer(c *gin.Context) { id := c.Param("id") var customer models.InvoiceCustomer if err := ic.db.First(&customer, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Customer not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve customer"}) return } var updateData models.InvoiceCustomer if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields customer.Name = updateData.Name customer.ICO = updateData.ICO customer.DIC = updateData.DIC customer.Address = updateData.Address customer.City = updateData.City customer.ZIP = updateData.ZIP customer.Country = updateData.Country customer.Email = updateData.Email customer.Phone = updateData.Phone customer.Website = updateData.Website customer.BusinessType = updateData.BusinessType customer.VATPayer = updateData.VATPayer customer.Notes = updateData.Notes customer.Active = updateData.Active // Get current user ID from context userID, exists := c.Get("user_id") if exists { customer.UpdatedBy = userID.(uint) } if err := ic.db.Save(&customer).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update customer"}) return } c.JSON(http.StatusOK, customer) } // DeleteCustomer deletes a customer func (ic *InvoiceController) DeleteCustomer(c *gin.Context) { id := c.Param("id") // Check if customer has invoices var invoiceCount int64 ic.db.Model(&models.Invoice{}).Where("customer_id = ?", id).Count(&invoiceCount) if invoiceCount > 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete customer with associated invoices"}) return } if err := ic.db.Delete(&models.InvoiceCustomer{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete customer"}) return } c.JSON(http.StatusOK, gin.H{"message": "Customer deleted successfully"}) } // SearchSupplierByICO retrieves supplier information by ICO for invoice settings func (ic *InvoiceController) SearchSupplierByICO(c *gin.Context) { ico := c.Param("ico") if ico == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ICO is required"}) return } // Validate ICO format (should be 8 digits) if len(ico) != 8 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ICO format. Must be 8 digits"}) return } for _, char := range ico { if char < '0' || char > '9' { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ICO format. Must contain only digits"}) return } } // Get information from ARES customerInfo, err := ic.getCustomerInfoFromAres(ico) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "found": false, "error": err.Error(), }) return } // Return in the format expected by the frontend c.JSON(http.StatusOK, gin.H{ "found": true, "name": customerInfo.Name, "address": customerInfo.Address, "city": customerInfo.City, "zip": customerInfo.ZIP, "vatId": customerInfo.DIC, }) } // AutofillCustomerByICO retrieves customer information by ICO func (ic *InvoiceController) AutofillCustomerByICO(c *gin.Context) { ico := c.Query("ico") if ico == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ICO is required"}) return } // Validate ICO format if len(ico) != 8 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ICO format. Must be 8 digits"}) return } for _, char := range ico { if char < '0' || char > '9' { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ICO format. Must contain only digits"}) return } } // First try to find in local database var customer models.InvoiceCustomer if err := ic.db.Where("ico = ? AND active = ?", ico, true).First(&customer).Error; err == nil { c.JSON(http.StatusOK, customer) return } // If not found locally, try to get from external API (Ares) customerInfo, err := ic.getCustomerInfoFromAres(ico) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Customer not found in database or external services"}) return } c.JSON(http.StatusOK, customerInfo) } // Settings Management // GetInvoiceSettings retrieves invoice settings func (ic *InvoiceController) GetInvoiceSettings(c *gin.Context) { var settings models.InvoiceSettings if err := ic.db.First(&settings).Error; err != nil { if err == gorm.ErrRecordNotFound { // Create default settings if none exist settings = models.InvoiceSettings{ CompanyName: "Fotbalový klub", CompanyICO: "12345678", CompanyDIC: "CZ12345678", CompanyAddress: "Sportovní 1", CompanyCity: "Praha", CompanyZIP: "11000", CompanyCountry: "Česká republika", BankName: "Česká spořitelna", BankAccount: "123456789/0800", BankIBAN: "CZ650800000000123456789", BankSWIFT: "GIBACZPX", InvoiceNumberFormat: "F{year}{seq:6}", NextInvoiceNumber: 1, CurrentYear: time.Now().Year(), DefaultPaymentTerm: 14, DefaultVATRate: 21.0, DefaultCurrency: "CZK", EmailSubject: "Faktura č. {invoice_number}", RegistrationNumber: "Spisová značka: 12345", TaxRegistrationNumber: "DIČ: CZ12345678", } ic.db.Create(&settings) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve settings"}) return } } c.JSON(http.StatusOK, settings) } // UpdateInvoiceSettings updates invoice settings func (ic *InvoiceController) UpdateInvoiceSettings(c *gin.Context) { var settings models.InvoiceSettings if err := ic.db.First(&settings).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Settings not found"}) return } var updateData models.InvoiceSettings if err := c.ShouldBindJSON(&updateData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields settings.CompanyName = updateData.CompanyName settings.CompanyICO = updateData.CompanyICO settings.CompanyDIC = updateData.CompanyDIC settings.CompanyAddress = updateData.CompanyAddress settings.CompanyCity = updateData.CompanyCity settings.CompanyZIP = updateData.CompanyZIP settings.CompanyCountry = updateData.CompanyCountry settings.BankName = updateData.BankName settings.BankAccount = updateData.BankAccount settings.BankIBAN = updateData.BankIBAN settings.BankSWIFT = updateData.BankSWIFT settings.InvoiceNumberFormat = updateData.InvoiceNumberFormat settings.NextInvoiceNumber = updateData.NextInvoiceNumber settings.CurrentYear = updateData.CurrentYear settings.DefaultPaymentTerm = updateData.DefaultPaymentTerm settings.DefaultVATRate = updateData.DefaultVATRate settings.DefaultCurrency = updateData.DefaultCurrency settings.EmailFrom = updateData.EmailFrom settings.EmailSubject = updateData.EmailSubject settings.EmailBody = updateData.EmailBody settings.PDFLogoPath = updateData.PDFLogoPath settings.PDFFooter = updateData.PDFFooter settings.RegistrationNumber = updateData.RegistrationNumber settings.TaxRegistrationNumber = updateData.TaxRegistrationNumber // Get current user ID from context userID, exists := c.Get("user_id") if exists { settings.UpdatedBy = userID.(uint) } if err := ic.db.Save(&settings).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update settings"}) return } c.JSON(http.StatusOK, settings) } // Helper functions func (ic *InvoiceController) generateInvoiceNumber(invoiceType string) (string, error) { year := time.Now().Year() // Get or create sequence for this type and year var sequence models.InvoiceSequence if err := ic.db.Where("type = ? AND year = ?", invoiceType, year).First(&sequence).Error; err != nil { if err == gorm.ErrRecordNotFound { // Create new sequence sequence = models.InvoiceSequence{ Type: invoiceType, Year: year, CurrentNumber: 1, Prefix: getInvoiceTypePrefix(invoiceType), Padding: 6, } if err := ic.db.Create(&sequence).Error; err != nil { return "", err } } else { return "", err } } // Generate invoice number invoiceNumber := fmt.Sprintf("%s%06d", sequence.Prefix, sequence.CurrentNumber) // Increment sequence sequence.CurrentNumber++ ic.db.Save(&sequence) return invoiceNumber, nil } func getInvoiceTypePrefix(invoiceType string) string { switch invoiceType { case "faktura": return "F" case "zalohova_faktura": return "ZF" case "proforma_faktura": return "PF" case "dobropis": return "D" default: return "F" } } func (ic *InvoiceController) fillSupplierInfo(invoice *models.Invoice) error { var settings models.InvoiceSettings if err := ic.db.First(&settings).Error; err != nil { return err } invoice.SupplierName = settings.CompanyName invoice.SupplierICO = settings.CompanyICO invoice.SupplierDIC = settings.CompanyDIC invoice.SupplierAddress = settings.CompanyAddress invoice.SupplierCity = settings.CompanyCity invoice.SupplierZIP = settings.CompanyZIP invoice.SupplierCountry = settings.CompanyCountry invoice.BankName = settings.BankName invoice.BankAccount = settings.BankAccount invoice.BankIBAN = settings.BankIBAN invoice.BankSWIFT = settings.BankSWIFT return nil } func (ic *InvoiceController) fillCustomerInfo(invoice *models.Invoice, customerID uint) error { var customer models.InvoiceCustomer if err := ic.db.First(&customer, customerID).Error; err != nil { return err } invoice.CustomerName = customer.Name invoice.CustomerICO = customer.ICO invoice.CustomerDIC = customer.DIC invoice.CustomerAddress = customer.Address invoice.CustomerCity = customer.City invoice.CustomerZIP = customer.ZIP invoice.CustomerCountry = customer.Country invoice.CustomerEmail = customer.Email invoice.CustomerPhone = customer.Phone return nil } func (ic *InvoiceController) calculateInvoiceTotals(invoice *models.Invoice) error { var totalVAT, totalAmountVAT, totalAmountWithoutVAT float64 for i := range invoice.Items { item := &invoice.Items[i] // Calculate item totals item.TotalPrice = item.Quantity * item.UnitPrice item.VATAmount = item.TotalPrice * item.VATRate / 100 item.TotalWithVAT = item.TotalPrice + item.VATAmount // Add to invoice totals totalAmountWithoutVAT += item.TotalPrice totalVAT += item.VATAmount totalAmountVAT += item.TotalWithVAT } invoice.TotalAmountWithoutVAT = totalAmountWithoutVAT invoice.TotalVAT = totalVAT invoice.TotalAmountVAT = totalAmountVAT invoice.TotalAmount = totalAmountVAT return nil } func (ic *InvoiceController) generatePDF(invoice *models.Invoice) (string, error) { // Create uploads directory if it doesn't exist uploadsDir := "uploads/invoices" if err := os.MkdirAll(uploadsDir, 0755); err != nil { return "", fmt.Errorf("failed to create uploads directory: %w", err) } // Create PDF pdf := gofpdf.New("P", "mm", "A4", "") pdf.AddPage() // Set font pdf.SetFont("Arial", "", 12) // Title pdf.SetFont("Arial", "B", 16) pdf.Cell(0, 10, "FAKTURA") pdf.Ln(15) // Reset font pdf.SetFont("Arial", "", 12) // Invoice details pdf.Cell(40, 7, "Číslo faktury:") pdf.Cell(0, 7, invoice.InvoiceNumber) pdf.Ln(7) pdf.Cell(40, 7, "Datum vystavení:") pdf.Cell(0, 7, invoice.IssueDate.Format("02.01.2006")) pdf.Ln(7) pdf.Cell(40, 7, "Datum splatnosti:") pdf.Cell(0, 7, invoice.DueDate.Format("02.01.2006")) pdf.Ln(15) // Supplier info pdf.SetFont("Arial", "B", 12) pdf.Cell(0, 7, "Dodavatel:") pdf.Ln(7) pdf.SetFont("Arial", "", 12) pdf.Cell(0, 7, invoice.SupplierName) pdf.Ln(5) pdf.Cell(0, 7, fmt.Sprintf("IČO: %s", invoice.SupplierICO)) pdf.Ln(5) if invoice.SupplierDIC != "" { pdf.Cell(0, 7, fmt.Sprintf("DIČ: %s", invoice.SupplierDIC)) pdf.Ln(5) } pdf.Cell(0, 7, invoice.SupplierAddress) pdf.Ln(5) pdf.Cell(0, 7, fmt.Sprintf("%s %s", invoice.SupplierZIP, invoice.SupplierCity)) pdf.Ln(15) // Customer info pdf.SetFont("Arial", "B", 12) pdf.Cell(0, 7, "Odběratel:") pdf.Ln(7) pdf.SetFont("Arial", "", 12) pdf.Cell(0, 7, invoice.CustomerName) pdf.Ln(5) pdf.Cell(0, 7, fmt.Sprintf("IČO: %s", invoice.CustomerICO)) pdf.Ln(5) if invoice.CustomerDIC != "" { pdf.Cell(0, 7, fmt.Sprintf("DIČ: %s", invoice.CustomerDIC)) pdf.Ln(5) } pdf.Cell(0, 7, invoice.CustomerAddress) pdf.Ln(5) pdf.Cell(0, 7, fmt.Sprintf("%s %s", invoice.CustomerZIP, invoice.CustomerCity)) pdf.Ln(15) // Items table header pdf.SetFont("Arial", "B", 12) pdf.Cell(100, 7, "Popis") pdf.Cell(20, 7, "Množství") pdf.Cell(30, 7, "Cena/jedn.") pdf.Cell(20, 7, "DPH %") pdf.Cell(30, 7, "Celkem s DPH") pdf.Ln(7) // Items pdf.SetFont("Arial", "", 12) for _, item := range invoice.Items { pdf.Cell(100, 7, item.Description) pdf.Cell(20, 7, fmt.Sprintf("%.2f", item.Quantity)) pdf.Cell(30, 7, fmt.Sprintf("%.2f Kč", item.UnitPrice)) pdf.Cell(20, 7, fmt.Sprintf("%.0f%%", item.VATRate)) pdf.Cell(30, 7, fmt.Sprintf("%.2f Kč", item.TotalWithVAT)) pdf.Ln(7) } // Total pdf.Ln(10) pdf.SetFont("Arial", "B", 12) pdf.Cell(150, 7, "Celková částka k úhradě:") pdf.Cell(0, 7, fmt.Sprintf("%.2f Kč", invoice.TotalAmountVAT)) pdf.Ln(7) // Bank info pdf.Ln(15) pdf.SetFont("Arial", "", 10) pdf.Cell(0, 7, fmt.Sprintf("Číslo účtu: %s", invoice.BankAccount)) pdf.Ln(5) if invoice.BankIBAN != "" { pdf.Cell(0, 7, fmt.Sprintf("IBAN: %s", invoice.BankIBAN)) pdf.Ln(5) } if invoice.BankSWIFT != "" { pdf.Cell(0, 7, fmt.Sprintf("SWIFT: %s", invoice.BankSWIFT)) pdf.Ln(5) } // Save PDF fileName := fmt.Sprintf("invoice_%s.pdf", invoice.InvoiceNumber) filePath := fmt.Sprintf("%s/%s", uploadsDir, fileName) err := pdf.OutputFileAndClose(filePath) if err != nil { return "", fmt.Errorf("failed to save PDF: %w", err) } return filePath, nil } // ARES API response structures type AresAddress struct { KodStatu string `json:"kodStatu"` NazevStatu string `json:"nazevStatu"` KodKraje int `json:"kodKraje"` NazevKraje string `json:"nazevKraje"` KodOkresu int `json:"kodOkresu"` NazevOkresu string `json:"nazevOkresu"` KodObce int `json:"kodObce"` NazevObce string `json:"nazevObce"` KodUlice int `json:"kodUlice"` NazevUlice string `json:"nazevUlice"` CisloDomovni int `json:"cisloDomovni"` CisloOrientacni int `json:"cisloOrientacni"` KodCastiObce int `json:"kodCastiObce"` NazevCastiObce string `json:"nazevCastiObce"` KodAdresnihoMista int `json:"kodAdresnihoMista"` Psc int `json:"psc"` TextovaAdresa string `json:"textovaAdresa"` StandardizaceAdresy bool `json:"standardizaceAdresy"` TypCisloDomovni int `json:"typCisloDomovni"` } type AresSubject struct { ICO string `json:"ico"` ObchodniJmeno string `json:"obchodniJmeno"` DIC string `json:"dic"` PravniForma string `json:"pravniForma"` Sidlo AresAddress `json:"sidlo"` DatumVzniku string `json:"datumVzniku"` DatumAktualizace string `json:"datumAktualizace"` PrimarniZdroj string `json:"primarniZdroj"` SeznamRegistraci struct { StavZdrojeRos string `json:"stavZdrojeRos"` StavZdrojeVr string `json:"stavZdrojeVr"` StavZdrojeRes string `json:"stavZdrojeRes"` StavZdrojeRzp string `json:"stavZdrojeRzp"` } `json:"seznamRegistraci"` } type AresResponse struct { EconomicSubjecets []AresSubject `json:"ekonomickeSubjekty"` PocetCelkem int `json:"pocetCelkem"` Pozadavek struct { Start int `json:"start"` Pocet int `json:"pocet"` Razeni []string `json:"razeni"` Filtr []string `json:"filtr"` } `json:"pozadavek"` } func (ic *InvoiceController) getCustomerInfoFromAres(ico string) (*models.InvoiceCustomer, error) { // ARES API endpoint - ICO as path parameter, not query parameter url := fmt.Sprintf("https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/%s", ico) // Create HTTP request with proper headers req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") // Make the request client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to call ARES API: %w", err) } defer resp.Body.Close() // Check response status if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("ARES API returned status %d: %s", resp.StatusCode, string(body)) } // Read and parse response body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Try parsing as direct subject first (for single ICO lookup) var subject AresSubject if err := json.Unmarshal(body, &subject); err != nil { // If that fails, try parsing as wrapped response var wrappedResponse AresResponse if err := json.Unmarshal(body, &wrappedResponse); err != nil { return nil, fmt.Errorf("failed to parse ARES response: %w", err) } // Check if we got any results if len(wrappedResponse.EconomicSubjecets) == 0 { return nil, fmt.Errorf("no subject found with IČO %s", ico) } subject = wrappedResponse.EconomicSubjecets[0] } // Check if subject is active (at least one source should be active) if subject.SeznamRegistraci.StavZdrojeRos != "AKTIVNI" && subject.SeznamRegistraci.StavZdrojeVr != "AKTIVNI" && subject.SeznamRegistraci.StavZdrojeRes != "AKTIVNI" && subject.SeznamRegistraci.StavZdrojeRzp != "AKTIVNI" { return nil, fmt.Errorf("subject with IČO %s is not active", ico) } // Build address string - use textovaAdresa if available, otherwise construct it address := subject.Sidlo.TextovaAdresa if address == "" { address = subject.Sidlo.NazevUlice if subject.Sidlo.CisloDomovni > 0 { address += fmt.Sprintf(" %d", subject.Sidlo.CisloDomovni) } if subject.Sidlo.CisloOrientacni > 0 { address += fmt.Sprintf("/%d", subject.Sidlo.CisloOrientacni) } if subject.Sidlo.NazevObce != "" { if address != "" { address += ", " } address += subject.Sidlo.NazevObce } } // Convert PSC from int to string zipCode := fmt.Sprintf("%d", subject.Sidlo.Psc) // Create customer model customer := &models.InvoiceCustomer{ Name: subject.ObchodniJmeno, ICO: subject.ICO, DIC: subject.DIC, Address: address, City: subject.Sidlo.NazevObce, ZIP: zipCode, Country: subject.Sidlo.NazevStatu, Active: true, } return customer, nil }