fixing filter pw for transfer, add transfer delete

This commit is contained in:
ragilap
2026-03-13 11:22:10 +07:00
parent 9dcccabc6a
commit 29956528e5
16 changed files with 1122 additions and 219 deletions
@@ -36,7 +36,7 @@ import (
const (
chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena masih memiliki population aktif"
chickinDeleteDownstreamGuardMessage = "Chickin tidak bisa dihapus karena masih dipakai oleh transaksi turunan. Hapus/unexecute Marketing, Recording, dan Transfer to Laying terlebih dahulu."
chickinDeleteDownstreamGuardMessage = "Chickin tidak bisa dihapus karena masih dipakai oleh transaksi turunan. Hapus/unexecute Marketing, Recording, Transfer, Adjustment, dan Transfer to Laying terlebih dahulu."
)
type ChickinService interface {
@@ -264,16 +264,16 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
availableQty = 0
}
if flockCategory == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
transferAvailable, err := s.resolveLayingTransferAvailableQty(c.Context(), nil, req.ProjectFlockKandangId, chickinReq.ProductWarehouseId)
sourceAvailable, err := s.resolveLayingSourceAvailableQty(c.Context(), nil, chickinReq.ProductWarehouseId, &chickinDate)
if err != nil {
s.Log.Errorf("Failed to resolve laying transfer availability for pfk=%d pw=%d: %+v", req.ProjectFlockKandangId, chickinReq.ProductWarehouseId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi stok transfer laying")
}
if transferAvailable < 0 {
transferAvailable = 0
if sourceAvailable < 0 {
sourceAvailable = 0
}
if transferAvailable < availableQty {
availableQty = transferAvailable
if sourceAvailable < availableQty {
availableQty = sourceAvailable
}
}
@@ -554,36 +554,44 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
return nil
}
func (s *chickinService) resolveLayingTransferAvailableQty(ctx context.Context, tx *gorm.DB, targetProjectFlockKandangID, productWarehouseID uint) (float64, error) {
if targetProjectFlockKandangID == 0 || productWarehouseID == 0 {
func (s *chickinService) resolveLayingSourceAvailableQty(ctx context.Context, tx *gorm.DB, productWarehouseID uint, asOf *time.Time) (float64, error) {
if productWarehouseID == 0 || s.FifoStockV2Svc == nil {
return 0, nil
}
db := s.Repository.DB().WithContext(ctx)
db := s.Repository.DB()
if tx != nil {
db = tx.WithContext(ctx)
db = tx
}
var available float64
err := db.Table("laying_transfer_targets ltt").
Select("COALESCE(SUM(GREATEST(0, COALESCE(ltt.total_qty,0) - COALESCE(ltt.total_used,0))), 0) AS available").
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id AND lt.deleted_at IS NULL").
Where("ltt.deleted_at IS NULL").
Where("ltt.target_project_flock_kandang_id = ?", targetProjectFlockKandangID).
Where("ltt.product_warehouse_id = ?", productWarehouseID).
Where("lt.executed_at IS NOT NULL").
Where(`(
SELECT a.action
FROM approvals a
WHERE a.approvable_type = ?
AND a.approvable_id = lt.id
ORDER BY a.id DESC
LIMIT 1
) = ?`, string(utils.ApprovalWorkflowTransferToLaying), entity.ApprovalActionApproved).
Scan(&available).Error
flagGroupCode, err := resolveChickinFlagGroupByProductWarehouse(ctx, db, productWarehouseID)
if err != nil {
return 0, err
}
if strings.TrimSpace(flagGroupCode) == "" {
return 0, nil
}
gatherRows, err := s.FifoStockV2Svc.Gather(ctx, commonSvc.FifoStockV2GatherRequest{
FlagGroupCode: flagGroupCode,
Lane: commonSvc.FifoStockV2Lane("STOCKABLE"),
AllocationPurpose: entity.StockAllocationPurposeConsume,
ProductWarehouseID: productWarehouseID,
AsOf: asOf,
Limit: 10000,
Tx: tx,
})
if err != nil {
return 0, err
}
available := 0.0
for _, row := range gatherRows {
if row.AvailableQuantity <= 0 {
continue
}
available += row.AvailableQuantity
}
return available, nil
}
@@ -650,6 +658,8 @@ func (s *chickinService) ensureNoDownstreamConsumptionForDelete(ctx context.Cont
Where("sa.usable_type IN ?", []string{
fifo.UsableKeyMarketingDelivery.String(),
fifo.UsableKeyRecordingDepletion.String(),
fifo.UsableKeyStockTransferOut.String(),
fifo.UsableKeyAdjustmentOut.String(),
fifo.UsableKeyTransferToLayingOut.String(),
}).
Group("sa.usable_type, sa.usable_id").
@@ -664,6 +674,8 @@ func (s *chickinService) ensureNoDownstreamConsumptionForDelete(ctx context.Cont
marketingIDs := make(map[uint]struct{})
recordingIDs := make(map[uint]struct{})
transferIDs := make(map[uint]struct{})
adjustmentIDs := make(map[uint]struct{})
transferLayingIDs := make(map[uint]struct{})
for _, row := range rows {
@@ -672,18 +684,28 @@ func (s *chickinService) ensureNoDownstreamConsumptionForDelete(ctx context.Cont
marketingIDs[row.UsableID] = struct{}{}
case fifo.UsableKeyRecordingDepletion.String():
recordingIDs[row.UsableID] = struct{}{}
case fifo.UsableKeyStockTransferOut.String():
transferIDs[row.UsableID] = struct{}{}
case fifo.UsableKeyAdjustmentOut.String():
adjustmentIDs[row.UsableID] = struct{}{}
case fifo.UsableKeyTransferToLayingOut.String():
transferLayingIDs[row.UsableID] = struct{}{}
}
}
details := make([]string, 0, 3)
details := make([]string, 0, 5)
if ids := sortedIDs(marketingIDs); len(ids) > 0 {
details = append(details, fmt.Sprintf("Marketing=%s", joinUint(ids)))
}
if ids := sortedIDs(recordingIDs); len(ids) > 0 {
details = append(details, fmt.Sprintf("Recording=%s", joinUint(ids)))
}
if ids := sortedIDs(transferIDs); len(ids) > 0 {
details = append(details, fmt.Sprintf("Transfer=%s", joinUint(ids)))
}
if ids := sortedIDs(adjustmentIDs); len(ids) > 0 {
details = append(details, fmt.Sprintf("Adjustment=%s", joinUint(ids)))
}
if ids := sortedIDs(transferLayingIDs); len(ids) > 0 {
details = append(details, fmt.Sprintf("TransferToLaying=%s", joinUint(ids)))
}
@@ -1292,17 +1314,8 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
}
if !shouldRestoreWarehouseQty {
var affectedTransferTargetIDs []uint
if err := tx.WithContext(ctx).
Model(&entity.StockAllocation{}).
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ? AND stockable_type = ?",
fifo.UsableKeyProjectChickin.String(),
chickin.Id,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
fifo.StockableKeyTransferToLayingIn.String(),
).
Pluck("stockable_id", &affectedTransferTargetIDs).Error; err != nil {
affectedStockables, err := s.listActiveConsumeStockableRefsByUsable(ctx, tx, chickin.Id)
if err != nil {
return err
}
@@ -1325,12 +1338,15 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
return err
}
s.Log.Infof(
"Release chickin stock laying id=%d released_consume_alloc=%d transfer_targets=%d",
"Release chickin stock laying id=%d released_consume_alloc=%d transfer_targets=%d stock_transfer_sources=%d purchase_sources=%d adjustment_sources=%d",
chickin.Id,
releaseResult.RowsAffected,
len(affectedTransferTargetIDs),
len(affectedStockables[fifo.StockableKeyTransferToLayingIn.String()]),
len(affectedStockables[fifo.StockableKeyStockTransferIn.String()]),
len(affectedStockables[fifo.StockableKeyPurchaseItems.String()]),
len(affectedStockables[fifo.StockableKeyAdjustmentIn.String()]),
)
if err := s.resyncTransferTargetUsageFromAllocations(ctx, tx, affectedTransferTargetIDs); err != nil {
if err := s.resyncStockableSourceUsageAfterRelease(ctx, tx, affectedStockables); err != nil {
return err
}
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
@@ -1465,63 +1481,179 @@ func (s *chickinService) logWarehouseQtySnapshot(
)
}
func (s *chickinService) resyncTransferTargetUsageFromAllocations(ctx context.Context, tx *gorm.DB, transferTargetIDs []uint) error {
if tx == nil || len(transferTargetIDs) == 0 {
return nil
func (s *chickinService) listActiveConsumeStockableRefsByUsable(ctx context.Context, tx *gorm.DB, chickinID uint) (map[string][]uint, error) {
result := map[string][]uint{
fifo.StockableKeyTransferToLayingIn.String(): nil,
fifo.StockableKeyStockTransferIn.String(): nil,
fifo.StockableKeyPurchaseItems.String(): nil,
fifo.StockableKeyAdjustmentIn.String(): nil,
}
if tx == nil || chickinID == 0 {
return result, nil
}
unique := make([]uint, 0, len(transferTargetIDs))
seen := make(map[uint]struct{}, len(transferTargetIDs))
for _, id := range transferTargetIDs {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
unique = append(unique, id)
type row struct {
StockableType string `gorm:"column:stockable_type"`
StockableID uint `gorm:"column:stockable_id"`
}
if len(unique) == 0 {
return nil
}
if err := tx.WithContext(ctx).
Model(&entity.LayingTransferTarget{}).
Where("id IN ?", unique).
Update("total_used", 0).Error; err != nil {
return err
}
type usageRow struct {
StockableID uint `gorm:"column:stockable_id"`
Used float64 `gorm:"column:used"`
}
var usageRows []usageRow
var rows []row
if err := tx.WithContext(ctx).
Table("stock_allocations").
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
Where("stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
Where("status = ?", entity.StockAllocationStatusActive).
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
Where("stockable_id IN ?", unique).
Group("stockable_id").
Scan(&usageRows).Error; err != nil {
Select("stockable_type, stockable_id").
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
fifo.UsableKeyProjectChickin.String(),
chickinID,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Where("stockable_type IN ?", []string{
fifo.StockableKeyTransferToLayingIn.String(),
fifo.StockableKeyStockTransferIn.String(),
fifo.StockableKeyPurchaseItems.String(),
fifo.StockableKeyAdjustmentIn.String(),
}).
Group("stockable_type, stockable_id").
Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if row.StockableID == 0 {
continue
}
result[row.StockableType] = append(result[row.StockableType], row.StockableID)
}
for key, ids := range result {
result[key] = uniqueUint(ids)
}
return result, nil
}
func (s *chickinService) resyncStockableSourceUsageAfterRelease(ctx context.Context, tx *gorm.DB, stockableRefs map[string][]uint) error {
if tx == nil || len(stockableRefs) == 0 {
return nil
}
if err := s.resetAndResyncUsedQuantity(
ctx,
tx,
"laying_transfer_targets",
"id",
"total_used",
fifo.StockableKeyTransferToLayingIn.String(),
stockableRefs[fifo.StockableKeyTransferToLayingIn.String()],
); err != nil {
return err
}
for _, row := range usageRows {
if err := tx.WithContext(ctx).
Model(&entity.LayingTransferTarget{}).
Where("id = ?", row.StockableID).
Update("total_used", row.Used).Error; err != nil {
return err
}
if err := s.resetAndResyncUsedQuantity(
ctx,
tx,
"stock_transfer_details",
"id",
"total_used",
fifo.StockableKeyStockTransferIn.String(),
stockableRefs[fifo.StockableKeyStockTransferIn.String()],
); err != nil {
return err
}
if err := s.resetAndResyncUsedQuantity(
ctx,
tx,
"purchase_items",
"id",
"total_used",
fifo.StockableKeyPurchaseItems.String(),
stockableRefs[fifo.StockableKeyPurchaseItems.String()],
); err != nil {
return err
}
if err := s.resetAndResyncUsedQuantity(
ctx,
tx,
"adjustment_stocks",
"id",
"total_used",
fifo.StockableKeyAdjustmentIn.String(),
stockableRefs[fifo.StockableKeyAdjustmentIn.String()],
); err != nil {
return err
}
return nil
}
func (s *chickinService) resetAndResyncUsedQuantity(
ctx context.Context,
tx *gorm.DB,
tableName string,
idColumn string,
usedColumn string,
stockableType string,
ids []uint,
) error {
ids = uniqueUint(ids)
if tx == nil || len(ids) == 0 {
return nil
}
if err := tx.WithContext(ctx).
Table(tableName).
Where(fmt.Sprintf("%s IN ?", idColumn), ids).
Update(usedColumn, 0).Error; err != nil {
return err
}
query := fmt.Sprintf(`
UPDATE %s AS t
SET %s = a.used
FROM (
SELECT stockable_id, COALESCE(SUM(qty), 0) AS used
FROM stock_allocations
WHERE stockable_type = ?
AND status = ?
AND allocation_purpose = ?
AND stockable_id IN ?
GROUP BY stockable_id
) AS a
WHERE t.%s = a.stockable_id
`, tableName, usedColumn, idColumn)
if err := tx.WithContext(ctx).Exec(
query,
stockableType,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
ids,
).Error; err != nil {
return err
}
return nil
}
func uniqueUint(values []uint) []uint {
if len(values) == 0 {
return nil
}
out := make([]uint, 0, len(values))
seen := make(map[uint]struct{}, len(values))
for _, value := range values {
if value == 0 {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}
func normalizeDateOnlyUTC(value time.Time) time.Time {
if value.IsZero() {
return time.Time{}