Files
MyClub/internal/controllers/invoice_controller.go
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

1100 lines
31 KiB
Go

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
}