mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
implement fifo-v2 to transfer stock pakan
This commit is contained in:
@@ -199,18 +199,18 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rule.ScopeSQL != nil && strings.TrimSpace(*rule.ScopeSQL) != "" {
|
if rule.ScopeSQL != nil && strings.TrimSpace(*rule.ScopeSQL) != "" {
|
||||||
whereParts = append(whereParts, fmt.Sprintf("(%s)", strings.TrimSpace(*rule.ScopeSQL)))
|
whereParts = append(whereParts, fmt.Sprintf("(%s)", normalizeScopeSQL(*rule.ScopeSQL)))
|
||||||
}
|
}
|
||||||
|
|
||||||
subquery := fmt.Sprintf(`
|
subquery := fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
? AS source_table,
|
?::text AS source_table,
|
||||||
? AS legacy_type_key,
|
?::text AS legacy_type_key,
|
||||||
? AS function_code,
|
?::text AS function_code,
|
||||||
src.%s AS source_id,
|
src.%s AS source_id,
|
||||||
src.%s AS product_warehouse_id,
|
src.%s AS product_warehouse_id,
|
||||||
%s AS sort_at,
|
%s AS sort_at,
|
||||||
? AS sort_priority,
|
?::int AS sort_priority,
|
||||||
%s AS quantity,
|
%s AS quantity,
|
||||||
%s AS used_quantity,
|
%s AS used_quantity,
|
||||||
%s AS pending_quantity,
|
%s AS pending_quantity,
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package fifo_stock_v2
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func normalizeScopeSQL(scopeSQL string) string {
|
||||||
|
scopeSQL = strings.TrimSpace(scopeSQL)
|
||||||
|
if scopeSQL == "" {
|
||||||
|
return scopeSQL
|
||||||
|
}
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(len(scopeSQL) + 16)
|
||||||
|
|
||||||
|
inSingleQuote := false
|
||||||
|
inDoubleQuote := false
|
||||||
|
|
||||||
|
for i := 0; i < len(scopeSQL); {
|
||||||
|
ch := scopeSQL[i]
|
||||||
|
|
||||||
|
if inSingleQuote {
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
if ch == '\'' {
|
||||||
|
if i < len(scopeSQL) && scopeSQL[i] == '\'' {
|
||||||
|
out.WriteByte(scopeSQL[i])
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
inSingleQuote = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if inDoubleQuote {
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
if ch == '"' {
|
||||||
|
inDoubleQuote = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '\'' {
|
||||||
|
inSingleQuote = true
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '"' {
|
||||||
|
inDoubleQuote = true
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIdentifierStart(ch) {
|
||||||
|
start := i
|
||||||
|
i++
|
||||||
|
for i < len(scopeSQL) && isIdentifierPart(scopeSQL[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
token := scopeSQL[start:i]
|
||||||
|
if strings.EqualFold(token, "deleted_at") && !hasAliasQualifier(scopeSQL, start) {
|
||||||
|
out.WriteString("src.deleted_at")
|
||||||
|
} else {
|
||||||
|
out.WriteString(token)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAliasQualifier(scopeSQL string, tokenStart int) bool {
|
||||||
|
for i := tokenStart - 1; i >= 0; i-- {
|
||||||
|
switch scopeSQL[i] {
|
||||||
|
case ' ', '\t', '\n', '\r':
|
||||||
|
continue
|
||||||
|
case '.':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIdentifierStart(ch byte) bool {
|
||||||
|
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIdentifierPart(ch byte) bool {
|
||||||
|
return isIdentifierStart(ch) || (ch >= '0' && ch <= '9')
|
||||||
|
}
|
||||||
@@ -52,7 +52,6 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||||
@@ -71,6 +70,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
)
|
)
|
||||||
|
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||||
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
expenseBridge := sTransfer.NewTransferExpenseBridge(
|
expenseBridge := sTransfer.NewTransferExpenseBridge(
|
||||||
db,
|
db,
|
||||||
stockTransferRepo,
|
stockTransferRepo,
|
||||||
@@ -111,7 +111,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService, expenseBridge)
|
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService, fifoStockV2Service, expenseBridge)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
TransferRoutes(router, userService, transferService)
|
TransferRoutes(router, userService, transferService)
|
||||||
|
|||||||
@@ -46,10 +46,11 @@ type transferService struct {
|
|||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
DocumentSvc commonSvc.DocumentService
|
DocumentSvc commonSvc.DocumentService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoSvc commonSvc.FifoService
|
||||||
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
ExpenseBridge TransferExpenseBridge
|
ExpenseBridge TransferExpenseBridge
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, expenseBridge TransferExpenseBridge) TransferService {
|
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService {
|
||||||
return &transferService{
|
return &transferService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -64,6 +65,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
|||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
DocumentSvc: documentSvc,
|
DocumentSvc: documentSvc,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
ExpenseBridge: expenseBridge,
|
ExpenseBridge: expenseBridge,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,26 +444,73 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pakanProducts := map[uint]bool{}
|
||||||
|
if s.FifoStockV2Svc != nil && len(req.Products) > 0 {
|
||||||
|
pakanProducts, err = s.resolvePakanProducts(c.Context(), tx, req.Products)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
detail := detailMap[uint64(product.ProductID)]
|
detail := detailMap[uint64(product.ProductID)]
|
||||||
|
|
||||||
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
outUsageQty := 0.0
|
||||||
UsableKey: fifo.UsableKeyStockTransferOut,
|
outPendingQty := 0.0
|
||||||
UsableID: uint(detail.Id),
|
useFifoV2 := s.FifoStockV2Svc != nil && pakanProducts[uint(product.ProductID)]
|
||||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
if useFifoV2 {
|
||||||
Quantity: product.ProductQty,
|
s.Log.Infof(
|
||||||
AllowPending: false,
|
"[fifo-v2][transfer] use reflow movement=%s detail_id=%d product_id=%d source_pw=%d qty=%.3f",
|
||||||
Tx: tx,
|
entityTransfer.MovementNumber,
|
||||||
})
|
detail.Id,
|
||||||
if err != nil {
|
product.ProductID,
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
*detail.SourceProductWarehouseID,
|
||||||
|
product.ProductQty,
|
||||||
|
)
|
||||||
|
reflowResult, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: "PAKAN",
|
||||||
|
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||||
|
Usable: commonSvc.FifoStockV2Ref{
|
||||||
|
ID: uint(detail.Id),
|
||||||
|
LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
|
||||||
|
FunctionCode: "STOCK_TRANSFER_OUT",
|
||||||
|
},
|
||||||
|
DesiredQty: product.ProductQty,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
||||||
|
}
|
||||||
|
outUsageQty = reflowResult.Allocate.AllocatedQty
|
||||||
|
outPendingQty = reflowResult.Allocate.PendingQty
|
||||||
|
s.Log.Infof(
|
||||||
|
"[fifo-v2][transfer] reflow result movement=%s detail_id=%d usage=%.3f pending=%.3f",
|
||||||
|
entityTransfer.MovementNumber,
|
||||||
|
detail.Id,
|
||||||
|
outUsageQty,
|
||||||
|
outPendingQty,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||||
|
UsableKey: fifo.UsableKeyStockTransferOut,
|
||||||
|
UsableID: uint(detail.Id),
|
||||||
|
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||||
|
Quantity: product.ProductQty,
|
||||||
|
AllowPending: false,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
||||||
|
}
|
||||||
|
outUsageQty = consumeResult.UsageQuantity
|
||||||
|
outPendingQty = consumeResult.PendingQuantity
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
Where("id = ?", detail.Id).
|
Where("id = ?", detail.Id).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"usage_qty": consumeResult.UsageQuantity,
|
"usage_qty": outUsageQty,
|
||||||
"pending_qty": consumeResult.PendingQuantity,
|
"pending_qty": outPendingQty,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
s.Log.Errorf("Failed to update tracking usage for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
s.Log.Errorf("Failed to update tracking usage for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
||||||
@@ -493,6 +542,17 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||||
|
inAddedQty := 0.0
|
||||||
|
if useFifoV2 {
|
||||||
|
s.Log.Infof(
|
||||||
|
"[fifo-v2][transfer] stock-in uses replenish path movement=%s detail_id=%d product_id=%d dest_pw=%d qty=%.3f",
|
||||||
|
entityTransfer.MovementNumber,
|
||||||
|
detail.Id,
|
||||||
|
product.ProductID,
|
||||||
|
*detail.DestProductWarehouseID,
|
||||||
|
product.ProductQty,
|
||||||
|
)
|
||||||
|
}
|
||||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||||
StockableKey: fifo.StockableKeyStockTransferIn,
|
StockableKey: fifo.StockableKeyStockTransferIn,
|
||||||
StockableID: uint(detail.Id),
|
StockableID: uint(detail.Id),
|
||||||
@@ -505,11 +565,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Errorf("Failed to replenish stock for product_id=%d, pw_id=%d, qty=%.2f: %+v", product.ProductID, *detail.DestProductWarehouseID, product.ProductQty, err)
|
s.Log.Errorf("Failed to replenish stock for product_id=%d, pw_id=%d, qty=%.2f: %+v", product.ProductID, *detail.DestProductWarehouseID, product.ProductQty, err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menambah stok gudang tujuan")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menambah stok gudang tujuan")
|
||||||
}
|
}
|
||||||
|
inAddedQty = replenishResult.AddedQuantity
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
Where("id = ?", detail.Id).
|
Where("id = ?", detail.Id).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"total_qty": replenishResult.AddedQuantity,
|
"total_qty": inAddedQty,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
||||||
@@ -596,6 +657,53 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferService) resolvePakanProducts(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
products []validation.TransferProduct,
|
||||||
|
) (map[uint]bool, error) {
|
||||||
|
out := make(map[uint]bool, len(products))
|
||||||
|
if len(products) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
productIDs := make([]uint, 0, len(products))
|
||||||
|
seen := make(map[uint]struct{}, len(products))
|
||||||
|
for _, product := range products {
|
||||||
|
if product.ProductID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[product.ProductID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[product.ProductID] = struct{}{}
|
||||||
|
productIDs = append(productIDs, product.ProductID)
|
||||||
|
}
|
||||||
|
if len(productIDs) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
}
|
||||||
|
var rows []row
|
||||||
|
err := tx.WithContext(ctx).
|
||||||
|
Table("flags f").
|
||||||
|
Select("DISTINCT f.flagable_id AS product_id").
|
||||||
|
Where("f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where("f.name IN ?", []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}).
|
||||||
|
Where("f.flagable_id IN ?", productIDs).
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
out[row.ProductID] = true
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
|
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
|
||||||
if s.ExpenseBridge == nil || transferID == 0 || len(payloads) == 0 {
|
if s.ExpenseBridge == nil || transferID == 0 || len(payloads) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user