mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
1100 lines
31 KiB
Go
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
|
|
}
|