mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 07:15:43 +00:00
fixing filter pw for transfer, add transfer delete
This commit is contained in:
+162
-112
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -56,12 +57,12 @@ type transferLayingService struct {
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
StockLogRepo rStockLogs.StockLogRepository
|
||||
ApprovalService commonSvc.ApprovalService
|
||||
FifoSvc commonSvc.FifoService
|
||||
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||
}
|
||||
|
||||
const (
|
||||
transferToLayingFlagGroupCode = "AYAM"
|
||||
transferToLayingFlagGroupCode = "AYAM"
|
||||
transferLayingDeleteDownstreamGuardMessage = "Transfer laying tidak dapat dihapus karena stok target transfer sudah dipakai transaksi turunan. Hapus dependensi terkait secara manual terlebih dahulu."
|
||||
)
|
||||
|
||||
func NewTransferLayingService(
|
||||
@@ -74,7 +75,6 @@ func NewTransferLayingService(
|
||||
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
approvalService commonSvc.ApprovalService,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||
validate *validator.Validate,
|
||||
) TransferLayingService {
|
||||
@@ -91,7 +91,6 @@ func NewTransferLayingService(
|
||||
WarehouseRepo: warehouseRepo,
|
||||
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
||||
ApprovalService: approvalService,
|
||||
FifoSvc: fifoSvc,
|
||||
FifoStockV2Svc: fifoStockV2Svc,
|
||||
}
|
||||
}
|
||||
@@ -610,6 +609,9 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if isLegacyTransfer(transfer) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat dihapus", transfer.TransferNumber))
|
||||
}
|
||||
if err := s.ensureNoDownstreamConsumptionForDelete(c.Context(), nil, transfer.TransferNumber, transfer.Targets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||
|
||||
@@ -635,6 +637,16 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
|
||||
// Lock header row to keep delete deterministic after single downstream guard check.
|
||||
if _, err := repoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
}); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
|
||||
}
|
||||
|
||||
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
||||
}
|
||||
@@ -1053,6 +1065,11 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
||||
)
|
||||
}
|
||||
|
||||
type targetReflowKey struct {
|
||||
productWarehouseID uint
|
||||
}
|
||||
targetReflow := make(map[targetReflowKey]struct{})
|
||||
|
||||
for _, target := range targets {
|
||||
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||
@@ -1060,15 +1077,6 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
||||
if target.TotalQty <= 0 {
|
||||
continue
|
||||
}
|
||||
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
|
||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
||||
StockableID: target.Id,
|
||||
ProductWarehouseID: *target.ProductWarehouseId,
|
||||
Quantity: -target.TotalQty,
|
||||
Tx: dbTransaction,
|
||||
}); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback stok target transfer laying: %v", err))
|
||||
}
|
||||
|
||||
stockLogDecrease := &entity.StockLog{
|
||||
ProductWarehouseId: *target.ProductWarehouseId,
|
||||
@@ -1092,6 +1100,20 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
||||
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar target saat unexecute")
|
||||
}
|
||||
|
||||
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]any{
|
||||
"total_qty": 0,
|
||||
"total_used": 0,
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal rollback kuantitas target transfer laying")
|
||||
}
|
||||
targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}
|
||||
}
|
||||
asOf := normalizeDateOnlyUTC(transfer.TransferDate)
|
||||
for key := range targetReflow {
|
||||
if err := reflowTransferLayingScope(c.Context(), s.FifoStockV2Svc, dbTransaction, key.productWarehouseID, &asOf); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 target transfer laying: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
|
||||
@@ -1229,9 +1251,6 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
||||
if s.FifoStockV2Svc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||
}
|
||||
if s.FifoSvc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO service is not available")
|
||||
}
|
||||
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
||||
targetRepoTx := repository.NewLayingTransferTargetRepository(tx)
|
||||
@@ -1327,29 +1346,22 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
||||
}
|
||||
|
||||
type targetReflowKey struct {
|
||||
productWarehouseID uint
|
||||
}
|
||||
targetReflow := make(map[targetReflowKey]struct{})
|
||||
|
||||
for _, target := range targets {
|
||||
if target.ProductWarehouseId == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
||||
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
||||
StockableID: target.Id,
|
||||
ProductWarehouseID: *target.ProductWarehouseId,
|
||||
Quantity: target.TotalQty,
|
||||
Note: ¬e,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
|
||||
}
|
||||
|
||||
if err := targetRepoTx.PatchOne(ctx, target.Id, map[string]any{
|
||||
"total_qty": target.TotalQty,
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
|
||||
}
|
||||
targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}
|
||||
|
||||
stockLogIncrease := &entity.StockLog{
|
||||
ProductWarehouseId: *target.ProductWarehouseId,
|
||||
@@ -1376,6 +1388,11 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
|
||||
}
|
||||
}
|
||||
for key := range targetReflow {
|
||||
if err := reflowTransferLayingScope(ctx, s.FifoStockV2Svc, tx, key.productWarehouseID, &asOf); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow FIFO v2 target transfer laying: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1595,31 +1612,19 @@ func (s *transferLayingService) hasDownstreamRecordingOnTarget(
|
||||
targetProjectFlockKandangID uint,
|
||||
sinceDate time.Time,
|
||||
) (bool, time.Time, error) {
|
||||
if targetProjectFlockKandangID == 0 {
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
|
||||
var earliest entity.Recording
|
||||
query := db.Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", targetProjectFlockKandangID).
|
||||
Where("deleted_at IS NULL")
|
||||
if !sinceDate.IsZero() {
|
||||
query = query.Where("record_datetime >= ?", sinceDate)
|
||||
}
|
||||
|
||||
if err := query.Order("record_datetime ASC").Limit(1).Take(&earliest).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
recordDate, err := targetRepo.GetEarliestRecordingDateByTarget(ctx, targetProjectFlockKandangID, sinceDate)
|
||||
if err != nil {
|
||||
return false, time.Time{}, err
|
||||
}
|
||||
|
||||
return true, normalizeDateOnlyUTC(earliest.RecordDatetime), nil
|
||||
if recordDate == nil {
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
return true, normalizeDateOnlyUTC(*recordDate), nil
|
||||
}
|
||||
|
||||
func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
||||
@@ -1628,81 +1633,45 @@ func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
||||
transferID uint,
|
||||
productWarehouseID uint,
|
||||
) (int64, error) {
|
||||
if transferID == 0 || productWarehouseID == 0 {
|
||||
return 0, nil
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
if tx == nil {
|
||||
return 0, errors.New("transaction is required")
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Where("usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
||||
Where("usable_id = ?", transferID).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
return targetRepo.CountActiveTransferSourceConsumeAllocations(ctx, transferID, productWarehouseID)
|
||||
}
|
||||
|
||||
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
return targetRepo.SyncPopulationUsageByProjectFlockKandang(ctx, projectFlockKandangID)
|
||||
}
|
||||
|
||||
var populationIDs []uint
|
||||
if err := db.Table("project_flock_populations pfp").
|
||||
Select("pfp.id").
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Pluck("pfp.id", &populationIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(populationIDs) == 0 {
|
||||
func sortedIDs(input map[uint]struct{}) []uint {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type usageRow struct {
|
||||
StockableID uint `gorm:"column:stockable_id"`
|
||||
Used float64 `gorm:"column:used"`
|
||||
}
|
||||
var usageRows []usageRow
|
||||
if err := db.Table("stock_allocations").
|
||||
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
||||
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("stockable_id IN ?", populationIDs).
|
||||
Group("stockable_id").
|
||||
Scan(&usageRows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id IN ?", populationIDs).
|
||||
Update("total_used_qty", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range usageRows {
|
||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id = ?", row.StockableID).
|
||||
Update("total_used_qty", row.Used).Error; err != nil {
|
||||
return err
|
||||
out := make([]uint, 0, len(input))
|
||||
for id := range input {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, id)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
return out
|
||||
}
|
||||
|
||||
return nil
|
||||
func joinUint(values []uint) string {
|
||||
if len(values) == 0 {
|
||||
return "-"
|
||||
}
|
||||
parts := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
parts = append(parts, fmt.Sprintf("%d", value))
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||
@@ -1721,3 +1690,84 @@ func isLegacyTransfer(transfer *entity.LayingTransfer) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *transferLayingService) ensureNoDownstreamConsumptionForDelete(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
transferNumber string,
|
||||
targets []entity.LayingTransferTarget,
|
||||
) error {
|
||||
targetIDs := make([]uint, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
if target.Id == 0 {
|
||||
continue
|
||||
}
|
||||
targetIDs = append(targetIDs, target.Id)
|
||||
}
|
||||
if len(targetIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
|
||||
rows, err := targetRepo.GetActiveDownstreamConsumptions(ctx, targetIDs)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to validate downstream consumption for transfer laying %s: %+v", transferNumber, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transaksi turunan transfer laying")
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dependencyMap := make(map[string]map[uint]struct{})
|
||||
for _, row := range rows {
|
||||
label := mapTransferLayingDownstreamUsableLabel(row.UsableType)
|
||||
if _, ok := dependencyMap[label]; !ok {
|
||||
dependencyMap[label] = make(map[uint]struct{})
|
||||
}
|
||||
dependencyMap[label][row.UsableID] = struct{}{}
|
||||
}
|
||||
|
||||
labels := make([]string, 0, len(dependencyMap))
|
||||
for label := range dependencyMap {
|
||||
labels = append(labels, label)
|
||||
}
|
||||
sort.Strings(labels)
|
||||
|
||||
details := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
details = append(details, fmt.Sprintf("%s=%s", label, joinUint(sortedIDs(dependencyMap[label]))))
|
||||
}
|
||||
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"%s Transfer %s. Dependensi aktif: %s.",
|
||||
transferLayingDeleteDownstreamGuardMessage,
|
||||
transferNumber,
|
||||
strings.Join(details, ", "),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func mapTransferLayingDownstreamUsableLabel(usableType string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(usableType)) {
|
||||
case fifo.UsableKeyRecordingStock.String(), fifo.UsableKeyRecordingDepletion.String():
|
||||
return "Recording"
|
||||
case fifo.UsableKeyProjectChickin.String():
|
||||
return "Chickin"
|
||||
case fifo.UsableKeyMarketingDelivery.String():
|
||||
return "Marketing"
|
||||
case fifo.UsableKeyTransferToLayingOut.String():
|
||||
return "TransferToLaying"
|
||||
case fifo.UsableKeyStockTransferOut.String():
|
||||
return "TransferStock"
|
||||
case fifo.UsableKeyAdjustmentOut.String():
|
||||
return "Adjustment"
|
||||
default:
|
||||
return strings.ToUpper(strings.TrimSpace(usableType))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user