mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 23:05:44 +00:00
fixing filter pw for transfer, add transfer delete
This commit is contained in:
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user