mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/fifo-v2' into 'development'
Implement delete transfer to laying, chickin, and stock adjustment See merge request mbugroup/lti-api!371
This commit is contained in:
@@ -151,25 +151,25 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
|
||||
// })
|
||||
// }
|
||||
|
||||
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
||||
// param := c.Params("id")
|
||||
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
// id, err := strconv.Atoi(param)
|
||||
// if err != nil {
|
||||
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
// }
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
||||
// return err
|
||||
// }
|
||||
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return c.Status(fiber.StatusOK).
|
||||
// JSON(response.Common{
|
||||
// Code: fiber.StatusOK,
|
||||
// Status: "success",
|
||||
// Message: "Delete chickin successfully",
|
||||
// })
|
||||
// }
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete chickin successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ChickinController) Approval(c *fiber.Ctx) error {
|
||||
req := new(validation.Approve)
|
||||
|
||||
@@ -3,6 +3,7 @@ package dto
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
areaRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
flockRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||
@@ -35,13 +36,13 @@ type ChickinRelationDTO struct {
|
||||
}
|
||||
|
||||
type ProjectFlockDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Period int `json:"period"`
|
||||
Category string `json:"category"`
|
||||
Flock *flockRelationDTO.FlockRelationDTO `json:"flock"`
|
||||
Area *areaRelationDTO.AreaRelationDTO `json:"area"`
|
||||
StandardFcr *float64 `json:"standard_fcr"`
|
||||
Location *locationRelationDTO.LocationRelationDTO `json:"location"`
|
||||
Id uint `json:"id"`
|
||||
Period int `json:"period"`
|
||||
Category string `json:"category"`
|
||||
Flock *flockRelationDTO.FlockRelationDTO `json:"flock"`
|
||||
Area *areaRelationDTO.AreaRelationDTO `json:"area"`
|
||||
StandardFcr *float64 `json:"standard_fcr"`
|
||||
Location *locationRelationDTO.LocationRelationDTO `json:"location"`
|
||||
}
|
||||
|
||||
type ProjectFlockKandangDTO struct {
|
||||
@@ -123,13 +124,13 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO {
|
||||
location = &mapped
|
||||
}
|
||||
return ProjectFlockDTO{
|
||||
Id: e.Id,
|
||||
Period: pfk.Period,
|
||||
Category: e.Category,
|
||||
Flock: flock,
|
||||
Area: area,
|
||||
Id: e.Id,
|
||||
Period: pfk.Period,
|
||||
Category: e.Category,
|
||||
Flock: flock,
|
||||
Area: area,
|
||||
StandardFcr: resolveProjectFlockStandardFcr(e),
|
||||
Location: location,
|
||||
Location: location,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +220,7 @@ func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 {
|
||||
}
|
||||
week := 1
|
||||
if e.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
week = 18
|
||||
week = config.LayingWeekStart()
|
||||
}
|
||||
for _, detail := range e.ProductionStandard.ProductionStandardDetails {
|
||||
if detail.Week == week && detail.StandardFCR != nil {
|
||||
|
||||
@@ -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.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne)
|
||||
// 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)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,14 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
@@ -33,13 +33,14 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||
// register workflow steps for chickin approvals
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||
}
|
||||
|
||||
expenseRepo := rExpense.NewExpenseRepository(db)
|
||||
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo,kandangRepo, validate)
|
||||
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, fifoStockV2Service, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, kandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
||||
|
||||
+89
-1
@@ -1,8 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -35,6 +37,7 @@ type projectFlockKandangService struct {
|
||||
Validate *validator.Validate
|
||||
Repository repository.ProjectFlockKandangRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||
ExpenseRepo expenseRepo.ExpenseRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
@@ -69,12 +72,13 @@ type ExpenseSummary struct {
|
||||
Reference string `json:"reference_number"`
|
||||
}
|
||||
|
||||
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||
return &projectFlockKandangService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
FifoStockV2Svc: fifoStockV2Svc,
|
||||
ExpenseRepo: expenseRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
@@ -694,7 +698,91 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
|
||||
sourceAvailable, err := s.resolveLayingSourceAvailableQty(c.Context(), nil, productWarehouse.Id, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if sourceAvailable < availableQty {
|
||||
availableQty = sourceAvailable
|
||||
}
|
||||
}
|
||||
|
||||
return availableQty, nil
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) resolveLayingSourceAvailableQty(ctx context.Context, tx *gorm.DB, productWarehouseID uint, asOf *time.Time) (float64, error) {
|
||||
if productWarehouseID == 0 || s.FifoStockV2Svc == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, 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
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, row := range gatherRows {
|
||||
if row.AvailableQuantity <= 0 {
|
||||
continue
|
||||
}
|
||||
total += row.AvailableQuantity
|
||||
}
|
||||
return math.Max(total, 0), nil
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||
type row struct {
|
||||
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||
}
|
||||
selected := row{}
|
||||
|
||||
db := s.Repository.DB()
|
||||
if tx != nil {
|
||||
db = tx
|
||||
}
|
||||
|
||||
err := db.WithContext(ctx).
|
||||
Table("fifo_stock_v2_route_rules rr").
|
||||
Select("rr.flag_group_code").
|
||||
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
|
||||
Where("rr.is_active = TRUE").
|
||||
Where("rr.lane = 'STOCKABLE'").
|
||||
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("fg.priority ASC, rr.id ASC").
|
||||
Limit(1).
|
||||
Take(&selected).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
|
||||
@@ -62,6 +63,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
|
||||
Search: c.Query("search", ""),
|
||||
SortBy: c.Query("sort_by", ""),
|
||||
SortOrder: c.Query("sort_order", ""),
|
||||
Status: strings.TrimSpace(c.Query("status", "")),
|
||||
}
|
||||
|
||||
if area := c.QueryInt("area_id", 0); area > 0 {
|
||||
@@ -272,10 +274,20 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
projectFlockId := c.QueryInt("project_flock_id", 0)
|
||||
kandangId := c.QueryInt("kandang_id", 0)
|
||||
withPopulation := c.QueryBool("withpopulation", false)
|
||||
recordDateRaw := strings.TrimSpace(c.Query("record_date", ""))
|
||||
var recordDate *time.Time
|
||||
|
||||
if projectFlockId == 0 || kandangId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
|
||||
}
|
||||
if recordDateRaw != "" {
|
||||
parsed, err := time.Parse("2006-01-02", recordDateRaw)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format")
|
||||
}
|
||||
utc := parsed.UTC()
|
||||
recordDate = &utc
|
||||
}
|
||||
|
||||
result, availableStock, err := u.ProjectflockService.GetProjectFlockKandangByProjectAndKandang(c, uint(projectFlockId), uint(kandangId))
|
||||
if err != nil {
|
||||
@@ -300,6 +312,12 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse)
|
||||
dtoResult.Warehouse = &mapped
|
||||
}
|
||||
if isTransition, isLaying, serr := u.ProjectflockService.GetProjectFlockKandangTransferStateAtDate(c, result.Id, recordDate); serr != nil {
|
||||
return serr
|
||||
} else {
|
||||
dtoResult.IsTransition = isTransition
|
||||
dtoResult.IsLaying = isLaying
|
||||
}
|
||||
if withPopulation {
|
||||
population := dtoResult.AvailableQuantity
|
||||
dtoResult.Population = &population
|
||||
|
||||
@@ -3,6 +3,7 @@ package dto
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
@@ -25,17 +26,17 @@ type ProjectFlockRelationDTO struct {
|
||||
|
||||
type ProjectFlockListDTO struct {
|
||||
ProjectFlockRelationDTO
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
StandardFcr *float64 `json:"standard_fcr,omitempty"`
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
StandardFcr *float64 `json:"standard_fcr,omitempty"`
|
||||
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
|
||||
ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
|
||||
ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||
}
|
||||
|
||||
type KandangWithProjectFlockIdDTO struct {
|
||||
@@ -212,7 +213,7 @@ func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 {
|
||||
}
|
||||
week := 1
|
||||
if e.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
week = 18
|
||||
week = config.LayingWeekStart()
|
||||
}
|
||||
for _, detail := range e.ProductionStandard.ProductionStandardDetails {
|
||||
if detail.Week == week && detail.StandardFCR != nil {
|
||||
|
||||
@@ -40,6 +40,8 @@ type ProjectFlockKandangDTO struct {
|
||||
AvailableQuantity float64 `json:"available_quantity"`
|
||||
Population *float64 `json:"population,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 {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
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"
|
||||
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"
|
||||
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)
|
||||
projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db)
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(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))
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
ProjectflockRoutes(router, userService, projectflockService)
|
||||
|
||||
+16
-5
@@ -51,6 +51,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx co
|
||||
err := r.DB().WithContext(ctx).
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("project_chickins.deleted_at IS NULL").
|
||||
Preload("ProjectChickin").
|
||||
Find(&records).Error
|
||||
if err != nil {
|
||||
@@ -87,6 +88,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProd
|
||||
err := r.DB().WithContext(ctx).
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ? AND project_flock_populations.product_warehouse_id = ?", projectFlockKandangID, productWarehouseID).
|
||||
Where("project_chickins.deleted_at IS NULL").
|
||||
Find(&records).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -99,8 +101,10 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("project_flock_populations").
|
||||
Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS available_qty").
|
||||
Joins("JOIN product_warehouses pw ON project_flock_populations.product_warehouse_id = pw.id").
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("project_chickins.deleted_at IS NULL").
|
||||
Where("project_flock_populations.deleted_at IS NULL").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -111,9 +115,12 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
|
||||
func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Select("COALESCE(SUM(total_qty - total_used_qty), 0)").
|
||||
Table("project_flock_populations").
|
||||
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0)").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_flock_populations.product_warehouse_id = ?", productWarehouseID).
|
||||
Where("project_chickins.deleted_at IS NULL").
|
||||
Where("project_flock_populations.deleted_at IS NULL").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -128,6 +135,8 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand
|
||||
Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS total_qty").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("project_chickins.deleted_at IS NULL").
|
||||
Where("project_flock_populations.deleted_at IS NULL").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -145,6 +154,8 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKand
|
||||
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("project_chickins.deleted_at IS NULL").
|
||||
Where("project_flock_populations.deleted_at IS NULL").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -110,6 +111,28 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali
|
||||
AND pfk.kandang_id IN ?
|
||||
)`, params.KandangIds)
|
||||
}
|
||||
if params.Status != "" {
|
||||
db = db.Where(`
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM approvals latest_approval
|
||||
WHERE latest_approval.approvable_type = ?
|
||||
AND latest_approval.approvable_id = project_flocks.id
|
||||
AND latest_approval.id = (
|
||||
SELECT a2.id
|
||||
FROM approvals a2
|
||||
WHERE a2.approvable_type = ?
|
||||
AND a2.approvable_id = project_flocks.id
|
||||
ORDER BY a2.id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
AND LOWER(latest_approval.step_name) = LOWER(?)
|
||||
)`,
|
||||
utils.ApprovalWorkflowProjectFlock.String(),
|
||||
utils.ApprovalWorkflowProjectFlock.String(),
|
||||
params.Status,
|
||||
)
|
||||
}
|
||||
|
||||
db = r.applySearchFilters(db, params.Search)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
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"
|
||||
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"
|
||||
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -44,6 +45,8 @@ type ProjectflockService interface {
|
||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
|
||||
GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error)
|
||||
GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error)
|
||||
GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (bool, bool, error)
|
||||
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||
@@ -64,6 +67,7 @@ type projectflockService struct {
|
||||
PivotRepo repository.ProjectFlockKandangRepository
|
||||
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||
RecordingRepo recordingRepo.RecordingRepository
|
||||
TransferLayingRepo transferLayingRepo.TransferLayingRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
}
|
||||
@@ -85,6 +89,7 @@ func NewProjectflockService(
|
||||
nonstockRepo nonstockRepository.NonstockRepository,
|
||||
populationRepo repository.ProjectFlockPopulationRepository,
|
||||
recordingRepo recordingRepo.RecordingRepository,
|
||||
transferLayingRepo transferLayingRepo.TransferLayingRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
validate *validator.Validate,
|
||||
|
||||
@@ -102,6 +107,7 @@ func NewProjectflockService(
|
||||
PivotRepo: pivotRepo,
|
||||
PopulationRepo: populationRepo,
|
||||
RecordingRepo: recordingRepo,
|
||||
TransferLayingRepo: transferLayingRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
||||
}
|
||||
@@ -538,6 +544,70 @@ func (s projectflockService) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, p
|
||||
return earliest, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) {
|
||||
return s.GetProjectFlockKandangTransferStateAtDate(ctx, projectFlockKandangID, nil)
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (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
|
||||
}
|
||||
|
||||
reference := normalizeDateOnlyUTC(time.Now().UTC())
|
||||
if referenceDate != nil && !referenceDate.IsZero() {
|
||||
reference = normalizeDateOnlyUTC(referenceDate.UTC())
|
||||
}
|
||||
isTransition := !reference.Before(physicalMoveDate) && reference.Before(economicCutoffDate)
|
||||
isLaying := !reference.Before(economicCutoffDate)
|
||||
|
||||
return isTransition, isLaying, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
||||
idStr = strings.TrimSpace(idStr)
|
||||
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
||||
@@ -579,6 +649,10 @@ func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idSt
|
||||
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) {
|
||||
if s.PopulationRepo == nil {
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
|
||||
|
||||
@@ -20,6 +20,7 @@ type Query struct {
|
||||
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||
Period int `query:"period" validate:"omitempty,number,gt=0"`
|
||||
Category string `query:"category" validate:"omitempty"`
|
||||
Status string `query:"status" validate:"omitempty,oneof=Pengajuan Aktif Selesai"`
|
||||
KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"`
|
||||
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=transfer_to_laying"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto"
|
||||
@@ -86,6 +87,8 @@ type RecordingRelationDTO struct {
|
||||
EggWeight float64 `json:"egg_weight"`
|
||||
PopulationCanChange bool `json:"population_can_change"`
|
||||
TransferExecuted *bool `json:"transfer_executed,omitempty"`
|
||||
IsTransition bool `json:"is_transition"`
|
||||
IsLaying bool `json:"is_laying"`
|
||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||
}
|
||||
|
||||
@@ -247,6 +250,8 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||
EggWeight: floatValue(e.EggWeight),
|
||||
PopulationCanChange: boolValueDefault(e.PopulationCanChange, true),
|
||||
TransferExecuted: e.TransferExecuted,
|
||||
IsTransition: boolValueDefault(e.IsTransition, false),
|
||||
IsLaying: boolValueDefault(e.IsLaying, false),
|
||||
Approval: latestApproval,
|
||||
}
|
||||
}
|
||||
@@ -304,7 +309,7 @@ func recordingWeekValue(e entity.Recording) int {
|
||||
}
|
||||
weekBase := 1
|
||||
if isLayingRecording(e) {
|
||||
weekBase = 18
|
||||
weekBase = config.LayingWeekStart()
|
||||
}
|
||||
return ((day - 1) / 7) + weekBase
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
nonstockRepo,
|
||||
projectFlockPopulationRepo,
|
||||
recordingRepo,
|
||||
transferLayingRepo,
|
||||
approvalService,
|
||||
validate,
|
||||
)
|
||||
@@ -154,7 +155,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
productWarehouseRepo,
|
||||
warehouseRepo,
|
||||
approvalService,
|
||||
fifoService,
|
||||
fifoStockV2Service,
|
||||
validate,
|
||||
)
|
||||
|
||||
@@ -71,6 +71,7 @@ type RecordingRepository interface {
|
||||
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
||||
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
|
||||
GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error)
|
||||
GetProjectFlockKandangIDsByPopulationWarehouseIDs(ctx context.Context, tx *gorm.DB, productWarehouseIDs []uint) ([]uint, error)
|
||||
ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error
|
||||
ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error)
|
||||
}
|
||||
@@ -874,6 +875,34 @@ func (r *RecordingRepositoryImpl) GetAverageTargetMetricsByProjectFlockKandangID
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetProjectFlockKandangIDsByPopulationWarehouseIDs(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
productWarehouseIDs []uint,
|
||||
) ([]uint, error) {
|
||||
if len(productWarehouseIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
var kandangIDs []uint
|
||||
if err := db.Table("project_flock_populations pfp").
|
||||
Select("DISTINCT pc.project_flock_kandang_id").
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("pfp.product_warehouse_id IN ?", productWarehouseIDs).
|
||||
Where("pfp.deleted_at IS NULL").
|
||||
Where("pc.deleted_at IS NULL").
|
||||
Pluck("pc.project_flock_kandang_id", &kandangIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kandangIDs, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
|
||||
@@ -185,12 +185,14 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
||||
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 {
|
||||
return nil, 0, stateErr
|
||||
}
|
||||
recordings[i].PopulationCanChange = boolPtr(populationCanChange)
|
||||
recordings[i].TransferExecuted = boolPtr(transferExecuted)
|
||||
recordings[i].IsTransition = boolPtr(isTransition)
|
||||
recordings[i].IsLaying = boolPtr(isLaying)
|
||||
}
|
||||
return recordings, total, nil
|
||||
}
|
||||
@@ -251,12 +253,14 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
|
||||
recording.DepletionRate = &rate
|
||||
}
|
||||
|
||||
populationCanChange, transferExecuted, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
|
||||
populationCanChange, transferExecuted, isTransition, isLaying, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
|
||||
if stateErr != nil {
|
||||
return nil, stateErr
|
||||
}
|
||||
recording.PopulationCanChange = boolPtr(populationCanChange)
|
||||
recording.TransferExecuted = boolPtr(transferExecuted)
|
||||
recording.IsTransition = boolPtr(isTransition)
|
||||
recording.IsLaying = boolPtr(isLaying)
|
||||
|
||||
return recording, nil
|
||||
}
|
||||
@@ -320,6 +324,15 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
if err := s.enforceTransferRecordingRoute(ctx, pfk, recordTime, routePayload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if routePayload.DepletionCount > 0 {
|
||||
if err := s.ensureDepletionMutationAllowed(ctx, &entity.Recording{
|
||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||
RecordDatetime: recordTime,
|
||||
ProjectFlockKandang: pfk,
|
||||
}, "buat"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.ProjectFlockSvc.EnsureProjectFlockApproved(ctx, pfk.ProjectFlockId); err != nil {
|
||||
return nil, err
|
||||
@@ -518,9 +531,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
|
||||
recordingEntity = recording
|
||||
if err := s.ensurePopulationMutationAllowed(ctx, recordingEntity, "ubah"); err != nil {
|
||||
return err
|
||||
}
|
||||
pfkForRoute := recordingEntity.ProjectFlockKandang
|
||||
if pfkForRoute == nil || pfkForRoute.Id == 0 {
|
||||
fetchedPfk, fetchErr := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recordingEntity.ProjectFlockKandangId)
|
||||
@@ -533,7 +543,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
pfkForRoute = fetchedPfk
|
||||
}
|
||||
routePayload := buildRecordingRoutePayloadFromUpdate(req, recordingEntity)
|
||||
routePayload := buildRecordingRoutePayloadFromUpdate(req)
|
||||
if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -590,6 +600,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
if match {
|
||||
hasDepletionChanges = false
|
||||
} else {
|
||||
if err := s.ensurePopulationMutationAllowed(ctx, recordingEntity, "ubah"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.ensureDepletionMutationAllowed(ctx, recordingEntity, "ubah"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -931,15 +944,15 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
s.Log.Errorf("Failed to find recording: %+v", err)
|
||||
return err
|
||||
}
|
||||
if err := s.ensurePopulationMutationAllowed(ctx, recording, "hapus"); err != nil {
|
||||
return err
|
||||
}
|
||||
existingDepletions, err := s.Repository.ListDepletions(tx, recording.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||
return err
|
||||
}
|
||||
if len(existingDepletions) > 0 {
|
||||
if err := s.ensurePopulationMutationAllowed(ctx, recording, "hapus"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.ensureDepletionMutationAllowed(ctx, recording, "hapus"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -990,46 +1003,122 @@ func (s *recordingService) resolveRecordingCategory(ctx context.Context, recordi
|
||||
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 {
|
||||
return true, false, nil, time.Time{}, nil
|
||||
return true, false, false, false, nil, time.Time{}, nil
|
||||
}
|
||||
|
||||
category, err := s.resolveRecordingCategory(ctx, recording)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||
return true, false, nil, time.Time{}, nil
|
||||
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||
}
|
||||
|
||||
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 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)
|
||||
return true, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||
s.Log.Errorf("Failed to resolve approved transfer for recording %d: %+v", recording.Id, err)
|
||||
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||
}
|
||||
if transfer == nil {
|
||||
return true, false, nil, time.Time{}, nil
|
||||
return true, false, false, false, nil, time.Time{}, nil
|
||||
}
|
||||
|
||||
transferDate := transferPhysicalMoveDate(transfer)
|
||||
if transferDate.IsZero() {
|
||||
return true, false, transfer, transferDate, nil
|
||||
return true, false, false, false, transfer, transferDate, nil
|
||||
}
|
||||
|
||||
transferExecuted := transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||
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))
|
||||
|
||||
if transferExecuted && !recordDate.Before(transferDate) {
|
||||
hasTargetLayingRecording, checkErr := s.hasAnyRecordingOnTransferTargets(ctx, transfer)
|
||||
if checkErr != nil {
|
||||
s.Log.Errorf("Failed to resolve target laying recording state for transfer %d: %+v", transfer.Id, checkErr)
|
||||
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi status transisi recording")
|
||||
}
|
||||
if hasTargetLayingRecording {
|
||||
isTransition = false
|
||||
isLaying = true
|
||||
} else {
|
||||
today := normalizeDateOnlyUTC(time.Now().UTC())
|
||||
if !today.Before(economicCutoffDate) {
|
||||
isTransition = true
|
||||
isLaying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return populationCanChange, transferExecuted, isTransition, isLaying, transfer, transferDate, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) hasAnyRecordingOnTransferTargets(ctx context.Context, transfer *entity.LayingTransfer) (bool, error) {
|
||||
if transfer == nil || transfer.Id == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
targetIDs, err := s.transferTargetProjectFlockKandangIDs(ctx, transfer.Id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(targetIDs) == 0 {
|
||||
// Keep existing behavior for legacy or incomplete target mapping.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = s.Repository.DB().
|
||||
WithContext(ctx).
|
||||
Table("recordings").
|
||||
Where("deleted_at IS NULL").
|
||||
Where("project_flock_kandangs_id IN ?", targetIDs).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) transferTargetProjectFlockKandangIDs(ctx context.Context, transferID uint) ([]uint, error) {
|
||||
if transferID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var targetIDs []uint
|
||||
err := s.Repository.DB().
|
||||
WithContext(ctx).
|
||||
Table("laying_transfer_targets").
|
||||
Where("laying_transfer_id = ?", transferID).
|
||||
Where("deleted_at IS NULL").
|
||||
Pluck("target_project_flock_kandang_id", &targetIDs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return targetIDs, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -1056,7 +1145,7 @@ func (s *recordingService) ensurePopulationMutationAllowed(ctx context.Context,
|
||||
}
|
||||
|
||||
func (s *recordingService) ensureDepletionMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
|
||||
if recording == nil || recording.Id == 0 || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
|
||||
if recording == nil || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1075,19 +1164,16 @@ func (s *recordingService) ensureDepletionMutationAllowed(ctx context.Context, r
|
||||
category = strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||
}
|
||||
|
||||
if !shouldGuardDepletionMutation(category) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
transfer *entity.LayingTransfer
|
||||
err error
|
||||
)
|
||||
|
||||
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 nil
|
||||
}
|
||||
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
@@ -1100,22 +1186,28 @@ func (s *recordingService) ensureDepletionMutationAllowed(ctx context.Context, r
|
||||
}
|
||||
|
||||
recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
|
||||
physicalMoveDate := transferPhysicalMoveDate(transfer)
|
||||
if physicalMoveDate.IsZero() || recordDate.Before(physicalMoveDate) {
|
||||
return nil
|
||||
transferNumber := strings.TrimSpace(transfer.TransferNumber)
|
||||
if transferNumber == "" {
|
||||
transferNumber = "-"
|
||||
}
|
||||
executedDate := normalizeDateOnlyUTC(*transfer.ExecutedAt)
|
||||
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"Deplesi recording tanggal %s tidak dapat di%s karena sudah mempengaruhi transfer laying %s yang sudah dieksekusi. Lakukan unexecute transfer terlebih dahulu bila belum ada pemakaian downstream.",
|
||||
"Deplesi recording tanggal %s tidak dapat di%s karena transfer laying %s sudah dieksekusi pada %s. Setelah transfer dieksekusi, mutasi deplesi di kandang growing tidak diizinkan (termasuk backdate).",
|
||||
recordDate.Format("2006-01-02"),
|
||||
operation,
|
||||
transfer.TransferNumber,
|
||||
transferNumber,
|
||||
executedDate.Format("2006-01-02"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func shouldGuardDepletionMutation(category string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(category), string(utils.ProjectFlockCategoryGrowing))
|
||||
}
|
||||
|
||||
func (s *recordingService) tryAutoExecuteTransferForRecordingCreate(c *fiber.Ctx, pfk *entity.ProjectFlockKandang, recordTime time.Time) error {
|
||||
if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil || s.TransferLayingSvc == nil {
|
||||
return nil
|
||||
@@ -1295,60 +1387,34 @@ func buildRecordingRoutePayloadFromCreate(req *validation.Create) recordingRoute
|
||||
return payload
|
||||
}
|
||||
|
||||
func buildRecordingRoutePayloadFromUpdate(req *validation.Update, existing *entity.Recording) recordingRoutePayload {
|
||||
func buildRecordingRoutePayloadFromUpdate(req *validation.Update) recordingRoutePayload {
|
||||
payload := recordingRoutePayload{}
|
||||
if req == nil && existing == nil {
|
||||
if req == nil {
|
||||
return payload
|
||||
}
|
||||
|
||||
if req != nil && req.Stocks != nil {
|
||||
if req.Stocks != nil {
|
||||
for _, stock := range req.Stocks {
|
||||
if stock.Qty > 0 {
|
||||
payload.StockCount++
|
||||
}
|
||||
}
|
||||
} else if existing != nil {
|
||||
for _, stock := range existing.Stocks {
|
||||
usageQty := 0.0
|
||||
if stock.UsageQty != nil {
|
||||
usageQty = *stock.UsageQty
|
||||
}
|
||||
pendingQty := 0.0
|
||||
if stock.PendingQty != nil {
|
||||
pendingQty = *stock.PendingQty
|
||||
}
|
||||
if usageQty > 0 || pendingQty > 0 {
|
||||
payload.StockCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req != nil && req.Depletions != nil {
|
||||
if req.Depletions != nil {
|
||||
for _, depletion := range req.Depletions {
|
||||
if depletion.Qty > 0 {
|
||||
payload.DepletionCount++
|
||||
}
|
||||
}
|
||||
} else if existing != nil {
|
||||
for _, depletion := range existing.Depletions {
|
||||
if depletion.Qty > 0 {
|
||||
payload.DepletionCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req != nil && req.Eggs != nil {
|
||||
if req.Eggs != nil {
|
||||
for _, egg := range req.Eggs {
|
||||
if egg.Qty > 0 {
|
||||
payload.EggCount++
|
||||
}
|
||||
}
|
||||
} else if existing != nil {
|
||||
for _, egg := range existing.Eggs {
|
||||
if egg.Qty > 0 {
|
||||
payload.EggCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
@@ -1590,7 +1656,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
|
||||
var feedIntake float64
|
||||
if remainingChick > 0 && usageInGrams > 0 {
|
||||
feedIntake = (usageInGrams / remainingChick) * 1000
|
||||
feedIntake = usageInGrams / remainingChick
|
||||
updates["feed_intake"] = feedIntake
|
||||
recording.FeedIntake = &feedIntake
|
||||
} else {
|
||||
@@ -2010,10 +2076,7 @@ func (s *recordingService) reflowApplyRecordingStocks(
|
||||
}
|
||||
s.logStockTrace("reflow_apply:done", *refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desiredTotal, actualUsage, actualPending))
|
||||
|
||||
logDecrease := actualUsage
|
||||
if actualPending > 0 {
|
||||
logDecrease += actualPending
|
||||
}
|
||||
logDecrease := recordingStockRollbackQty(*refreshed)
|
||||
if logDecrease > 0 && shouldWriteLog {
|
||||
log := &entity.StockLog{
|
||||
ProductWarehouseId: refreshed.ProductWarehouseId,
|
||||
@@ -2057,11 +2120,8 @@ func (s *recordingService) reflowResetRecordingStocks(
|
||||
continue
|
||||
}
|
||||
|
||||
currentUsage := 0.0
|
||||
if stock.UsageQty != nil {
|
||||
currentUsage = *stock.UsageQty
|
||||
}
|
||||
s.logStockTrace("reflow_reset:start", stock, "")
|
||||
rollbackQty := recordingStockRollbackQty(stock)
|
||||
s.logStockTrace("reflow_reset:start", stock, fmt.Sprintf("rollback_qty=%.3f", rollbackQty))
|
||||
|
||||
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
|
||||
return err
|
||||
@@ -2078,13 +2138,13 @@ func (s *recordingService) reflowResetRecordingStocks(
|
||||
s.Log.Errorf("Failed to reflow FIFO v2 rollback for recording stock %d: %+v", stock.Id, err)
|
||||
return err
|
||||
}
|
||||
s.logStockTrace("reflow_reset:done", stock, "")
|
||||
s.logStockTrace("reflow_reset:done", stock, fmt.Sprintf("rollback_qty=%.3f", rollbackQty))
|
||||
|
||||
if currentUsage > 0 && shouldWriteLog {
|
||||
if rollbackQty > 0 && shouldWriteLog {
|
||||
log := &entity.StockLog{
|
||||
ProductWarehouseId: stock.ProductWarehouseId,
|
||||
CreatedBy: actorID,
|
||||
Increase: currentUsage,
|
||||
Increase: rollbackQty,
|
||||
LoggableType: string(utils.StockLogTypeRecording),
|
||||
LoggableId: stock.RecordingId,
|
||||
Notes: note,
|
||||
@@ -2098,6 +2158,24 @@ func (s *recordingService) reflowResetRecordingStocks(
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordingStockRollbackQty(stock entity.RecordingStock) float64 {
|
||||
usage := 0.0
|
||||
if stock.UsageQty != nil {
|
||||
usage = *stock.UsageQty
|
||||
}
|
||||
pending := 0.0
|
||||
if stock.PendingQty != nil {
|
||||
pending = *stock.PendingQty
|
||||
}
|
||||
if usage < 0 {
|
||||
usage = 0
|
||||
}
|
||||
if pending < 0 {
|
||||
pending = 0
|
||||
}
|
||||
return usage + pending
|
||||
}
|
||||
|
||||
type desiredStock struct {
|
||||
Usage float64
|
||||
Pending float64
|
||||
@@ -2316,15 +2394,10 @@ func (s *recordingService) reflowResetRecordingDepletionsOut(
|
||||
return errors.New("stock log repository is not available")
|
||||
}
|
||||
logState := newRecordingStockLogState()
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
||||
|
||||
for _, depletion := range depletions {
|
||||
if depletion.Id == 0 {
|
||||
continue
|
||||
}
|
||||
if err := stockAllocationRepo.ReleaseByUsable(ctx, fifo.UsableKeyRecordingDepletion.String(), depletion.Id, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logDepletionTrace("reflow_reset:start", depletion, "")
|
||||
|
||||
sourceWarehouseID := uint(0)
|
||||
@@ -2611,19 +2684,8 @@ func (s *recordingService) resyncPopulationUsageForDepletions(
|
||||
}
|
||||
|
||||
if len(sourceWarehouseIDs) > 0 {
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
var sourceKandangIDs []uint
|
||||
if err := db.Table("project_flock_populations pfp").
|
||||
Select("DISTINCT pc.project_flock_kandang_id").
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("pfp.product_warehouse_id IN ?", sourceWarehouseIDs).
|
||||
Where("pfp.deleted_at IS NULL").
|
||||
Where("pc.deleted_at IS NULL").
|
||||
Pluck("pc.project_flock_kandang_id", &sourceKandangIDs).Error; err != nil {
|
||||
sourceKandangIDs, err := s.Repository.GetProjectFlockKandangIDsByPopulationWarehouseIDs(ctx, tx, sourceWarehouseIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2635,62 +2697,7 @@ func (s *recordingService) resyncPopulationUsageForDepletions(
|
||||
}
|
||||
|
||||
for kandangID := range kandangIDs {
|
||||
if err := s.resyncPopulationUsageByProjectFlockKandang(ctx, tx, kandangID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *recordingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
var populationIDs []uint
|
||||
if err := db.Table("project_flock_populations pfp").
|
||||
Select("pfp.id").
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Pluck("pfp.id", &populationIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(populationIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type usageRow struct {
|
||||
StockableID uint `gorm:"column:stockable_id"`
|
||||
Used float64 `gorm:"column:used"`
|
||||
}
|
||||
var usageRows []usageRow
|
||||
if err := db.Table("stock_allocations").
|
||||
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
||||
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("stockable_id IN ?", populationIDs).
|
||||
Group("stockable_id").
|
||||
Scan(&usageRows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id IN ?", populationIDs).
|
||||
Update("total_used_qty", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range usageRows {
|
||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id = ?", row.StockableID).
|
||||
Update("total_used_qty", row.Used).Error; err != nil {
|
||||
if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, kandangID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
||||
productWarehouseRepo,
|
||||
warehouseRepo,
|
||||
approvalService,
|
||||
fifoService,
|
||||
fifoStockV2Service,
|
||||
validate,
|
||||
)
|
||||
|
||||
+132
@@ -2,15 +2,22 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LayingTransferTargetRepository interface {
|
||||
repository.BaseRepository[entity.LayingTransferTarget]
|
||||
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error)
|
||||
GetActiveDownstreamConsumptions(ctx context.Context, targetIDs []uint) ([]TargetDownstreamConsumption, error)
|
||||
GetEarliestRecordingDateByTarget(ctx context.Context, targetProjectFlockKandangID uint, sinceDate time.Time) (*time.Time, error)
|
||||
CountActiveTransferSourceConsumeAllocations(ctx context.Context, transferID uint, productWarehouseID uint) (int64, error)
|
||||
SyncPopulationUsageByProjectFlockKandang(ctx context.Context, projectFlockKandangID uint) error
|
||||
}
|
||||
|
||||
type LayingTransferTargetRepositoryImpl struct {
|
||||
@@ -18,6 +25,11 @@ type LayingTransferTargetRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type TargetDownstreamConsumption struct {
|
||||
UsableType string `gorm:"column:usable_type"`
|
||||
UsableID uint `gorm:"column:usable_id"`
|
||||
}
|
||||
|
||||
func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository {
|
||||
return &LayingTransferTargetRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db),
|
||||
@@ -36,3 +48,123 @@ func (r *LayingTransferTargetRepositoryImpl) GetByLayingTransferId(ctx context.C
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func (r *LayingTransferTargetRepositoryImpl) GetActiveDownstreamConsumptions(ctx context.Context, targetIDs []uint) ([]TargetDownstreamConsumption, error) {
|
||||
if len(targetIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var rows []TargetDownstreamConsumption
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("stock_allocations").
|
||||
Select("usable_type, usable_id").
|
||||
Where("stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Where("stockable_id IN ?", targetIDs).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("deleted_at IS NULL").
|
||||
Group("usable_type, usable_id").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *LayingTransferTargetRepositoryImpl) GetEarliestRecordingDateByTarget(ctx context.Context, targetProjectFlockKandangID uint, sinceDate time.Time) (*time.Time, error) {
|
||||
if targetProjectFlockKandangID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var earliest entity.Recording
|
||||
query := r.db.WithContext(ctx).
|
||||
Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", targetProjectFlockKandangID).
|
||||
Where("deleted_at IS NULL")
|
||||
if !sinceDate.IsZero() {
|
||||
query = query.Where("record_datetime >= ?", sinceDate)
|
||||
}
|
||||
if err := query.Order("record_datetime ASC").Limit(1).Take(&earliest).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := earliest.RecordDatetime.UTC()
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (r *LayingTransferTargetRepositoryImpl) CountActiveTransferSourceConsumeAllocations(ctx context.Context, transferID uint, productWarehouseID uint) (int64, error) {
|
||||
if transferID == 0 || productWarehouseID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
err := r.db.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
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *LayingTransferTargetRepositoryImpl) SyncPopulationUsageByProjectFlockKandang(ctx context.Context, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var populationIDs []uint
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("project_flock_populations pfp").
|
||||
Select("pfp.id").
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Pluck("pfp.id", &populationIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(populationIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type usageRow struct {
|
||||
StockableID uint `gorm:"column:stockable_id"`
|
||||
Used float64 `gorm:"column:used"`
|
||||
}
|
||||
var usageRows []usageRow
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("stock_allocations").
|
||||
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
||||
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("stockable_id IN ?", populationIDs).
|
||||
Group("stockable_id").
|
||||
Scan(&usageRows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id IN ?", populationIDs).
|
||||
Update("total_used_qty", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range usageRows {
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id = ?", row.StockableID).
|
||||
Update("total_used_qty", row.Used).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ const (
|
||||
transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN"
|
||||
transferLayingStockableLane = "STOCKABLE"
|
||||
transferLayingSourceTable = "laying_transfer_targets"
|
||||
|
||||
transferLayingOutFunctionCode = "TRANSFER_TO_LAYING_OUT"
|
||||
transferLayingUsableLane = "USABLE"
|
||||
transferLayingUsableSourceTable = "laying_transfers"
|
||||
transferLayingLegacyUsableSourceTable = "laying_transfer_sources"
|
||||
)
|
||||
|
||||
func reflowTransferLayingScope(
|
||||
@@ -85,3 +90,90 @@ func resolveTransferLayingFlagGroupByProductWarehouse(ctx context.Context, tx *g
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
+225
-101
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -56,12 +57,12 @@ type transferLayingService struct {
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
StockLogRepo rStockLogs.StockLogRepository
|
||||
ApprovalService commonSvc.ApprovalService
|
||||
FifoSvc commonSvc.FifoService
|
||||
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||
}
|
||||
|
||||
const (
|
||||
transferToLayingFlagGroupCode = "AYAM"
|
||||
transferToLayingFlagGroupCode = "AYAM"
|
||||
transferLayingDeleteDownstreamGuardMessage = "Transfer laying tidak dapat dihapus karena stok target transfer sudah dipakai transaksi turunan. Hapus dependensi terkait secara manual terlebih dahulu."
|
||||
)
|
||||
|
||||
func NewTransferLayingService(
|
||||
@@ -74,7 +75,6 @@ func NewTransferLayingService(
|
||||
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
approvalService commonSvc.ApprovalService,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||
validate *validator.Validate,
|
||||
) TransferLayingService {
|
||||
@@ -91,7 +91,6 @@ func NewTransferLayingService(
|
||||
WarehouseRepo: warehouseRepo,
|
||||
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
||||
ApprovalService: approvalService,
|
||||
FifoSvc: fifoSvc,
|
||||
FifoStockV2Svc: fifoStockV2Svc,
|
||||
}
|
||||
}
|
||||
@@ -610,6 +609,9 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if isLegacyTransfer(transfer) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat dihapus", transfer.TransferNumber))
|
||||
}
|
||||
if err := s.ensureNoDownstreamConsumptionForDelete(c.Context(), nil, transfer.TransferNumber, transfer.Targets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||
|
||||
@@ -635,6 +637,16 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
|
||||
// Lock header row to keep delete deterministic after single downstream guard check.
|
||||
if _, err := repoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
}); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
|
||||
}
|
||||
|
||||
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
||||
}
|
||||
@@ -1026,6 +1038,38 @@ 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),
|
||||
)
|
||||
}
|
||||
|
||||
type targetReflowKey struct {
|
||||
productWarehouseID uint
|
||||
}
|
||||
targetReflow := make(map[targetReflowKey]struct{})
|
||||
|
||||
for _, target := range targets {
|
||||
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||
@@ -1033,15 +1077,6 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
||||
if target.TotalQty <= 0 {
|
||||
continue
|
||||
}
|
||||
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
|
||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
||||
StockableID: target.Id,
|
||||
ProductWarehouseID: *target.ProductWarehouseId,
|
||||
Quantity: -target.TotalQty,
|
||||
Tx: dbTransaction,
|
||||
}); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback stok target transfer laying: %v", err))
|
||||
}
|
||||
|
||||
stockLogDecrease := &entity.StockLog{
|
||||
ProductWarehouseId: *target.ProductWarehouseId,
|
||||
@@ -1065,23 +1100,56 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
||||
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar target saat unexecute")
|
||||
}
|
||||
|
||||
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]any{
|
||||
"total_qty": 0,
|
||||
"total_used": 0,
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal rollback kuantitas target transfer laying")
|
||||
}
|
||||
targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}
|
||||
}
|
||||
asOf := normalizeDateOnlyUTC(transfer.TransferDate)
|
||||
for key := range targetReflow {
|
||||
if err := reflowTransferLayingScope(c.Context(), s.FifoStockV2Svc, dbTransaction, key.productWarehouseID, &asOf); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 target transfer laying: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
asOf := normalizeDateOnlyUTC(transfer.TransferDate)
|
||||
if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
|
||||
"source_usage_qty": 0,
|
||||
"source_pending_usage_qty": 0,
|
||||
}, nil); err != nil {
|
||||
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(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
@@ -1183,9 +1251,6 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
||||
if s.FifoStockV2Svc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||
}
|
||||
if s.FifoSvc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO service is not available")
|
||||
}
|
||||
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
||||
targetRepoTx := repository.NewLayingTransferTargetRepository(tx)
|
||||
@@ -1281,29 +1346,22 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
||||
}
|
||||
|
||||
type targetReflowKey struct {
|
||||
productWarehouseID uint
|
||||
}
|
||||
targetReflow := make(map[targetReflowKey]struct{})
|
||||
|
||||
for _, target := range targets {
|
||||
if target.ProductWarehouseId == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
||||
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
||||
StockableID: target.Id,
|
||||
ProductWarehouseID: *target.ProductWarehouseId,
|
||||
Quantity: target.TotalQty,
|
||||
Note: ¬e,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
|
||||
}
|
||||
|
||||
if err := targetRepoTx.PatchOne(ctx, target.Id, map[string]any{
|
||||
"total_qty": target.TotalQty,
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
|
||||
}
|
||||
targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}
|
||||
|
||||
stockLogIncrease := &entity.StockLog{
|
||||
ProductWarehouseId: *target.ProductWarehouseId,
|
||||
@@ -1330,6 +1388,11 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
|
||||
}
|
||||
}
|
||||
for key := range targetReflow {
|
||||
if err := reflowTransferLayingScope(ctx, s.FifoStockV2Svc, tx, key.productWarehouseID, &asOf); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow FIFO v2 target transfer laying: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1549,86 +1612,66 @@ func (s *transferLayingService) hasDownstreamRecordingOnTarget(
|
||||
targetProjectFlockKandangID uint,
|
||||
sinceDate time.Time,
|
||||
) (bool, time.Time, error) {
|
||||
if targetProjectFlockKandangID == 0 {
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
|
||||
var earliest entity.Recording
|
||||
query := db.Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", targetProjectFlockKandangID).
|
||||
Where("deleted_at IS NULL")
|
||||
if !sinceDate.IsZero() {
|
||||
query = query.Where("record_datetime >= ?", sinceDate)
|
||||
}
|
||||
|
||||
if err := query.Order("record_datetime ASC").Limit(1).Take(&earliest).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
recordDate, err := targetRepo.GetEarliestRecordingDateByTarget(ctx, targetProjectFlockKandangID, sinceDate)
|
||||
if err != nil {
|
||||
return false, time.Time{}, err
|
||||
}
|
||||
if recordDate == nil {
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
return true, normalizeDateOnlyUTC(*recordDate), nil
|
||||
}
|
||||
|
||||
return true, normalizeDateOnlyUTC(earliest.RecordDatetime), nil
|
||||
func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
transferID uint,
|
||||
productWarehouseID uint,
|
||||
) (int64, error) {
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
return targetRepo.CountActiveTransferSourceConsumeAllocations(ctx, transferID, productWarehouseID)
|
||||
}
|
||||
|
||||
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
return targetRepo.SyncPopulationUsageByProjectFlockKandang(ctx, projectFlockKandangID)
|
||||
}
|
||||
|
||||
var populationIDs []uint
|
||||
if err := db.Table("project_flock_populations pfp").
|
||||
Select("pfp.id").
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Pluck("pfp.id", &populationIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(populationIDs) == 0 {
|
||||
func sortedIDs(input map[uint]struct{}) []uint {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type usageRow struct {
|
||||
StockableID uint `gorm:"column:stockable_id"`
|
||||
Used float64 `gorm:"column:used"`
|
||||
}
|
||||
var usageRows []usageRow
|
||||
if err := db.Table("stock_allocations").
|
||||
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
||||
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("stockable_id IN ?", populationIDs).
|
||||
Group("stockable_id").
|
||||
Scan(&usageRows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id IN ?", populationIDs).
|
||||
Update("total_used_qty", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range usageRows {
|
||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id = ?", row.StockableID).
|
||||
Update("total_used_qty", row.Used).Error; err != nil {
|
||||
return err
|
||||
out := make([]uint, 0, len(input))
|
||||
for id := range input {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, id)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
return out
|
||||
}
|
||||
|
||||
return nil
|
||||
func joinUint(values []uint) string {
|
||||
if len(values) == 0 {
|
||||
return "-"
|
||||
}
|
||||
parts := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
parts = append(parts, fmt.Sprintf("%d", value))
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||
@@ -1647,3 +1690,84 @@ func isLegacyTransfer(transfer *entity.LayingTransfer) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *transferLayingService) ensureNoDownstreamConsumptionForDelete(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
transferNumber string,
|
||||
targets []entity.LayingTransferTarget,
|
||||
) error {
|
||||
targetIDs := make([]uint, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
if target.Id == 0 {
|
||||
continue
|
||||
}
|
||||
targetIDs = append(targetIDs, target.Id)
|
||||
}
|
||||
if len(targetIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetRepo := s.LayingTransferTargetRepo
|
||||
if tx != nil {
|
||||
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||
}
|
||||
|
||||
rows, err := targetRepo.GetActiveDownstreamConsumptions(ctx, targetIDs)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to validate downstream consumption for transfer laying %s: %+v", transferNumber, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transaksi turunan transfer laying")
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dependencyMap := make(map[string]map[uint]struct{})
|
||||
for _, row := range rows {
|
||||
label := mapTransferLayingDownstreamUsableLabel(row.UsableType)
|
||||
if _, ok := dependencyMap[label]; !ok {
|
||||
dependencyMap[label] = make(map[uint]struct{})
|
||||
}
|
||||
dependencyMap[label][row.UsableID] = struct{}{}
|
||||
}
|
||||
|
||||
labels := make([]string, 0, len(dependencyMap))
|
||||
for label := range dependencyMap {
|
||||
labels = append(labels, label)
|
||||
}
|
||||
sort.Strings(labels)
|
||||
|
||||
details := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
details = append(details, fmt.Sprintf("%s=%s", label, joinUint(sortedIDs(dependencyMap[label]))))
|
||||
}
|
||||
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"%s Transfer %s. Dependensi aktif: %s.",
|
||||
transferLayingDeleteDownstreamGuardMessage,
|
||||
transferNumber,
|
||||
strings.Join(details, ", "),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func mapTransferLayingDownstreamUsableLabel(usableType string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(usableType)) {
|
||||
case fifo.UsableKeyRecordingStock.String(), fifo.UsableKeyRecordingDepletion.String():
|
||||
return "Recording"
|
||||
case fifo.UsableKeyProjectChickin.String():
|
||||
return "Chickin"
|
||||
case fifo.UsableKeyMarketingDelivery.String():
|
||||
return "Marketing"
|
||||
case fifo.UsableKeyTransferToLayingOut.String():
|
||||
return "TransferToLaying"
|
||||
case fifo.UsableKeyStockTransferOut.String():
|
||||
return "TransferStock"
|
||||
case fifo.UsableKeyAdjustmentOut.String():
|
||||
return "Adjustment"
|
||||
default:
|
||||
return strings.ToUpper(strings.TrimSpace(usableType))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||
@@ -380,12 +381,13 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
}
|
||||
}
|
||||
weekBase := 1
|
||||
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
|
||||
weekBase = 18
|
||||
isLayingCategory := strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying))
|
||||
if isLayingCategory {
|
||||
weekBase = config.LayingWeekStart()
|
||||
}
|
||||
if req.Week < weekBase {
|
||||
if weekBase == 18 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
||||
if isLayingCategory {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
||||
}
|
||||
@@ -399,8 +401,8 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence")
|
||||
}
|
||||
if latestWeek == 0 && req.Week != weekBase {
|
||||
if weekBase == 18 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
||||
if isLayingCategory {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
||||
}
|
||||
@@ -474,7 +476,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
}); err != nil {
|
||||
s.Log.Errorf("Failed to create uniformity: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if s.DocumentSvc != nil {
|
||||
actorIDCopy := actorID
|
||||
@@ -575,12 +577,13 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
||||
}
|
||||
}
|
||||
weekBase := 1
|
||||
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
|
||||
weekBase = 18
|
||||
isLayingCategory := strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying))
|
||||
if isLayingCategory {
|
||||
weekBase = config.LayingWeekStart()
|
||||
}
|
||||
if targetWeek < weekBase {
|
||||
if weekBase == 18 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
||||
if isLayingCategory {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user