fix: all implemented fifo v2

This commit is contained in:
Hafizh A. Y
2026-03-02 12:44:20 +07:00
parent dd61b66af0
commit d5a1751868
11 changed files with 319 additions and 369 deletions
+20 -52
View File
@@ -11,12 +11,10 @@ import (
"strings" "strings"
"time" "time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database" "gitlab.com/mbugroup/lti-api.git/internal/database"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -61,13 +59,7 @@ func main() {
ctx := context.Background() ctx := context.Background()
db := database.Connect(config.DBHost, config.DBName) db := database.Connect(config.DBHost, config.DBName)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
fifoSvc := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, nil)
fifoStockV2Svc := commonSvc.NewFifoStockV2Service(db, nil) fifoStockV2Svc := commonSvc.NewFifoStockV2Service(db, nil)
if err := registerAdjustmentFIFO(fifoSvc); err != nil {
log.Fatalf("failed to register adjustment fifo config: %v", err)
}
adjustments, err := loadAdjustments(ctx, db, ids) adjustments, err := loadAdjustments(ctx, db, ids)
if err != nil { if err != nil {
@@ -185,7 +177,7 @@ func main() {
} }
fmt.Printf( fmt.Printf(
"PLAN adj=%d lane=STOCKABLE function=%s total=%.3f remove_qty=%.3f action=reverse_stock+delete\n", "PLAN adj=%d lane=STOCKABLE function=%s total=%.3f remove_qty=%.3f action=reflow_to_zero+delete\n",
adj.ID, adj.ID,
route.FunctionCode, route.FunctionCode,
adj.TotalQty, adj.TotalQty,
@@ -198,16 +190,25 @@ func main() {
} }
err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if removeQty > 0 { if err := tx.WithContext(ctx).
if err := fifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{ Table("adjustment_stocks").
StockableKey: fifo.StockableKeyAdjustmentIn, Where("id = ?", adj.ID).
StockableID: adj.ID, Updates(map[string]any{
ProductWarehouseID: adj.ProductWarehouseID, "total_qty": 0,
Quantity: -removeQty, "total_used": 0,
Tx: tx, }).Error; err != nil {
}); err != nil { return fmt.Errorf("set stockable qty to zero: %w", err)
return fmt.Errorf("reverse stockable quantity: %w", err) }
}
reflowReq := commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: route.FlagGroupCode,
ProductWarehouseID: adj.ProductWarehouseID,
AsOf: &adj.CreatedAt,
IdempotencyKey: fmt.Sprintf("delete-adjustment-stockable-%d-%d", adj.ID, time.Now().UnixNano()),
Tx: tx,
}
if _, err := fifoStockV2Svc.Reflow(ctx, reflowReq); err != nil {
return fmt.Errorf("reflow stockable to zero: %w", err)
} }
if err := hardDeleteStockableAllocations(ctx, tx, fifo.StockableKeyAdjustmentIn.String(), adj.ID); err != nil { if err := hardDeleteStockableAllocations(ctx, tx, fifo.StockableKeyAdjustmentIn.String(), adj.ID); err != nil {
@@ -243,39 +244,6 @@ func main() {
} }
} }
func registerAdjustmentFIFO(fifoSvc commonSvc.FifoService) error {
if err := fifoSvc.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyAdjustmentIn,
Table: "adjustment_stocks",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
}); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") {
return err
}
if err := fifoSvc.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyAdjustmentOut,
Table: "adjustment_stocks",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
}); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") {
return err
}
return nil
}
func modeLabel(apply bool) string { func modeLabel(apply bool) string {
if apply { if apply {
return "APPLY" return "APPLY"
+85 -1
View File
@@ -178,14 +178,35 @@ func main() {
successApply++ successApply++
} }
orphanPopulationRows := int64(0)
syncedPopulationQtyRows := int64(0)
syncedPopulationUsedRows := int64(0)
if rowsOrphan, rowsQty, rowsUsed, err := resyncProjectFlockPopulation(ctx, db, projectFlockKandangID); err != nil {
fmt.Printf("FAIL population_resync project_flock_kandang_id=%d error=%v\n", projectFlockKandangID, err)
failedApply++
} else {
orphanPopulationRows = rowsOrphan
syncedPopulationQtyRows = rowsQty
syncedPopulationUsedRows = rowsUsed
fmt.Printf(
"SYNC project_flock_populations orphan_marked=%d qty_synced=%d used_synced=%d\n",
orphanPopulationRows,
syncedPopulationQtyRows,
syncedPopulationUsedRows,
)
}
fmt.Println() fmt.Println()
fmt.Printf( fmt.Printf(
"Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=%d failed_apply=%d\n", "Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=%d failed_apply=%d population_orphan=%d population_qty_synced=%d population_used_synced=%d\n",
len(targets), len(targets),
skippedPW, skippedPW,
failedResolve, failedResolve,
successApply, successApply,
failedApply, failedApply,
orphanPopulationRows,
syncedPopulationQtyRows,
syncedPopulationUsedRows,
) )
if failedResolve > 0 || failedApply > 0 { if failedResolve > 0 || failedApply > 0 {
os.Exit(1) os.Exit(1)
@@ -379,3 +400,66 @@ func resolveFlagGroupsByProductWarehouse(ctx context.Context, db *gorm.DB, produ
} }
return groups, nil return groups, nil
} }
func resyncProjectFlockPopulation(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (int64, int64, int64, error) {
if projectFlockKandangID == 0 {
return 0, 0, 0, nil
}
orphanResult := db.WithContext(ctx).Exec(`
UPDATE project_flock_populations pfp
SET deleted_at = NOW(),
updated_at = NOW()
FROM project_chickins pc
WHERE pfp.project_chickin_id = pc.id
AND pc.project_flock_kandang_id = ?
AND pc.deleted_at IS NOT NULL
AND pfp.deleted_at IS NULL
`, projectFlockKandangID)
if orphanResult.Error != nil {
return 0, 0, 0, orphanResult.Error
}
qtyResult := db.WithContext(ctx).Exec(`
UPDATE project_flock_populations p
SET total_qty = GREATEST(COALESCE(pc.usage_qty, 0), 0),
updated_at = NOW()
FROM project_chickins pc
WHERE p.project_chickin_id = pc.id
AND pc.project_flock_kandang_id = ?
AND pc.deleted_at IS NULL
AND p.deleted_at IS NULL
`, projectFlockKandangID)
if qtyResult.Error != nil {
return 0, 0, 0, qtyResult.Error
}
usedResult := db.WithContext(ctx).Exec(`
WITH scoped AS (
SELECT pfp.id, pfp.total_qty
FROM project_flock_populations pfp
JOIN project_chickins pc ON pc.id = pfp.project_chickin_id
WHERE pc.project_flock_kandang_id = ?
AND pc.deleted_at IS NULL
AND pfp.deleted_at IS NULL
),
alloc AS (
SELECT sa.stockable_id, SUM(sa.qty) AS used_qty
FROM stock_allocations sa
WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
AND sa.status = 'ACTIVE'
GROUP BY sa.stockable_id
)
UPDATE project_flock_populations p
SET total_used_qty = LEAST(COALESCE(a.used_qty, 0), GREATEST(s.total_qty, 0)),
updated_at = NOW()
FROM scoped s
LEFT JOIN alloc a ON a.stockable_id = s.id
WHERE p.id = s.id
`, projectFlockKandangID)
if usedResult.Error != nil {
return 0, 0, 0, usedResult.Error
}
return orphanResult.RowsAffected, qtyResult.RowsAffected, usedResult.RowsAffected, nil
}
@@ -5,7 +5,6 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rAdjustmentStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories" rAdjustmentStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
@@ -17,7 +16,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
) )
type AdjustmentModule struct{} type AdjustmentModule struct{}
@@ -31,50 +29,14 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
productRepo := rproduct.NewProductRepository(db) productRepo := rproduct.NewProductRepository(db)
adjustmentStockRepo := rAdjustmentStock.NewAdjustmentStockRepository(db) adjustmentStockRepo := rAdjustmentStock.NewAdjustmentStockRepository(db)
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyAdjustmentIn,
Table: "adjustment_stocks",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
})
if err != nil {
panic("Failed to register ADJUSTMENT_IN as Stockable: " + err.Error())
}
err = fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyAdjustmentOut,
Table: "adjustment_stocks",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
})
if err != nil {
panic("Failed to register ADJUSTMENT_OUT as Usable: " + err.Error())
}
adjustmentService := sAdjustment.NewAdjustmentService( adjustmentService := sAdjustment.NewAdjustmentService(
productRepo, productRepo,
stockLogsRepo, stockLogsRepo,
warehouseRepo, warehouseRepo,
productWarehouseRepo, productWarehouseRepo,
adjustmentStockRepo, adjustmentStockRepo,
fifoService,
fifoStockV2Service, fifoStockV2Service,
validate, validate,
projectFlockKandangRepo, projectFlockKandangRepo,
@@ -39,7 +39,6 @@ type adjustmentService struct {
ProductRepo productRepo.ProductRepository ProductRepo productRepo.ProductRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
AdjustmentStockRepository adjustmentStockRepo.AdjustmentStockRepository AdjustmentStockRepository adjustmentStockRepo.AdjustmentStockRepository
FifoSvc common.FifoService
FifoStockV2Svc common.FifoStockV2Service FifoStockV2Svc common.FifoStockV2Service
} }
@@ -55,7 +54,6 @@ func NewAdjustmentService(
warehouseRepo warehouseRepo.WarehouseRepository, warehouseRepo warehouseRepo.WarehouseRepository,
productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository,
adjustmentStockRepo adjustmentStockRepo.AdjustmentStockRepository, adjustmentStockRepo adjustmentStockRepo.AdjustmentStockRepository,
fifoSvc common.FifoService,
fifoStockV2Svc common.FifoStockV2Service, fifoStockV2Svc common.FifoStockV2Service,
validate *validator.Validate, validate *validator.Validate,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
@@ -69,7 +67,6 @@ func NewAdjustmentService(
ProductRepo: productRepo, ProductRepo: productRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
AdjustmentStockRepository: adjustmentStockRepo, AdjustmentStockRepository: adjustmentStockRepo,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc, FifoStockV2Svc: fifoStockV2Svc,
} }
} }
+1 -36
View File
@@ -24,7 +24,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
) )
type TransferModule struct{} type TransferModule struct{}
@@ -43,7 +42,6 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
kandangRepo := rKandang.NewKandangRepository(db) kandangRepo := rKandang.NewKandangRepository(db)
nonstockRepo := rNonstock.NewNonstockRepository(db) nonstockRepo := rNonstock.NewNonstockRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db) documentRepo := commonRepo.NewDocumentRepository(db)
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
expenseRepository := expenseRepo.NewExpenseRepository(db) expenseRepository := expenseRepo.NewExpenseRepository(db)
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db) expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
@@ -69,7 +67,6 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
validate, validate,
) )
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
expenseBridge := sTransfer.NewTransferExpenseBridge( expenseBridge := sTransfer.NewTransferExpenseBridge(
db, db,
@@ -79,39 +76,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
expenseServiceInstance, expenseServiceInstance,
) )
err = fifoService.RegisterStockable(fifo.StockableConfig{ transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoStockV2Service, expenseBridge)
Key: fifo.StockableKeyStockTransferIn,
Table: "stock_transfer_details",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "dest_product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
})
if err != nil {
panic(err)
}
err = fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyStockTransferOut,
Table: "stock_transfer_details",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "source_product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
})
if err != nil {
panic(err)
}
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)
@@ -44,12 +44,11 @@ type transferService struct {
WarehouseRepo warehouseRepo.WarehouseRepository WarehouseRepo warehouseRepo.WarehouseRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
DocumentSvc commonSvc.DocumentService DocumentSvc commonSvc.DocumentService
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service 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, fifoStockV2Svc commonSvc.FifoStockV2Service, 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, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService {
return &transferService{ return &transferService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -63,7 +62,6 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
DocumentSvc: documentSvc, DocumentSvc: documentSvc,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc, FifoStockV2Svc: fifoStockV2Svc,
ExpenseBridge: expenseBridge, ExpenseBridge: expenseBridge,
} }
@@ -257,7 +257,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
for idx, chickin := range newChikins { for idx, chickin := range newChikins {
desiredQty := chickinQtyMap[uint(idx)] desiredQty := chickinQtyMap[uint(idx)]
if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, chickin, desiredQty, actorID); err != nil { if err := s.StageChickinStocks(c.Context(), dbTransaction, chickin, desiredQty, actorID); err != nil {
return err return err
} }
} }
@@ -368,7 +368,7 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
return err return err
} }
if chickin.UsageQty > 0 { if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 {
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil { if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
return err return err
} }
@@ -479,6 +479,20 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
} }
for _, chickin := range chickins { for _, chickin := range chickins {
approvedQty := chickin.UsageQty
if approvedQty <= 0 {
approvedQty = chickin.PendingUsageQty
}
if approvedQty < 0 {
approvedQty = 0
}
if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, &chickin, approvedQty, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to finalize usage qty for chickin %d", chickin.Id))
}
chickin.UsageQty = approvedQty
chickin.PendingUsageQty = 0
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id) populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id))
@@ -510,19 +524,13 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create population for chickin %d", chickin.Id)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create population for chickin %d", chickin.Id))
} }
if err := chickinRepoTx.PatchOne(c.Context(), chickin.Id, map[string]any{
"pending_usage_qty": 0,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset pending usage qty for chickin %d", chickin.Id))
}
if err := s.ReplenishChickinStocks(c.Context(), dbTransaction, &chickin, sourcePW, population, actorID); err != nil { if err := s.ReplenishChickinStocks(c.Context(), dbTransaction, &chickin, sourcePW, population, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock for chickin %d", chickin.Id)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock for chickin %d", chickin.Id))
} }
} }
} }
if action == entity.ApprovalActionRejected { if action == entity.ApprovalActionRejected {
chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID) chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID))
} }
@@ -532,6 +540,17 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
} }
for _, chickin := range chickins { for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id))
}
if populationExists {
continue
}
if chickin.UsageQty <= 0 && chickin.PendingUsageQty <= 0 {
continue
}
if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin, actorID); err != nil { if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err))
@@ -612,6 +631,20 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0) return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0)
} }
func (s *chickinService) StageChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, desiredQty float64, actorID uint) error {
if chickin == nil {
return nil
}
if tx == nil {
return errors.New("transaction is required")
}
if desiredQty < 0 {
return errors.New("desired quantity must be zero or greater")
}
return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, desiredQty)
}
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error { func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
if chickin == nil || targetPW == nil || population == nil { if chickin == nil || targetPW == nil || population == nil {
return nil return nil
@@ -2,7 +2,6 @@ package transfer_layings
import ( import (
"fmt" "fmt"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -14,7 +13,6 @@ import (
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
@@ -34,45 +32,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db) projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rInventory.NewProductWarehouseRepository(db) productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
// daftarin jadi stockable
if err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyTransferToLayingIn,
Table: "laying_transfer_targets",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register transfer to laying stockable workflow: %v", err))
}
}
// daftarin jadi usable
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyTransferToLayingOut,
Table: "laying_transfer_sources",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_usage_qty",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -90,7 +50,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
productWarehouseRepo, productWarehouseRepo,
warehouseRepo, warehouseRepo,
approvalService, approvalService,
fifoService, fifoStockV2Service,
validate, validate,
) )
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -12,12 +12,12 @@ import (
) )
const ( const (
chickinOutFunctionCode = "CHICKIN_OUT" transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN"
chickinUsableLane = "USABLE" transferLayingStockableLane = "STOCKABLE"
chickinSourceTable = "project_chickins" transferLayingSourceTable = "laying_transfer_targets"
) )
func reflowChickinScope( func reflowTransferLayingScope(
ctx context.Context, ctx context.Context,
fifoStockV2Svc commonSvc.FifoStockV2Service, fifoStockV2Svc commonSvc.FifoStockV2Service,
tx *gorm.DB, tx *gorm.DB,
@@ -34,7 +34,7 @@ func reflowChickinScope(
return fmt.Errorf("product warehouse id is required") return fmt.Errorf("product warehouse id is required")
} }
flagGroupCode, err := resolveChickinFlagGroupByProductWarehouse(ctx, tx, productWarehouseID) flagGroupCode, err := resolveTransferLayingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
if err != nil { if err != nil {
return err return err
} }
@@ -51,7 +51,7 @@ func reflowChickinScope(
return err return err
} }
func resolveChickinFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) { func resolveTransferLayingFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
type row struct { type row struct {
FlagGroupCode string `gorm:"column:flag_group_code"` FlagGroupCode string `gorm:"column:flag_group_code"`
} }
@@ -62,9 +62,9 @@ func resolveChickinFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB,
Select("rr.flag_group_code"). Select("rr.flag_group_code").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE"). Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE"). Where("rr.is_active = TRUE").
Where("rr.lane = ?", chickinUsableLane). Where("rr.lane = ?", transferLayingStockableLane).
Where("rr.function_code = ?", chickinOutFunctionCode). Where("rr.function_code = ?", transferLayingInFunctionCode).
Where("rr.source_table = ?", chickinSourceTable). Where("rr.source_table = ?", transferLayingSourceTable).
Where(` Where(`
EXISTS ( EXISTS (
SELECT 1 SELECT 1
@@ -19,7 +19,6 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -51,7 +50,7 @@ type transferLayingService struct {
WarehouseRepo rWarehouse.WarehouseRepository WarehouseRepo rWarehouse.WarehouseRepository
StockLogRepo rStockLogs.StockLogRepository StockLogRepo rStockLogs.StockLogRepository
ApprovalService commonSvc.ApprovalService ApprovalService commonSvc.ApprovalService
FifoSvc commonSvc.FifoService FifoStockV2Svc commonSvc.FifoStockV2Service
} }
func NewTransferLayingService( func NewTransferLayingService(
@@ -64,7 +63,7 @@ func NewTransferLayingService(
productWarehouseRepo rInventory.ProductWarehouseRepository, productWarehouseRepo rInventory.ProductWarehouseRepository,
warehouseRepo rWarehouse.WarehouseRepository, warehouseRepo rWarehouse.WarehouseRepository,
approvalService commonSvc.ApprovalService, approvalService commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service,
validate *validator.Validate, validate *validator.Validate,
) TransferLayingService { ) TransferLayingService {
return &transferLayingService{ return &transferLayingService{
@@ -80,7 +79,7 @@ func NewTransferLayingService(
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()), StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
ApprovalService: approvalService, ApprovalService: approvalService,
FifoSvc: fifoSvc, FifoStockV2Svc: fifoStockV2Svc,
} }
} }
@@ -744,7 +743,6 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction) repoTx := s.Repository.WithTx(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction) stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)
@@ -771,6 +769,9 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
} }
if action == entity.ApprovalActionApproved { if action == entity.ApprovalActionApproved {
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID) sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil { if err != nil {
@@ -792,58 +793,70 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
totalSourceRequested += source.RequestedQty totalSourceRequested += source.RequestedQty
} }
sourceBeforeUsage := make(map[uint]float64, len(sources))
affectedPW := make(map[uint]struct{})
for _, source := range sources { for _, source := range sources {
if source.ProductWarehouseId == nil { if source.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID))
} }
sourceShare := (source.RequestedQty / totalSourceRequested) * totalTargetQty sourceShare := 0.0
if totalSourceRequested > 0 {
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ sourceShare = (source.RequestedQty / totalSourceRequested) * totalTargetQty
UsableKey: fifo.UsableKeyTransferToLayingOut,
UsableID: source.Id,
ProductWarehouseID: *source.ProductWarehouseId,
Quantity: sourceShare,
AllowPending: false,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err))
} }
sourceBeforeUsage[source.Id] = source.UsageQty
if err := sourceRepoTx.PatchOne(c.Context(), source.Id, map[string]interface{}{ if err := sourceRepoTx.PatchOne(c.Context(), source.Id, map[string]interface{}{
"usage_qty": source.UsageQty + consumeResult.UsageQuantity, "usage_qty": sourceShare,
"pending_usage_qty": consumeResult.PendingQuantity, "pending_usage_qty": 0,
}, nil); err != nil { }, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty") return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
} }
targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare) affectedPW[*source.ProductWarehouseId] = struct{}{}
}
for i, target := range targets { for _, target := range targets {
roundedQty := math.Round(targetShares[i]) if target.ProductWarehouseId == nil {
if roundedQty <= 0 { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
continue }
}
mappingAllocation := &entity.StockAllocation{ if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
StockableType: fifo.UsableKeyTransferToLayingOut.String(), "total_qty": target.TotalQty,
StockableId: source.Id, }, nil); err != nil {
UsableType: fifo.StockableKeyTransferToLayingIn.String(), return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
UsableId: target.Id, }
ProductWarehouseId: *source.ProductWarehouseId, affectedPW[*target.ProductWarehouseId] = struct{}{}
Qty: roundedQty, }
Status: entity.StockAllocationStatusActive,
} for pwID := range affectedPW {
if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil { asOfCopy := transfer.TransferDate
return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target") if err := reflowTransferLayingScope(c.Context(), s.FifoStockV2Svc, dbTransaction, pwID, &asOfCopy); err != nil {
} return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow FIFO stock transfer laying (pw=%d): %v", pwID, err))
}
}
for _, source := range sources {
if source.ProductWarehouseId == nil {
continue
}
refreshedSource, err := sourceRepoTx.GetByID(c.Context(), source.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal refresh source transfer setelah reflow")
}
usageDelta := refreshedSource.UsageQty - sourceBeforeUsage[source.Id]
if usageDelta <= 0 {
continue
} }
stockLogDecrease := &entity.StockLog{ stockLogDecrease := &entity.StockLog{
ProductWarehouseId: *source.ProductWarehouseId, ProductWarehouseId: *source.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
Increase: 0, Increase: 0,
Decrease: sourceShare, Decrease: usageDelta,
LoggableType: string(utils.StockLogTypeTransferLaying), LoggableType: string(utils.StockLogTypeTransferLaying),
LoggableId: approvableID, LoggableId: approvableID,
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber), Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
@@ -867,26 +880,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
for _, target := range targets { for _, target := range targets {
if target.ProductWarehouseId == nil { if target.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) continue
}
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
_, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id,
ProductWarehouseID: *target.ProductWarehouseId,
Quantity: target.TotalQty,
Note: &note,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
}
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
"total_qty": target.TotalQty,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
} }
stockLogIncrease := &entity.StockLog{ stockLogIncrease := &entity.StockLog{
@@ -256,35 +256,13 @@ func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
} }
if len(purchase.Items) > 0 { if len(purchase.Items) > 0 {
itemIDs := make([]uint, 0, len(purchase.Items)) lockedIDs, err := s.resolveChickinLockedItemIDs(c.Context(), s.PurchaseRepo.DB(), purchase.Items)
for i := range purchase.Items { if err != nil {
if purchase.Items[i].Id == 0 { return nil, err
continue
}
itemIDs = append(itemIDs, purchase.Items[i].Id)
} }
if len(itemIDs) > 0 { for i := range purchase.Items {
var usedIDs []uint if _, ok := lockedIDs[purchase.Items[i].Id]; ok {
if err := s.PurchaseRepo.DB().WithContext(c.Context()). purchase.Items[i].HasChickin = true
Model(&entity.StockAllocation{}).
Distinct("stockable_id").
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
fifo.StockableKeyPurchaseItems.String(),
itemIDs,
fifo.UsableKeyProjectChickin.String(),
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
).
Pluck("stockable_id", &usedIDs).Error; err != nil {
return nil, err
}
usedSet := make(map[uint]struct{}, len(usedIDs))
for _, id := range usedIDs {
usedSet[id] = struct{}{}
}
for i := range purchase.Items {
if _, ok := usedSet[purchase.Items[i].Id]; ok {
purchase.Items[i].HasChickin = true
}
} }
} }
} }
@@ -532,48 +510,31 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
} }
if action == entity.ApprovalActionApproved { if action == entity.ApprovalActionApproved {
itemIDs := make([]uint, 0, len(purchase.Items))
itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items)) itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items))
for i := range purchase.Items { for i := range purchase.Items {
if purchase.Items[i].Id == 0 { if purchase.Items[i].Id == 0 {
continue continue
} }
itemIDs = append(itemIDs, purchase.Items[i].Id)
itemByID[purchase.Items[i].Id] = purchase.Items[i] itemByID[purchase.Items[i].Id] = purchase.Items[i]
} }
if len(itemIDs) > 0 { lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, s.PurchaseRepo.DB(), purchase.Items)
var usedIDs []uint if err != nil {
if err := s.PurchaseRepo.DB().WithContext(ctx). return nil, err
Model(&entity.StockAllocation{}). }
Distinct("stockable_id"). if len(lockedIDs) > 0 {
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", for _, payload := range req.Items {
fifo.StockableKeyPurchaseItems.String(), if payload.PurchaseItemID == 0 || payload.Qty == nil {
itemIDs, continue
fifo.UsableKeyProjectChickin.String(),
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
).
Pluck("stockable_id", &usedIDs).Error; err != nil {
return nil, err
}
if len(usedIDs) > 0 {
usedSet := make(map[uint]struct{}, len(usedIDs))
for _, id := range usedIDs {
usedSet[id] = struct{}{}
} }
for _, payload := range req.Items { if _, locked := lockedIDs[payload.PurchaseItemID]; !locked {
if payload.PurchaseItemID == 0 || payload.Qty == nil { continue
continue }
} item, ok := itemByID[payload.PurchaseItemID]
if _, used := usedSet[payload.PurchaseItemID]; !used { if !ok {
continue continue
} }
item, ok := itemByID[payload.PurchaseItemID] if *payload.Qty != item.SubQty {
if !ok { return nil, utils.BadRequest("Purchase sudah chickin, qty tidak bisa diubah")
continue
}
if *payload.Qty != item.SubQty {
return nil, utils.BadRequest("Purchase sudah chickin, qty tidak bisa diubah")
}
} }
} }
} }
@@ -827,49 +788,32 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
} }
} }
if action == entity.ApprovalActionApproved { if action == entity.ApprovalActionApproved {
itemIDs := make([]uint, 0, len(purchase.Items))
itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items)) itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items))
for i := range purchase.Items { for i := range purchase.Items {
if purchase.Items[i].Id == 0 { if purchase.Items[i].Id == 0 {
continue continue
} }
itemIDs = append(itemIDs, purchase.Items[i].Id)
itemByID[purchase.Items[i].Id] = purchase.Items[i] itemByID[purchase.Items[i].Id] = purchase.Items[i]
} }
if len(itemIDs) > 0 { lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, s.PurchaseRepo.DB(), purchase.Items)
var usedIDs []uint if err != nil {
if err := s.PurchaseRepo.DB().WithContext(ctx). return nil, err
Model(&entity.StockAllocation{}). }
Distinct("stockable_id"). if len(lockedIDs) > 0 {
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", for _, payload := range req.Items {
fifo.StockableKeyPurchaseItems.String(), if _, used := lockedIDs[payload.PurchaseItemID]; !used {
itemIDs, continue
fifo.UsableKeyProjectChickin.String(),
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
).
Pluck("stockable_id", &usedIDs).Error; err != nil {
return nil, err
}
if len(usedIDs) > 0 {
usedSet := make(map[uint]struct{}, len(usedIDs))
for _, id := range usedIDs {
usedSet[id] = struct{}{}
} }
for _, payload := range req.Items { item, ok := itemByID[payload.PurchaseItemID]
if _, used := usedSet[payload.PurchaseItemID]; !used { if !ok {
continue continue
} }
item, ok := itemByID[payload.PurchaseItemID] receivedQty := item.SubQty
if !ok { if payload.ReceivedQty != nil {
continue receivedQty = *payload.ReceivedQty
} }
receivedQty := item.SubQty if receivedQty != item.TotalQty {
if payload.ReceivedQty != nil { return nil, utils.BadRequest("Purchase sudah chickin, qty penerimaan tidak bisa diubah")
receivedQty = *payload.ReceivedQty
}
if receivedQty != item.TotalQty {
return nil, utils.BadRequest("Purchase sudah chickin, qty penerimaan tidak bisa diubah")
}
} }
} }
} }
@@ -1437,28 +1381,12 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
} }
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
itemIDs := make([]uint, 0, len(itemsToDelete)) lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, tx, itemsToDelete)
for _, item := range itemsToDelete { if err != nil {
if item.Id == 0 { return err
continue
}
itemIDs = append(itemIDs, item.Id)
} }
if len(itemIDs) > 0 { if len(lockedIDs) > 0 {
var count int64 return utils.BadRequest("Purchase already chickin, failed to delete purchase")
if err := tx.Model(&entity.StockAllocation{}).
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
fifo.StockableKeyPurchaseItems.String(),
itemIDs,
fifo.UsableKeyProjectChickin.String(),
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
return utils.BadRequest("Purchase already chickin, failed to delete purchase")
}
} }
if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, note, actorID); err != nil { if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, note, actorID); err != nil {
@@ -1957,6 +1885,67 @@ func (s *purchaseService) applyTravelDocumentURLs(ctx context.Context, purchase
} }
} }
func collectPurchaseItemIDs(items []entity.PurchaseItem) []uint {
itemIDs := make([]uint, 0, len(items))
for i := range items {
if items[i].Id == 0 {
continue
}
itemIDs = append(itemIDs, items[i].Id)
}
return itemIDs
}
func (s *purchaseService) resolveChickinLockedItemIDs(ctx context.Context, db *gorm.DB, items []entity.PurchaseItem) (map[uint]struct{}, error) {
itemIDs := collectPurchaseItemIDs(items)
return s.resolveChickinLockedItemIDsByItemID(ctx, db, itemIDs)
}
func (s *purchaseService) resolveChickinLockedItemIDsByItemID(ctx context.Context, db *gorm.DB, itemIDs []uint) (map[uint]struct{}, error) {
locked := make(map[uint]struct{})
if len(itemIDs) == 0 {
return locked, nil
}
if db == nil {
return nil, errors.New("database is required")
}
var allocationLockedIDs []uint
if err := db.WithContext(ctx).
Model(&entity.StockAllocation{}).
Distinct("stockable_id").
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
fifo.StockableKeyPurchaseItems.String(),
itemIDs,
fifo.UsableKeyProjectChickin.String(),
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
).
Pluck("stockable_id", &allocationLockedIDs).Error; err != nil {
return nil, err
}
for _, itemID := range allocationLockedIDs {
locked[itemID] = struct{}{}
}
var conversionLockedIDs []uint
if err := db.WithContext(ctx).
Table("purchase_items pi").
Distinct("pi.id").
Joins("JOIN project_chickins pc ON pc.product_warehouse_id = pi.product_warehouse_id AND pc.deleted_at IS NULL").
Joins("JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id AND pfp.deleted_at IS NULL").
Where("pi.id IN ?", itemIDs).
Where("pi.project_flock_kandang_id IS NOT NULL").
Where("pc.project_flock_kandang_id = pi.project_flock_kandang_id").
Pluck("pi.id", &conversionLockedIDs).Error; err != nil {
return nil, err
}
for _, itemID := range conversionLockedIDs {
locked[itemID] = struct{}{}
}
return locked, nil
}
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint { func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
seen := make(map[uint]struct{}) seen := make(map[uint]struct{})
ids := make([]uint, 0) ids := make([]uint, 0)