feat: implement product reference number generation and update inventory transaction response structure
Deploy Application / deploy (push) Successful in 28s Details

This commit is contained in:
Habib Fatkhul Rohman 2025-11-24 14:28:42 +07:00
parent 8812a294d8
commit 2676a20026
4 changed files with 164 additions and 15 deletions

View File

@ -1,7 +1,12 @@
package entities package entities
import ( import (
"fmt"
"strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
) )
type MProductEntity struct { type MProductEntity struct {
@ -52,6 +57,7 @@ type MProductEntity struct {
UomToUom MUomEntity `gorm:"foreignKey:UomToUomID;references:ID"` UomToUom MUomEntity `gorm:"foreignKey:UomToUomID;references:ID"`
CrossReferences []MCrossReferenceEntity `gorm:"foreignKey:ProductID;references:ID"` CrossReferences []MCrossReferenceEntity `gorm:"foreignKey:ProductID;references:ID"`
InventoryTransactions []InventoryTransactionEntity `gorm:"foreignKey:ProductID;references:ID"` InventoryTransactions []InventoryTransactionEntity `gorm:"foreignKey:ProductID;references:ID"`
InventoryStorages []InventoryStorageEntity `gorm:"foreignKey:ProductID;references:ID"`
FullAuditTrail FullAuditTrail
} }
@ -59,3 +65,64 @@ type MProductEntity struct {
func (MProductEntity) TableName() string { func (MProductEntity) TableName() string {
return "m_products" return "m_products"
} }
// GenerateRefNumberProduct generates a new reference number for a product
func GenerateRefNumberProduct(db *gorm.DB, clientId string, categoryId string) (string, error) {
prefix := "PRD"
// Ambil nama client berdasarkan clientId
var client struct {
Name string
}
if err := db.Table("m_clients").Select("name").Where("id = ?", clientId).First(&client).Error; err != nil {
return "", fmt.Errorf("client not found")
}
if client.Name == "" {
return "", fmt.Errorf("client name is empty")
}
// Ambil search key kategori berdasarkan categoryId
var category struct {
SearchKey string
}
if err := db.Table("m_categories").Select("search_key").Where("id = ?", categoryId).First(&category).Error; err != nil {
return "", fmt.Errorf("category not found")
}
if category.SearchKey == "" {
return "", fmt.Errorf("category search key is empty")
}
categoryInitial := ""
lettersOnly := ""
for _, r := range category.SearchKey {
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
lettersOnly += string(r)
}
}
if len(lettersOnly) > 0 {
categoryInitial = strings.ToUpper(lettersOnly)
}
// Ambil tahun dan bulan sekarang
now := time.Now()
period := now.Format("0601") // YYYYMM
// Cari sequence terakhir untuk client dan kategori di periode ini
var lastProduct MProductEntity
prefixQuery := fmt.Sprintf("%s-%s-%s", prefix, categoryInitial, period)
err := db.
Where("client_id = ? AND category_id = ? AND LEFT(ref_number, ?) = ?", clientId, categoryId, len(prefixQuery), prefixQuery).
Order("ref_number DESC").
First(&lastProduct).Error
seq := 1
if err == nil && lastProduct.RefNumber != "" {
parts := strings.Split(lastProduct.RefNumber, "-")
if len(parts) == 4 {
fmt.Sscanf(parts[3], "%d", &seq)
seq++
}
}
refNum := fmt.Sprintf("%s-%s-%s-%04d", prefix, categoryInitial, period, seq)
return refNum, nil
}

View File

@ -34,7 +34,7 @@ var (
type ( type (
ProductCreateRequest struct { ProductCreateRequest struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
RefNumber string `json:"ref_number" binding:"required"` // RefNumber string `json:"ref_number" binding:"required"`
SKU string `json:"sku" binding:"required"` SKU string `json:"sku" binding:"required"`
Description string `json:"description"` Description string `json:"description"`
Status string `json:"status" binding:"required"` Status string `json:"status" binding:"required"`
@ -69,7 +69,7 @@ type (
ProductUpdateRequest struct { ProductUpdateRequest struct {
Name *string `json:"name"` Name *string `json:"name"`
RefNumber *string `json:"ref_number"` // RefNumber *string `json:"ref_number"`
SKU *string `json:"sku"` SKU *string `json:"sku"`
Description *string `json:"description"` Description *string `json:"description"`
Status *string `json:"status"` Status *string `json:"status"`
@ -154,6 +154,13 @@ type (
ProductInventoryTransactionResponse struct { ProductInventoryTransactionResponse struct {
ID string `json:"id"` ID string `json:"id"`
TransactionDate string `json:"transaction_date"`
TransactionType string `json:"transaction_type"` TransactionType string `json:"transaction_type"`
TransactionQuantity float64 `json:"transaction_quantity"`
Lot string `json:"lot"`
Locater string `json:"locater"`
InvReceiptRef string `json:"inv_receipt_ref,omitempty"`
InvIssueRef string `json:"inv_issue_ref,omitempty"`
InvMoveRef string `json:"inv_move_ref,omitempty"`
} }
) )

View File

@ -116,13 +116,19 @@ func (r *productRepository) GetById(ctx context.Context, tx *gorm.DB, productId
Preload("UomToUom"). Preload("UomToUom").
Preload("CrossReferences"). Preload("CrossReferences").
Preload("CrossReferences.Vendor"). Preload("CrossReferences.Vendor").
Preload("InvTransactions"). Preload("InventoryStorages").
Preload("InvTransactions.Product"). Preload("InventoryTransactions").
Preload("InvTransactions.Client"). Preload("InventoryTransactions.Client").
Preload("InvTransactions.Aisle"). Preload("InventoryTransactions.Aisle").
Preload("InvTransactions.InvReceipt"). Preload("InventoryTransactions.InvReceipt").
Preload("InvTransactions.InvIssue"). Preload("InventoryTransactions.InvReceipt.ReceiptLines").
Preload("InvTransactions.InvMove"). Preload("InventoryTransactions.InvReceipt.ReceiptLines.Product").
Preload("InventoryTransactions.InvIssue").
Preload("InventoryTransactions.InvIssue.IssueLines").
Preload("InventoryTransactions.InvIssue.IssueLines.Product").
Preload("InventoryTransactions.InvMove").
Preload("InventoryTransactions.InvMove.MovementLines").
Preload("InventoryTransactions.InvMove.MovementLines.Product").
First(&product, "id = ?", productId).Error; err != nil { First(&product, "id = ?", productId).Error; err != nil {
return product, err return product, err
} }

View File

@ -8,7 +8,9 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/modules/product/query" "github.com/Caknoooo/go-gin-clean-starter/modules/product/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/product/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/product/repository"
pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -81,9 +83,14 @@ func (s *productService) Create(ctx context.Context, req dto.ProductCreateReques
tx.Rollback() tx.Rollback()
} }
}() }()
refNumber, err := entities.GenerateRefNumberProduct(tx, req.ClientID, *req.CategoryID)
if err != nil {
tx.Rollback()
return dto.ProductResponse{}, err
}
product := entities.MProductEntity{ product := entities.MProductEntity{
Name: req.Name, Name: req.Name,
RefNumber: req.RefNumber, RefNumber: refNumber,
SKU: req.SKU, SKU: req.SKU,
Description: req.Description, Description: req.Description,
Status: req.Status, Status: req.Status,
@ -175,9 +182,6 @@ func (s *productService) Update(ctx context.Context, req dto.ProductUpdateReques
if req.Name != nil { if req.Name != nil {
product.Name = *req.Name product.Name = *req.Name
} }
if req.RefNumber != nil {
product.RefNumber = *req.RefNumber
}
if req.SKU != nil { if req.SKU != nil {
product.SKU = *req.SKU product.SKU = *req.SKU
} }
@ -331,6 +335,70 @@ func mapProductToResponse(product entities.MProductEntity) dto.ProductResponse {
}) })
} }
logrus.Infof("Inventory Transactions Count: %d", len(product.InventoryTransactions))
invTransactions := make([]dto.ProductInventoryTransactionResponse, 0, len(product.InventoryTransactions))
for _, it := range product.InventoryTransactions {
var transactionQuantity float64
var lot, locater, invReceiptRef, invIssueRef, invMoveRef string
var transactionDate string
// Receipt
if it.InvReceipt.ID != uuid.Nil {
invReceiptRef = it.InvReceipt.ReferenceNumber
transactionDate = utils.DateTimeToString(it.TransactionDate)
// Cari line yang sesuai product
for _, line := range it.InvReceipt.ReceiptLines {
if line.ProductID == it.ProductID {
transactionQuantity = line.Quantity
lot = line.BatchNumber
// Jika ada field lokasi, isi di sini
// locater = line.Locater
break
}
}
}
// Issue
if it.InvIssue.ID != uuid.Nil {
invIssueRef = it.InvIssue.DocumentNumber
transactionDate = utils.DateTimeToString(it.TransactionDate)
for _, line := range it.InvIssue.IssueLines {
if line.ProductID == it.ProductID {
transactionQuantity = line.IssuedQuantity
// lot = line.BatchNumber
// locater = line.Locater
break
}
}
}
// Move
if it.InvMove.ID != uuid.Nil {
invMoveRef = it.InvMove.MovementNumber
transactionDate = utils.DateTimeToString(it.TransactionDate)
for _, line := range it.InvMove.MovementLines {
if line.ProductID == it.ProductID {
transactionQuantity = line.MovedQuantity
// lot = line.BatchNumber
// locater = line.Locater
break
}
}
}
invTransactions = append(invTransactions, dto.ProductInventoryTransactionResponse{
ID: it.ID.String(),
TransactionDate: transactionDate,
TransactionType: it.TransactionType,
TransactionQuantity: transactionQuantity,
Lot: lot,
Locater: locater,
InvReceiptRef: invReceiptRef,
InvIssueRef: invIssueRef,
InvMoveRef: invMoveRef,
})
}
return dto.ProductResponse{ return dto.ProductResponse{
ID: product.ID.String(), ID: product.ID.String(),
Name: product.Name, Name: product.Name,
@ -396,5 +464,6 @@ func mapProductToResponse(product entities.MProductEntity) dto.ProductResponse {
Name: product.UomToUom.Name, Name: product.UomToUom.Name,
}, },
CrossReferences: crossRefs, CrossReferences: crossRefs,
InvTransactions: invTransactions,
} }
} }