mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Fix transfer to laying delete and fix chikin delete with response recording
This commit is contained in:
@@ -45,4 +45,6 @@ type Recording struct {
|
|||||||
StandardFcr *float64 `gorm:"-"`
|
StandardFcr *float64 `gorm:"-"`
|
||||||
PopulationCanChange *bool `gorm:"-"`
|
PopulationCanChange *bool `gorm:"-"`
|
||||||
TransferExecuted *bool `gorm:"-"`
|
TransferExecuted *bool `gorm:"-"`
|
||||||
|
IsTransition *bool `gorm:"-"`
|
||||||
|
IsLaying *bool `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,25 +151,25 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
|
|||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
||||||
// param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
// id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
||||||
// return err
|
return err
|
||||||
// }
|
}
|
||||||
|
|
||||||
// return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
// JSON(response.Common{
|
JSON(response.Common{
|
||||||
// Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
// Status: "success",
|
Status: "success",
|
||||||
// Message: "Delete chickin successfully",
|
Message: "Delete chickin successfully",
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
|
|
||||||
func (u *ChickinController) Approval(c *fiber.Ctx) error {
|
func (u *ChickinController) Approval(c *fiber.Ctx) error {
|
||||||
req := new(validation.Approve)
|
req := new(validation.Approve)
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
|
|||||||
route.Post("/",m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne)
|
route.Post("/",m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne)
|
||||||
route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne)
|
route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne)
|
||||||
// route.Patch("/:id", ctrl.UpdateOne)
|
// route.Patch("/:id", ctrl.UpdateOne)
|
||||||
// route.Delete("/:id", ctrl.DeleteOne)
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Post("/approvals",m.RequirePermissions(m.P_ChickinsApproval), ctrl.Approval)
|
route.Post("/approvals",m.RequirePermissions(m.P_ChickinsApproval), ctrl.Approval)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ import (
|
|||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
const chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena sudah memiliki population. Lakukan rollback/penyesuaian population terlebih dahulu"
|
const (
|
||||||
|
chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena masih memiliki population aktif"
|
||||||
|
chickinDeleteRecordingGuardMessage = "Chickin tidak dapat dihapus karena masih terkait recording. Lakukan rollback/delete recording terlebih dahulu"
|
||||||
|
)
|
||||||
|
|
||||||
type ChickinService interface {
|
type ChickinService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
||||||
@@ -133,16 +136,31 @@ func (s chickinService) ensureNotTransferred(ctx context.Context, projectFlockKa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, projectFlockKandangID)
|
// Restriction transfer->laying untuk chickin hanya berlaku pada kandang kategori growing.
|
||||||
if err != nil {
|
if s.ProjectflockKandangRepo != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
pfk, err := s.ProjectflockKandangRepo.GetByIDLight(ctx, projectFlockKandangID)
|
||||||
return nil
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to resolve project flock kandang %d: %+v", projectFlockKandangID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
}
|
}
|
||||||
|
if err == nil && pfk != nil {
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkExecuted := func(transfer *entity.LayingTransfer) bool {
|
||||||
|
return transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceTransfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, projectFlockKandangID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Failed to resolve transfer laying by source kandang %d: %+v", projectFlockKandangID, err)
|
s.Log.Errorf("Failed to resolve transfer laying by source kandang %d: %+v", projectFlockKandangID, err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
}
|
}
|
||||||
|
if checkExecuted(sourceTransfer) {
|
||||||
if transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero() {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sudah dipindahkan ke laying")
|
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sudah dipindahkan ke laying")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +193,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
|
|
||||||
newChikins := make([]*entity.ProjectChickin, 0)
|
newChikins := make([]*entity.ProjectChickin, 0)
|
||||||
chickinQtyMap := make(map[uint]float64)
|
chickinQtyMap := make(map[uint]float64)
|
||||||
|
flockCategory := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
||||||
|
|
||||||
for idx, chickinReq := range req.ChickinRequests {
|
for idx, chickinReq := range req.ChickinRequests {
|
||||||
|
|
||||||
@@ -194,8 +213,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
if productWarehouse.Product.Id != 0 {
|
if productWarehouse.Product.Id != 0 {
|
||||||
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
if flockCategory != string(utils.ProjectFlockCategoryGrowing) && flockCategory != string(utils.ProjectFlockCategoryLaying) {
|
||||||
if category != string(utils.ProjectFlockCategoryGrowing) && category != string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
return nil, fmt.Errorf("invalid flock category for chickin")
|
return nil, fmt.Errorf("invalid flock category for chickin")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +262,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
if availableQty < 0 {
|
if availableQty < 0 {
|
||||||
availableQty = 0
|
availableQty = 0
|
||||||
}
|
}
|
||||||
|
if flockCategory == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||||
|
transferAvailable, err := s.resolveLayingTransferAvailableQty(c.Context(), nil, req.ProjectFlockKandangId, chickinReq.ProductWarehouseId)
|
||||||
|
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 transferAvailable < availableQty {
|
||||||
|
availableQty = transferAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chickinQtyMap[uint(idx)] = availableQty
|
chickinQtyMap[uint(idx)] = availableQty
|
||||||
}
|
}
|
||||||
@@ -253,6 +284,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
if err := s.ensurePopulationRouteScope(c.Context(), dbTransaction); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
repositoryTx := repository.NewChickinRepository(dbTransaction)
|
repositoryTx := repository.NewChickinRepository(dbTransaction)
|
||||||
existingChikins, err := repositoryTx.GetByProjectFlockKandangIDForUpdate(c.Context(), req.ProjectFlockKandangId)
|
existingChikins, err := repositoryTx.GetByProjectFlockKandangIDForUpdate(c.Context(), req.ProjectFlockKandangId)
|
||||||
@@ -425,13 +459,8 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
if err := s.ensureNotTransferred(c.Context(), chickin.ProjectFlockKandangId); err != nil {
|
if err := s.ensureNotTransferred(c.Context(), chickin.ProjectFlockKandangId); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hasPopulation, err := s.ProjectflockPopulationRepo.ExistsByProjectChickinID(c.Context(), chickin.Id)
|
if err := s.ensureNoExecutedTransferForDelete(c.Context(), chickin.ProjectFlockKandangId); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
s.Log.Errorf("Failed to check population by chickin %d: %+v", chickin.Id, err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi population chickin")
|
|
||||||
}
|
|
||||||
if hasPopulation {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, chickinDeletePopulationGuardMessage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
@@ -440,27 +469,51 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := s.ensurePopulationRouteScope(c.Context(), tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
chickinRepoTx := repository.NewChickinRepository(tx)
|
chickinRepoTx := repository.NewChickinRepository(tx)
|
||||||
if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 {
|
lockedChickin, err := chickinRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
if err := s.ReleaseChickinStocks(c.Context(), tx, chickin, actorID); err != nil {
|
return db.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeAllocBefore, traceAllocBefore, err := s.countActiveChickinAllocations(c.Context(), tx, lockedChickin.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Log.Infof(
|
||||||
|
"Delete chickin start id=%d usage=%.3f pending=%.3f active_consume_alloc=%d active_trace_alloc=%d",
|
||||||
|
lockedChickin.Id,
|
||||||
|
lockedChickin.UsageQty,
|
||||||
|
lockedChickin.PendingUsageQty,
|
||||||
|
consumeAllocBefore,
|
||||||
|
traceAllocBefore,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := s.ensureNoRelatedRecording(c.Context(), tx, lockedChickin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasActiveConsumeAlloc, err := s.hasActiveChickinConsumeAllocations(c.Context(), tx, lockedChickin.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if lockedChickin.UsageQty > 0 || lockedChickin.PendingUsageQty > 0 || hasActiveConsumeAlloc {
|
||||||
|
if err := s.ReleaseChickinStocks(c.Context(), tx, lockedChickin, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
|
||||||
note := "delete chickin rollback"
|
if err := s.rollbackChickinPopulation(c.Context(), tx, lockedChickin.Id); err != nil {
|
||||||
if err := tx.WithContext(c.Context()).
|
return err
|
||||||
Model(&entity.StockAllocation{}).
|
|
||||||
Where("usable_type = ? AND usable_id = ? AND status = ?",
|
|
||||||
fifo.UsableKeyProjectChickin.String(),
|
|
||||||
chickin.Id,
|
|
||||||
entity.StockAllocationStatusActive,
|
|
||||||
).
|
|
||||||
Updates(map[string]any{
|
|
||||||
"status": entity.StockAllocationStatusReleased,
|
|
||||||
"released_at": now,
|
|
||||||
"note": note,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal release alokasi FIFO chickin")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
||||||
@@ -473,10 +526,29 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.syncChickinTraceForProductWarehouse(c.Context(), tx, chickin.ProductWarehouseId); err != nil {
|
reflowAsOf := normalizeDateOnlyUTC(lockedChickin.ChickInDate)
|
||||||
|
if reflowAsOf.IsZero() {
|
||||||
|
reflowAsOf = time.Now().UTC()
|
||||||
|
}
|
||||||
|
if err := s.reflowWarehouseAfterChickinDelete(c.Context(), tx, lockedChickin.ProductWarehouseId, reflowAsOf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.syncChickinTraceForProductWarehouse(c.Context(), tx, lockedChickin.ProductWarehouseId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeAllocAfter, traceAllocAfter, err := s.countActiveChickinAllocations(c.Context(), tx, lockedChickin.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Log.Infof(
|
||||||
|
"Delete chickin complete id=%d active_consume_alloc=%d active_trace_alloc=%d",
|
||||||
|
lockedChickin.Id,
|
||||||
|
consumeAllocAfter,
|
||||||
|
traceAllocAfter,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -489,6 +561,325 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s chickinService) ensureNoExecutedTransferForDelete(ctx context.Context, projectFlockKandangID uint) error {
|
||||||
|
if projectFlockKandangID == 0 || s.TransferLayingRepo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete guard by executed transfer hanya untuk kandang kategori growing.
|
||||||
|
if s.ProjectflockKandangRepo != nil {
|
||||||
|
pfk, err := s.ProjectflockKandangRepo.GetByIDLight(ctx, projectFlockKandangID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to resolve project flock kandang %d for delete guard: %+v", projectFlockKandangID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
|
}
|
||||||
|
if err == nil && pfk != nil {
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isExecuted := func(transfer *entity.LayingTransfer) bool {
|
||||||
|
return transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceTransfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, projectFlockKandangID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to resolve transfer laying by source kandang %d for delete guard: %+v", projectFlockKandangID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
|
}
|
||||||
|
targetTransfer, err := s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, projectFlockKandangID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to resolve transfer laying by target kandang %d for delete guard: %+v", projectFlockKandangID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExecuted(sourceTransfer) || isExecuted(targetTransfer) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
"Chickin tidak dapat dihapus karena transfer laying sudah dieksekusi. Lakukan unexecute transfer terlebih dahulu",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) resolveLayingTransferAvailableQty(ctx context.Context, tx *gorm.DB, targetProjectFlockKandangID, productWarehouseID uint) (float64, error) {
|
||||||
|
if targetProjectFlockKandangID == 0 || productWarehouseID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
if tx != nil {
|
||||||
|
db = tx.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return available, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s chickinService) ensurePopulationRouteScope(ctx context.Context, tx *gorm.DB) error {
|
||||||
|
db := tx
|
||||||
|
if db == nil {
|
||||||
|
db = s.Repository.DB()
|
||||||
|
}
|
||||||
|
if db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
result := db.WithContext(ctx).
|
||||||
|
Table("fifo_stock_v2_route_rules").
|
||||||
|
Where("is_active = TRUE").
|
||||||
|
Where("lane = ?", "STOCKABLE").
|
||||||
|
Where("function_code = ?", "POPULATION_IN").
|
||||||
|
Where("source_table = ?", "project_flock_populations").
|
||||||
|
Where("(scope_sql IS NULL OR TRIM(scope_sql) = '')").
|
||||||
|
Updates(map[string]any{
|
||||||
|
"scope_sql": "deleted_at IS NULL",
|
||||||
|
"updated_at": now,
|
||||||
|
})
|
||||||
|
if result.Error != nil {
|
||||||
|
s.Log.Errorf("Failed to enforce FIFO population route scope: %+v", result.Error)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi konfigurasi FIFO chickin")
|
||||||
|
}
|
||||||
|
if result.RowsAffected > 0 {
|
||||||
|
s.Log.Warnf(
|
||||||
|
"Auto-fixed FIFO population route scope for chickin flow (rows=%d)",
|
||||||
|
result.RowsAffected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) ensureNoRelatedRecording(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin) error {
|
||||||
|
if chickin == nil || chickin.ProjectFlockKandangId == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
if tx != nil {
|
||||||
|
db = tx.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDateFloor := normalizeDateOnlyUTC(chickin.ChickInDate)
|
||||||
|
var earliest entity.Recording
|
||||||
|
query := db.Model(&entity.Recording{}).
|
||||||
|
Where("project_flock_kandangs_id = ?", chickin.ProjectFlockKandangId).
|
||||||
|
Where("deleted_at IS NULL")
|
||||||
|
if !recordDateFloor.IsZero() {
|
||||||
|
query = query.Where("record_datetime >= ?", recordDateFloor)
|
||||||
|
}
|
||||||
|
if err := query.Order("record_datetime ASC, id ASC").Take(&earliest).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to validate related recordings for chickin %d: %+v", chickin.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi recording terkait chickin")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s (recording tanggal %s)",
|
||||||
|
chickinDeleteRecordingGuardMessage,
|
||||||
|
normalizeDateOnlyUTC(earliest.RecordDatetime).Format("2006-01-02"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) hasActiveChickinConsumeAllocations(ctx context.Context, tx *gorm.DB, chickinID uint) (bool, error) {
|
||||||
|
if tx == nil || chickinID == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
|
chickinID,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) countActiveChickinAllocations(ctx context.Context, tx *gorm.DB, chickinID uint) (consume int64, trace int64, err error) {
|
||||||
|
if tx == nil || chickinID == 0 {
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
baseQuery := tx.WithContext(ctx).Model(&entity.StockAllocation{}).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ?",
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
|
chickinID,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
)
|
||||||
|
if err := baseQuery.Session(&gorm.Session{}).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
|
Count(&consume).Error; err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
if err := baseQuery.Session(&gorm.Session{}).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeTraceChickin).
|
||||||
|
Count(&trace).Error; err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return consume, trace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) reflowWarehouseAfterChickinDelete(ctx context.Context, tx *gorm.DB, productWarehouseID uint, asOf time.Time) error {
|
||||||
|
if tx == nil || productWarehouseID == 0 || s.FifoStockV2Svc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := s.ensurePopulationRouteScope(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
qtyBefore, hasQtyBefore := s.tryLoadWarehouseQty(ctx, tx, productWarehouseID)
|
||||||
|
|
||||||
|
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to resolve flag group for delete chickin reflow pw=%d: %+v", productWarehouseID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi stok setelah delete chickin")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
ProductWarehouseID: productWarehouseID,
|
||||||
|
AsOf: &asOf,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow warehouse after delete chickin pw=%d: %+v", productWarehouseID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi stok setelah delete chickin")
|
||||||
|
}
|
||||||
|
|
||||||
|
processedUsables := 0
|
||||||
|
rollbackQty := 0.0
|
||||||
|
allocateQty := 0.0
|
||||||
|
if result != nil {
|
||||||
|
processedUsables = result.ProcessedUsables
|
||||||
|
rollbackQty = result.Rollback.ReleasedQty
|
||||||
|
allocateQty = result.Allocate.AllocatedQty
|
||||||
|
}
|
||||||
|
s.Log.Infof(
|
||||||
|
"Delete chickin warehouse reflow pw=%d processed_usables=%d rollback_qty=%.3f allocate_qty=%.3f",
|
||||||
|
productWarehouseID,
|
||||||
|
processedUsables,
|
||||||
|
rollbackQty,
|
||||||
|
allocateQty,
|
||||||
|
)
|
||||||
|
s.logWarehouseQtySnapshot(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
productWarehouseID,
|
||||||
|
"reflow_after_delete_chickin",
|
||||||
|
0,
|
||||||
|
hasQtyBefore,
|
||||||
|
qtyBefore,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) rollbackChickinPopulation(ctx context.Context, tx *gorm.DB, chickinID uint) error {
|
||||||
|
if tx == nil || chickinID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var populationIDs []uint
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
|
Where("project_chickin_id = ?", chickinID).
|
||||||
|
Pluck("id", &populationIDs).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to list population ids for chickin %d: %+v", chickinID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil population chickin")
|
||||||
|
}
|
||||||
|
if len(populationIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
note := "delete chickin rollback population"
|
||||||
|
releaseResult := tx.WithContext(ctx).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("stockable_type = ? AND stockable_id IN ? AND status = ?",
|
||||||
|
fifo.StockableKeyProjectFlockPopulation.String(),
|
||||||
|
populationIDs,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
).
|
||||||
|
Where("NOT (usable_type = ? AND usable_id = ?)",
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
|
chickinID,
|
||||||
|
).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": entity.StockAllocationStatusReleased,
|
||||||
|
"released_at": now,
|
||||||
|
"note": note,
|
||||||
|
})
|
||||||
|
if releaseResult.Error != nil {
|
||||||
|
err := releaseResult.Error
|
||||||
|
s.Log.Errorf("Failed to release population allocation for chickin %d: %+v", chickinID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal release alokasi population chickin")
|
||||||
|
}
|
||||||
|
if releaseResult.RowsAffected > 0 {
|
||||||
|
s.Log.Infof(
|
||||||
|
"Delete chickin rollback population id=%d released_population_alloc=%d",
|
||||||
|
chickinID,
|
||||||
|
releaseResult.RowsAffected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteResult := tx.WithContext(ctx).
|
||||||
|
Where("id IN ?", populationIDs).
|
||||||
|
Delete(&entity.ProjectFlockPopulation{})
|
||||||
|
if deleteResult.Error != nil {
|
||||||
|
err := deleteResult.Error
|
||||||
|
s.Log.Errorf("Failed to delete populations for chickin %d: %+v", chickinID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus population chickin")
|
||||||
|
}
|
||||||
|
if deleteResult.RowsAffected > 0 {
|
||||||
|
s.Log.Infof(
|
||||||
|
"Delete chickin rollback population id=%d deleted_population=%d",
|
||||||
|
chickinID,
|
||||||
|
deleteResult.RowsAffected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func isForeignKeyViolation(err error) bool {
|
func isForeignKeyViolation(err error) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
@@ -561,6 +952,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
if err := s.ensurePopulationRouteScope(c.Context(), dbTransaction); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
@@ -818,6 +1212,9 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
|
|||||||
if asOf.IsZero() {
|
if asOf.IsZero() {
|
||||||
asOf = chickin.CreatedAt
|
asOf = chickin.CreatedAt
|
||||||
}
|
}
|
||||||
|
if err := s.ensurePopulationRouteScope(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return reflowChickinScope(ctx, s.FifoStockV2Svc, tx, targetPW.Id, &asOf)
|
return reflowChickinScope(ctx, s.FifoStockV2Svc, tx, targetPW.Id, &asOf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,14 +1225,298 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return errors.New("transaction is required")
|
return errors.New("transaction is required")
|
||||||
}
|
}
|
||||||
|
if chickin.ProductWarehouseId == 0 {
|
||||||
|
return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0)
|
||||||
|
}
|
||||||
|
qtyBefore, hasQtyBefore := s.tryLoadWarehouseQty(ctx, tx, chickin.ProductWarehouseId)
|
||||||
|
|
||||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
var activeConsumeCount int64
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
|
chickin.Id,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
).
|
||||||
|
Count(&activeConsumeCount).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if activeConsumeCount == 0 || s.FifoStockV2Svc == nil {
|
||||||
|
s.Log.Infof(
|
||||||
|
"Release chickin stock fallback id=%d active_consume_alloc=%d fifo_available=%t",
|
||||||
|
chickin.Id,
|
||||||
|
activeConsumeCount,
|
||||||
|
s.FifoStockV2Svc != nil,
|
||||||
|
)
|
||||||
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logWarehouseQtySnapshot(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
chickin.ProductWarehouseId,
|
||||||
|
"release_chickin_fallback_no_active_alloc",
|
||||||
|
chickin.Id,
|
||||||
|
hasQtyBefore,
|
||||||
|
qtyBefore,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRestoreWarehouseQty := true
|
||||||
|
if s.ProjectflockKandangRepo != nil && chickin.ProjectFlockKandangId != 0 {
|
||||||
|
pfk, err := s.ProjectflockKandangRepo.GetByIDLight(ctx, chickin.ProjectFlockKandangId)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == nil && pfk != nil {
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
if category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||||
|
shouldRestoreWarehouseQty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
releaseResult := tx.WithContext(ctx).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
|
chickin.Id,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": entity.StockAllocationStatusReleased,
|
||||||
|
"released_at": now,
|
||||||
|
"note": "chickin rollback without qty adjust",
|
||||||
|
})
|
||||||
|
if releaseResult.Error != nil {
|
||||||
|
err := releaseResult.Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Log.Infof(
|
||||||
|
"Release chickin stock laying id=%d released_consume_alloc=%d transfer_targets=%d",
|
||||||
|
chickin.Id,
|
||||||
|
releaseResult.RowsAffected,
|
||||||
|
len(affectedTransferTargetIDs),
|
||||||
|
)
|
||||||
|
if err := s.resyncTransferTargetUsageFromAllocations(ctx, tx, affectedTransferTargetIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logWarehouseQtySnapshot(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
chickin.ProductWarehouseId,
|
||||||
|
"release_chickin_laying_no_restore",
|
||||||
|
chickin.Id,
|
||||||
|
hasQtyBefore,
|
||||||
|
qtyBefore,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rollbackResult, err := s.FifoStockV2Svc.Rollback(ctx, commonSvc.FifoStockV2RollbackRequest{
|
||||||
|
ProductWarehouseID: chickin.ProductWarehouseId,
|
||||||
|
Usable: commonSvc.FifoStockV2Ref{
|
||||||
|
ID: chickin.Id,
|
||||||
|
LegacyTypeKey: fifo.UsableKeyProjectChickin.String(),
|
||||||
|
FunctionCode: "CHICKIN_OUT",
|
||||||
|
},
|
||||||
|
Reason: "delete/reject chickin rollback",
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to rollback FIFO v2 for chickin %d: %+v", chickin.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
releasedQty := 0.0
|
||||||
|
detailCount := 0
|
||||||
|
if rollbackResult != nil {
|
||||||
|
releasedQty = rollbackResult.ReleasedQty
|
||||||
|
detailCount = len(rollbackResult.Details)
|
||||||
|
}
|
||||||
|
s.Log.Infof(
|
||||||
|
"Release chickin stock fifo rollback id=%d released_qty=%.3f detail_count=%d",
|
||||||
|
chickin.Id,
|
||||||
|
releasedQty,
|
||||||
|
detailCount,
|
||||||
|
)
|
||||||
|
s.logWarehouseQtySnapshot(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
chickin.ProductWarehouseId,
|
||||||
|
"release_chickin_fifo_rollback",
|
||||||
|
chickin.Id,
|
||||||
|
hasQtyBefore,
|
||||||
|
qtyBefore,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) tryLoadWarehouseQty(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (float64, bool) {
|
||||||
|
if tx == nil || productWarehouseID == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
Qty float64 `gorm:"column:qty"`
|
||||||
|
}
|
||||||
|
out := row{}
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Table("product_warehouses").
|
||||||
|
Select("COALESCE(qty, 0) AS qty").
|
||||||
|
Where("id = ?", productWarehouseID).
|
||||||
|
Take(&out).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
errText := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||||
|
if strings.Contains(errText, "no such column") && strings.Contains(errText, "qty") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
if strings.Contains(errText, "column") && strings.Contains(errText, "qty") && strings.Contains(errText, "does not exist") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
s.Log.Warnf("Failed to load warehouse qty snapshot pw=%d: %+v", productWarehouseID, err)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Qty, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) logWarehouseQtySnapshot(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
stage string,
|
||||||
|
chickinID uint,
|
||||||
|
hasBefore bool,
|
||||||
|
beforeQty float64,
|
||||||
|
) {
|
||||||
|
afterQty, hasAfter := s.tryLoadWarehouseQty(ctx, tx, productWarehouseID)
|
||||||
|
if !hasBefore && !hasAfter {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasBefore && hasAfter {
|
||||||
|
s.Log.Infof(
|
||||||
|
"Warehouse qty snapshot stage=%s chickin_id=%d pw=%d before=%.3f after=%.3f delta=%.3f",
|
||||||
|
stage,
|
||||||
|
chickinID,
|
||||||
|
productWarehouseID,
|
||||||
|
beforeQty,
|
||||||
|
afterQty,
|
||||||
|
afterQty-beforeQty,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAfter {
|
||||||
|
s.Log.Infof(
|
||||||
|
"Warehouse qty snapshot stage=%s chickin_id=%d pw=%d after=%.3f",
|
||||||
|
stage,
|
||||||
|
chickinID,
|
||||||
|
productWarehouseID,
|
||||||
|
afterQty,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Log.Infof(
|
||||||
|
"Warehouse qty snapshot stage=%s chickin_id=%d pw=%d before=%.3f",
|
||||||
|
stage,
|
||||||
|
chickinID,
|
||||||
|
productWarehouseID,
|
||||||
|
beforeQty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) resyncTransferTargetUsageFromAllocations(ctx context.Context, tx *gorm.DB, transferTargetIDs []uint) error {
|
||||||
|
if tx == nil || len(transferTargetIDs) == 0 {
|
||||||
|
return 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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||||
|
if value.IsZero() {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) error {
|
func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) error {
|
||||||
if productWarehouseID == 0 {
|
if productWarehouseID == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -849,14 +1530,9 @@ func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context
|
|||||||
return s.syncChickinTraceForProductWarehouse(ctx, innerTx, productWarehouseID)
|
return s.syncChickinTraceForProductWarehouse(ctx, innerTx, productWarehouseID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if err := s.ensurePopulationRouteScope(ctx, tx); err != nil {
|
||||||
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(flagGroupCode) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if err := tx.WithContext(ctx).
|
if err := tx.WithContext(ctx).
|
||||||
@@ -874,6 +1550,14 @@ func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type chickinTraceRow struct {
|
type chickinTraceRow struct {
|
||||||
ID uint `gorm:"column:id"`
|
ID uint `gorm:"column:id"`
|
||||||
UsageQty float64 `gorm:"column:usage_qty"`
|
UsageQty float64 `gorm:"column:usage_qty"`
|
||||||
|
|||||||
@@ -300,6 +300,12 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
|||||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse)
|
mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse)
|
||||||
dtoResult.Warehouse = &mapped
|
dtoResult.Warehouse = &mapped
|
||||||
}
|
}
|
||||||
|
if isTransition, isLaying, serr := u.ProjectflockService.GetProjectFlockKandangTransferState(c, result.Id); serr != nil {
|
||||||
|
return serr
|
||||||
|
} else {
|
||||||
|
dtoResult.IsTransition = isTransition
|
||||||
|
dtoResult.IsLaying = isLaying
|
||||||
|
}
|
||||||
if withPopulation {
|
if withPopulation {
|
||||||
population := dtoResult.AvailableQuantity
|
population := dtoResult.AvailableQuantity
|
||||||
dtoResult.Population = &population
|
dtoResult.Population = &population
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type ProjectFlockKandangDTO struct {
|
|||||||
AvailableQuantity float64 `json:"available_quantity"`
|
AvailableQuantity float64 `json:"available_quantity"`
|
||||||
Population *float64 `json:"population,omitempty"`
|
Population *float64 `json:"population,omitempty"`
|
||||||
ChickInDate *time.Time `json:"chick_in_date,omitempty"`
|
ChickInDate *time.Time `json:"chick_in_date,omitempty"`
|
||||||
|
IsTransition bool `json:"is_transition"`
|
||||||
|
IsLaying bool `json:"is_laying"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
|
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
|
|
||||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -35,6 +36,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||||
projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db)
|
projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db)
|
||||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||||
|
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
||||||
@@ -46,7 +48,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, approvalService, validate)
|
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, transferLayingRepo, approvalService, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectflockRoutes(router, userService, projectflockService)
|
ProjectflockRoutes(router, userService, projectflockService)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||||
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
|
transferLayingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
||||||
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -44,6 +45,7 @@ type ProjectflockService interface {
|
|||||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||||
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
|
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
|
||||||
GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error)
|
GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error)
|
||||||
|
GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error)
|
||||||
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||||
@@ -64,6 +66,7 @@ type projectflockService struct {
|
|||||||
PivotRepo repository.ProjectFlockKandangRepository
|
PivotRepo repository.ProjectFlockKandangRepository
|
||||||
PopulationRepo repository.ProjectFlockPopulationRepository
|
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||||
RecordingRepo recordingRepo.RecordingRepository
|
RecordingRepo recordingRepo.RecordingRepository
|
||||||
|
TransferLayingRepo transferLayingRepo.TransferLayingRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
}
|
}
|
||||||
@@ -85,6 +88,7 @@ func NewProjectflockService(
|
|||||||
nonstockRepo nonstockRepository.NonstockRepository,
|
nonstockRepo nonstockRepository.NonstockRepository,
|
||||||
populationRepo repository.ProjectFlockPopulationRepository,
|
populationRepo repository.ProjectFlockPopulationRepository,
|
||||||
recordingRepo recordingRepo.RecordingRepository,
|
recordingRepo recordingRepo.RecordingRepository,
|
||||||
|
transferLayingRepo transferLayingRepo.TransferLayingRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
|
|
||||||
@@ -102,6 +106,7 @@ func NewProjectflockService(
|
|||||||
PivotRepo: pivotRepo,
|
PivotRepo: pivotRepo,
|
||||||
PopulationRepo: populationRepo,
|
PopulationRepo: populationRepo,
|
||||||
RecordingRepo: recordingRepo,
|
RecordingRepo: recordingRepo,
|
||||||
|
TransferLayingRepo: transferLayingRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
||||||
}
|
}
|
||||||
@@ -538,6 +543,63 @@ func (s projectflockService) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, p
|
|||||||
return earliest, nil
|
return earliest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) {
|
||||||
|
if projectFlockKandangID == 0 || s.TransferLayingRepo == nil || s.PivotRepo == nil {
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pfk, err := s.PivotRepo.GetByIDLight(ctx.Context(), projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve project flock kandang %d for transfer state: %+v", projectFlockKandangID, err)
|
||||||
|
return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state")
|
||||||
|
}
|
||||||
|
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
var transfer *entity.LayingTransfer
|
||||||
|
switch category {
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx.Context(), projectFlockKandangID)
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx.Context(), projectFlockKandangID)
|
||||||
|
default:
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve transfer state for project flock kandang %d: %+v", projectFlockKandangID, err)
|
||||||
|
return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state")
|
||||||
|
}
|
||||||
|
if transfer == nil {
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalMoveDate := normalizeDateOnlyUTC(transfer.TransferDate)
|
||||||
|
if physicalMoveDate.IsZero() {
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
economicCutoffDate := physicalMoveDate
|
||||||
|
if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() {
|
||||||
|
economicCutoffDate = normalizeDateOnlyUTC(*transfer.EconomicCutoffDate)
|
||||||
|
} else if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
|
||||||
|
economicCutoffDate = normalizeDateOnlyUTC(*transfer.EffectiveMoveDate)
|
||||||
|
}
|
||||||
|
if economicCutoffDate.Before(physicalMoveDate) {
|
||||||
|
economicCutoffDate = physicalMoveDate
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceDate := normalizeDateOnlyUTC(time.Now().UTC())
|
||||||
|
isTransition := !referenceDate.Before(physicalMoveDate) && referenceDate.Before(economicCutoffDate)
|
||||||
|
isLaying := !referenceDate.Before(economicCutoffDate)
|
||||||
|
|
||||||
|
return isTransition, isLaying, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
||||||
idStr = strings.TrimSpace(idStr)
|
idStr = strings.TrimSpace(idStr)
|
||||||
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
||||||
@@ -579,6 +641,10 @@ func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idSt
|
|||||||
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
|
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||||
|
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
|
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
|
||||||
if s.PopulationRepo == nil {
|
if s.PopulationRepo == nil {
|
||||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ type RecordingRelationDTO struct {
|
|||||||
EggWeight float64 `json:"egg_weight"`
|
EggWeight float64 `json:"egg_weight"`
|
||||||
PopulationCanChange bool `json:"population_can_change"`
|
PopulationCanChange bool `json:"population_can_change"`
|
||||||
TransferExecuted *bool `json:"transfer_executed,omitempty"`
|
TransferExecuted *bool `json:"transfer_executed,omitempty"`
|
||||||
|
IsTransition bool `json:"is_transition"`
|
||||||
|
IsLaying bool `json:"is_laying"`
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +249,8 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
EggWeight: floatValue(e.EggWeight),
|
EggWeight: floatValue(e.EggWeight),
|
||||||
PopulationCanChange: boolValueDefault(e.PopulationCanChange, true),
|
PopulationCanChange: boolValueDefault(e.PopulationCanChange, true),
|
||||||
TransferExecuted: e.TransferExecuted,
|
TransferExecuted: e.TransferExecuted,
|
||||||
|
IsTransition: boolValueDefault(e.IsTransition, false),
|
||||||
|
IsLaying: boolValueDefault(e.IsLaying, false),
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
nonstockRepo,
|
nonstockRepo,
|
||||||
projectFlockPopulationRepo,
|
projectFlockPopulationRepo,
|
||||||
recordingRepo,
|
recordingRepo,
|
||||||
|
transferLayingRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -185,12 +185,14 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
||||||
recordings[i].DepletionRate = &rate
|
recordings[i].DepletionRate = &rate
|
||||||
|
|
||||||
populationCanChange, transferExecuted, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), &recordings[i])
|
populationCanChange, transferExecuted, isTransition, isLaying, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), &recordings[i])
|
||||||
if stateErr != nil {
|
if stateErr != nil {
|
||||||
return nil, 0, stateErr
|
return nil, 0, stateErr
|
||||||
}
|
}
|
||||||
recordings[i].PopulationCanChange = boolPtr(populationCanChange)
|
recordings[i].PopulationCanChange = boolPtr(populationCanChange)
|
||||||
recordings[i].TransferExecuted = boolPtr(transferExecuted)
|
recordings[i].TransferExecuted = boolPtr(transferExecuted)
|
||||||
|
recordings[i].IsTransition = boolPtr(isTransition)
|
||||||
|
recordings[i].IsLaying = boolPtr(isLaying)
|
||||||
}
|
}
|
||||||
return recordings, total, nil
|
return recordings, total, nil
|
||||||
}
|
}
|
||||||
@@ -251,12 +253,14 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
|
|||||||
recording.DepletionRate = &rate
|
recording.DepletionRate = &rate
|
||||||
}
|
}
|
||||||
|
|
||||||
populationCanChange, transferExecuted, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
|
populationCanChange, transferExecuted, isTransition, isLaying, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
|
||||||
if stateErr != nil {
|
if stateErr != nil {
|
||||||
return nil, stateErr
|
return nil, stateErr
|
||||||
}
|
}
|
||||||
recording.PopulationCanChange = boolPtr(populationCanChange)
|
recording.PopulationCanChange = boolPtr(populationCanChange)
|
||||||
recording.TransferExecuted = boolPtr(transferExecuted)
|
recording.TransferExecuted = boolPtr(transferExecuted)
|
||||||
|
recording.IsTransition = boolPtr(isTransition)
|
||||||
|
recording.IsLaying = boolPtr(isLaying)
|
||||||
|
|
||||||
return recording, nil
|
return recording, nil
|
||||||
}
|
}
|
||||||
@@ -990,46 +994,58 @@ func (s *recordingService) resolveRecordingCategory(ctx context.Context, recordi
|
|||||||
return strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category)), nil
|
return strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) evaluatePopulationMutationState(ctx context.Context, recording *entity.Recording) (bool, bool, *entity.LayingTransfer, time.Time, error) {
|
func (s *recordingService) evaluatePopulationMutationState(ctx context.Context, recording *entity.Recording) (bool, bool, bool, bool, *entity.LayingTransfer, time.Time, error) {
|
||||||
if recording == nil || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
|
if recording == nil || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
|
||||||
return true, false, nil, time.Time{}, nil
|
return true, false, false, false, nil, time.Time{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
category, err := s.resolveRecordingCategory(ctx, recording)
|
category, err := s.resolveRecordingCategory(ctx, recording)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to resolve recording category for population mutation check (recording=%d): %+v", recording.Id, err)
|
s.Log.Errorf("Failed to resolve recording category for population mutation check (recording=%d): %+v", recording.Id, err)
|
||||||
return true, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||||
}
|
|
||||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
|
||||||
return true, false, nil, time.Time{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
|
var transfer *entity.LayingTransfer
|
||||||
|
switch category {
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, recording.ProjectFlockKandangId)
|
||||||
|
default:
|
||||||
|
return true, false, false, false, nil, time.Time{}, nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return true, false, nil, time.Time{}, nil
|
return true, false, false, false, nil, time.Time{}, nil
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to resolve approved transfer by source kandang for recording %d: %+v", recording.Id, err)
|
s.Log.Errorf("Failed to resolve approved transfer for recording %d: %+v", recording.Id, err)
|
||||||
return true, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||||
}
|
}
|
||||||
if transfer == nil {
|
if transfer == nil {
|
||||||
return true, false, nil, time.Time{}, nil
|
return true, false, false, false, nil, time.Time{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
transferDate := transferPhysicalMoveDate(transfer)
|
transferDate := transferPhysicalMoveDate(transfer)
|
||||||
if transferDate.IsZero() {
|
if transferDate.IsZero() {
|
||||||
return true, false, transfer, transferDate, nil
|
return true, false, false, false, transfer, transferDate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
transferExecuted := transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
transferExecuted := transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||||
recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
|
recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
|
||||||
populationCanChange := !(transferExecuted && !recordDate.Before(transferDate))
|
_, economicCutoffDate := transferRecordingWindow(transfer)
|
||||||
|
isTransition := !recordDate.Before(transferDate) && recordDate.Before(economicCutoffDate)
|
||||||
|
isLaying := !recordDate.Before(economicCutoffDate)
|
||||||
|
|
||||||
return populationCanChange, transferExecuted, transfer, transferDate, nil
|
populationCanChange := true
|
||||||
|
if category == strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||||
|
populationCanChange = !(transferExecuted && !recordDate.Before(transferDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
return populationCanChange, transferExecuted, isTransition, isLaying, transfer, transferDate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) ensurePopulationMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
|
func (s *recordingService) ensurePopulationMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
|
||||||
populationCanChange, _, transfer, transferDate, err := s.evaluatePopulationMutationState(ctx, recording)
|
populationCanChange, _, _, _, transfer, transferDate, err := s.evaluatePopulationMutationState(ctx, recording)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1590,7 +1606,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
|
|
||||||
var feedIntake float64
|
var feedIntake float64
|
||||||
if remainingChick > 0 && usageInGrams > 0 {
|
if remainingChick > 0 && usageInGrams > 0 {
|
||||||
feedIntake = (usageInGrams / remainingChick) * 1000
|
feedIntake = usageInGrams / remainingChick
|
||||||
updates["feed_intake"] = feedIntake
|
updates["feed_intake"] = feedIntake
|
||||||
recording.FeedIntake = &feedIntake
|
recording.FeedIntake = &feedIntake
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ const (
|
|||||||
transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN"
|
transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN"
|
||||||
transferLayingStockableLane = "STOCKABLE"
|
transferLayingStockableLane = "STOCKABLE"
|
||||||
transferLayingSourceTable = "laying_transfer_targets"
|
transferLayingSourceTable = "laying_transfer_targets"
|
||||||
|
|
||||||
|
transferLayingOutFunctionCode = "TRANSFER_TO_LAYING_OUT"
|
||||||
|
transferLayingUsableLane = "USABLE"
|
||||||
|
transferLayingUsableSourceTable = "laying_transfers"
|
||||||
|
transferLayingLegacyUsableSourceTable = "laying_transfer_sources"
|
||||||
)
|
)
|
||||||
|
|
||||||
func reflowTransferLayingScope(
|
func reflowTransferLayingScope(
|
||||||
@@ -85,3 +90,90 @@ func resolveTransferLayingFlagGroupByProductWarehouse(ctx context.Context, tx *g
|
|||||||
|
|
||||||
return strings.TrimSpace(selected.FlagGroupCode), nil
|
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type transferLayingUsableRouteRule struct {
|
||||||
|
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||||
|
SourceTable string `gorm:"column:source_table"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveTransferLayingUsableFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||||
|
rows := make([]transferLayingUsableRouteRule, 0)
|
||||||
|
err := tx.WithContext(ctx).
|
||||||
|
Table("fifo_stock_v2_route_rules rr").
|
||||||
|
Select("rr.flag_group_code, rr.source_table").
|
||||||
|
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.lane = ?", transferLayingUsableLane).
|
||||||
|
Where("rr.function_code = ?", transferLayingOutFunctionCode).
|
||||||
|
Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM product_warehouses pw
|
||||||
|
JOIN flags f ON f.flagable_id = pw.product_id
|
||||||
|
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
|
||||||
|
WHERE pw.id = ?
|
||||||
|
AND f.flagable_type = ?
|
||||||
|
AND fm.flag_group_code = rr.flag_group_code
|
||||||
|
)
|
||||||
|
`, productWarehouseID, entity.FlagableTypeProduct).
|
||||||
|
Order("rr.id ASC").
|
||||||
|
Find(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateTransferLayingUsableRouteRules(rows, productWarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTransferLayingUsableRouteRules(rows []transferLayingUsableRouteRule, productWarehouseID uint) (string, error) {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT tidak ditemukan untuk source warehouse %d",
|
||||||
|
productWarehouseID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedFlagGroup string
|
||||||
|
hasHeaderRule := false
|
||||||
|
hasLegacyRule := false
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
sourceTable := strings.ToLower(strings.TrimSpace(row.SourceTable))
|
||||||
|
flagGroupCode := strings.TrimSpace(row.FlagGroupCode)
|
||||||
|
|
||||||
|
switch sourceTable {
|
||||||
|
case transferLayingUsableSourceTable:
|
||||||
|
if flagGroupCode == "" {
|
||||||
|
return "", fmt.Errorf("konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT memiliki flag_group_code kosong")
|
||||||
|
}
|
||||||
|
hasHeaderRule = true
|
||||||
|
if selectedFlagGroup == "" {
|
||||||
|
selectedFlagGroup = flagGroupCode
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if selectedFlagGroup != flagGroupCode {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT ambigu untuk source warehouse %d",
|
||||||
|
productWarehouseID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case transferLayingLegacyUsableSourceTable:
|
||||||
|
hasLegacyRule = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasLegacyRule {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"konfigurasi FIFO v2 legacy untuk TRANSFER_TO_LAYING_OUT masih aktif (source_table=%s)",
|
||||||
|
transferLayingLegacyUsableSourceTable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if !hasHeaderRule {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT aktif untuk source_table=%s tidak ditemukan",
|
||||||
|
transferLayingUsableSourceTable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedFlagGroup, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateTransferLayingUsableRouteRules(t *testing.T) {
|
||||||
|
t.Run("valid header rule", func(t *testing.T) {
|
||||||
|
flagGroup, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
|
||||||
|
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
|
||||||
|
}, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if flagGroup != "AYAM" {
|
||||||
|
t.Fatalf("unexpected flag group: %s", flagGroup)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing usable header rule", func(t *testing.T) {
|
||||||
|
_, err := validateTransferLayingUsableRouteRules(nil, 10)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "tidak ditemukan") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("legacy rule still active", func(t *testing.T) {
|
||||||
|
_, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
|
||||||
|
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
|
||||||
|
{FlagGroupCode: "AYAM", SourceTable: transferLayingLegacyUsableSourceTable},
|
||||||
|
}, 10)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "legacy") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ambiguous active header rules", func(t *testing.T) {
|
||||||
|
_, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
|
||||||
|
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
|
||||||
|
{FlagGroupCode: "PAKAN", SourceTable: transferLayingUsableSourceTable},
|
||||||
|
}, 10)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "ambigu") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1026,6 +1026,33 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := resolveTransferLayingUsableFlagGroupByProductWarehouse(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
*transfer.SourceProductWarehouseId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Konfigurasi FIFO v2 transfer laying tidak valid: %v", err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
activeConsumeAllocCount, err := s.countActiveTransferSourceConsumeAllocations(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
transfer.Id,
|
||||||
|
*transfer.SourceProductWarehouseId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi alokasi FIFO source transfer laying")
|
||||||
|
}
|
||||||
|
if transfer.SourceUsageQty > 1e-6 && activeConsumeAllocCount == 0 {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Unexecute transfer laying %s gagal: alokasi FIFO source tidak ditemukan", transfer.TransferNumber),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
|
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||||
@@ -1067,21 +1094,40 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
asOf := normalizeDateOnlyUTC(transfer.TransferDate)
|
rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
|
||||||
|
ProductWarehouseID: *transfer.SourceProductWarehouseId,
|
||||||
|
Usable: commonSvc.FifoStockV2Ref{
|
||||||
|
ID: transfer.Id,
|
||||||
|
LegacyTypeKey: fifo.UsableKeyTransferToLayingOut.String(),
|
||||||
|
FunctionCode: transferLayingOutFunctionCode,
|
||||||
|
},
|
||||||
|
Reason: fmt.Sprintf("transfer laying unexecute #%s [%s]", transfer.TransferNumber, flagGroupCode),
|
||||||
|
Tx: dbTransaction,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 source transfer laying: %v", err))
|
||||||
|
}
|
||||||
|
releasedQty := 0.0
|
||||||
|
if rollbackResult != nil {
|
||||||
|
releasedQty = rollbackResult.ReleasedQty
|
||||||
|
}
|
||||||
|
if transfer.SourceUsageQty > 1e-6 && releasedQty < transfer.SourceUsageQty-1e-6 {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Rollback FIFO v2 source transfer laying tidak lengkap. Dibutuhkan %.3f, terlepas %.3f",
|
||||||
|
transfer.SourceUsageQty,
|
||||||
|
releasedQty,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
|
if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
|
||||||
"source_usage_qty": 0,
|
"source_usage_qty": 0,
|
||||||
"source_pending_usage_qty": 0,
|
"source_pending_usage_qty": 0,
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal reset kuantitas source transfer laying")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal reset kuantitas source transfer laying")
|
||||||
}
|
}
|
||||||
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
|
|
||||||
FlagGroupCode: transferToLayingFlagGroupCode,
|
|
||||||
ProductWarehouseID: *transfer.SourceProductWarehouseId,
|
|
||||||
AsOf: &asOf,
|
|
||||||
Tx: dbTransaction,
|
|
||||||
}); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 source transfer laying: %v", err))
|
|
||||||
}
|
|
||||||
if err := fifoV2.ReleasePopulationConsumptionByUsable(
|
if err := fifoV2.ReleasePopulationConsumptionByUsable(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
dbTransaction,
|
dbTransaction,
|
||||||
@@ -1576,6 +1622,34 @@ func (s *transferLayingService) hasDownstreamRecordingOnTarget(
|
|||||||
return true, normalizeDateOnlyUTC(earliest.RecordDatetime), nil
|
return true, normalizeDateOnlyUTC(earliest.RecordDatetime), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
transferID uint,
|
||||||
|
productWarehouseID uint,
|
||||||
|
) (int64, error) {
|
||||||
|
if transferID == 0 || productWarehouseID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||||
if projectFlockKandangID == 0 {
|
if projectFlockKandangID == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user