mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
dev: initiate adjustment recording and trf to laying
This commit is contained in:
@@ -297,6 +297,9 @@ func (r *HppRepositoryImpl) GetTransferSourceSummary(ctx context.Context, projec
|
|||||||
Table("laying_transfer_targets AS ltt").
|
Table("laying_transfer_targets AS ltt").
|
||||||
Select("lt.from_project_flock_id AS project_flock_id, COALESCE(SUM(ltt.total_qty), 0) AS total_qty").
|
Select("lt.from_project_flock_id AS project_flock_id, COALESCE(SUM(ltt.total_qty), 0) AS total_qty").
|
||||||
Joins("JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id").
|
Joins("JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id").
|
||||||
|
Where("lt.deleted_at IS NULL").
|
||||||
|
Where("ltt.deleted_at IS NULL").
|
||||||
|
Where("lt.executed_at IS NOT NULL").
|
||||||
Where("ltt.target_project_flock_kandang_id = ?", projectFlockKandangId).
|
Where("ltt.target_project_flock_kandang_id = ?", projectFlockKandangId).
|
||||||
Group("lt.from_project_flock_id").
|
Group("lt.from_project_flock_id").
|
||||||
Scan(&summary).Error
|
Scan(&summary).Error
|
||||||
|
|||||||
+60
-54
@@ -22,60 +22,61 @@ type SSOClientConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
IsProd bool
|
IsProd bool
|
||||||
AppHost string
|
AppHost string
|
||||||
Version string
|
Version string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
AppPort int
|
AppPort int
|
||||||
DBHost string
|
DBHost string
|
||||||
DBUser string
|
DBUser string
|
||||||
DBPassword string
|
DBPassword string
|
||||||
DBName string
|
DBName string
|
||||||
DBPort int
|
DBPort int
|
||||||
DBSSLMode string
|
DBSSLMode string
|
||||||
DBSSLRootCert string
|
DBSSLRootCert string
|
||||||
DBSSLCert string
|
DBSSLCert string
|
||||||
DBSSLKey string
|
DBSSLKey string
|
||||||
JWTSecret string
|
JWTSecret string
|
||||||
JWTAccessExp int
|
JWTAccessExp int
|
||||||
JWTRefreshExp int
|
JWTRefreshExp int
|
||||||
JWTResetPasswordExp int
|
JWTResetPasswordExp int
|
||||||
JWTVerifyEmailExp int
|
JWTVerifyEmailExp int
|
||||||
RedisURL string
|
RedisURL string
|
||||||
CORSAllowOrigins []string
|
CORSAllowOrigins []string
|
||||||
CORSAllowMethods []string
|
CORSAllowMethods []string
|
||||||
CORSAllowHeaders []string
|
CORSAllowHeaders []string
|
||||||
CORSExposeHeaders []string
|
CORSExposeHeaders []string
|
||||||
CORSAllowCredentials bool
|
CORSAllowCredentials bool
|
||||||
CORSMaxAge int
|
CORSMaxAge int
|
||||||
SSOIssuer string
|
SSOIssuer string
|
||||||
SSOJWKSURL string
|
SSOJWKSURL string
|
||||||
SSOAllowedAudiences []string
|
SSOAllowedAudiences []string
|
||||||
SSOAuthorizeURL string
|
SSOAuthorizeURL string
|
||||||
SSOTokenURL string
|
SSOTokenURL string
|
||||||
SSOGetMeURL string
|
SSOGetMeURL string
|
||||||
SSOPortalURL string
|
SSOPortalURL string
|
||||||
SSOClients map[string]SSOClientConfig
|
SSOClients map[string]SSOClientConfig
|
||||||
SSOAccessCookieName string
|
SSOAccessCookieName string
|
||||||
SSORefreshCookieName string
|
SSORefreshCookieName string
|
||||||
SSOCookieDomain string
|
SSOCookieDomain string
|
||||||
SSOCookieSecure bool
|
SSOCookieSecure bool
|
||||||
SSOCookieSameSite string
|
SSOCookieSameSite string
|
||||||
SSOAccessTokenMaxBytes int
|
SSOAccessTokenMaxBytes int
|
||||||
SSOTokenBlacklistPrefix string
|
SSOTokenBlacklistPrefix string
|
||||||
SSOPKCETTL time.Duration
|
SSOPKCETTL time.Duration
|
||||||
SSOUserSyncDrift time.Duration
|
SSOUserSyncDrift time.Duration
|
||||||
SSOUserSyncNonceTTL time.Duration
|
SSOUserSyncNonceTTL time.Duration
|
||||||
SSOUserSyncMaxBodyBytes int
|
SSOUserSyncMaxBodyBytes int
|
||||||
S3Endpoint string
|
S3Endpoint string
|
||||||
S3Region string
|
S3Region string
|
||||||
S3Bucket string
|
S3Bucket string
|
||||||
S3AccessKey string
|
S3AccessKey string
|
||||||
S3SecretKey string
|
S3SecretKey string
|
||||||
S3ForcePathStyle bool
|
S3ForcePathStyle bool
|
||||||
S3PublicBaseURL string
|
S3PublicBaseURL string
|
||||||
S3EnvPrefix string
|
S3EnvPrefix string
|
||||||
S3DocumentKeyPrefix string
|
S3DocumentKeyPrefix string
|
||||||
|
TransferToLayingGrowingMaxWeek int
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -117,6 +118,11 @@ func init() {
|
|||||||
// Redis
|
// Redis
|
||||||
RedisURL = viper.GetString("REDIS_URL")
|
RedisURL = viper.GetString("REDIS_URL")
|
||||||
|
|
||||||
|
TransferToLayingGrowingMaxWeek = viper.GetInt("TRANSFER_TO_LAYING_GROWING_MAX_WEEK")
|
||||||
|
if TransferToLayingGrowingMaxWeek <= 0 {
|
||||||
|
TransferToLayingGrowingMaxWeek = 19
|
||||||
|
}
|
||||||
|
|
||||||
// Object storage
|
// Object storage
|
||||||
S3Endpoint = strings.TrimSpace(viper.GetString("S3_ENDPOINT"))
|
S3Endpoint = strings.TrimSpace(viper.GetString("S3_ENDPOINT"))
|
||||||
S3Region = strings.TrimSpace(viper.GetString("S3_REGION"))
|
S3Region = strings.TrimSpace(viper.GetString("S3_REGION"))
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_executed_by;
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_executed_at;
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_effective_move_date;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_laying_transfers_executed_by;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS executed_by,
|
||||||
|
DROP COLUMN IF EXISTS executed_at,
|
||||||
|
DROP COLUMN IF EXISTS effective_move_date;
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN IF NOT EXISTS effective_move_date DATE,
|
||||||
|
ADD COLUMN IF NOT EXISTS executed_at TIMESTAMPTZ,
|
||||||
|
ADD COLUMN IF NOT EXISTS executed_by BIGINT;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_laying_transfers_executed_by'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_transfers_executed_by
|
||||||
|
FOREIGN KEY (executed_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_effective_move_date
|
||||||
|
ON laying_transfers(effective_move_date);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_executed_at
|
||||||
|
ON laying_transfers(executed_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_executed_by
|
||||||
|
ON laying_transfers(executed_by);
|
||||||
|
|
||||||
|
-- Backfill historical approved transfers. Before deferred execution,
|
||||||
|
-- approved transfers were executed immediately during approval.
|
||||||
|
UPDATE laying_transfers lt
|
||||||
|
SET
|
||||||
|
effective_move_date = COALESCE(lt.effective_move_date, lt.transfer_date),
|
||||||
|
executed_at = COALESCE(lt.executed_at, lt.updated_at),
|
||||||
|
executed_by = COALESCE(lt.executed_by, lt.created_by)
|
||||||
|
WHERE (
|
||||||
|
SELECT a.action
|
||||||
|
FROM approvals a
|
||||||
|
WHERE a.approvable_type = 'TRANSFER_TO_LAYINGS'
|
||||||
|
AND a.approvable_id = lt.id
|
||||||
|
ORDER BY a.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
) = 'APPROVED';
|
||||||
@@ -12,16 +12,20 @@ type LayingTransfer struct {
|
|||||||
FromProjectFlockId uint `gorm:"not null"`
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
ToProjectFlockId uint `gorm:"not null"`
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
TransferDate time.Time `gorm:"type:date;not null"`
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
|
EffectiveMoveDate *time.Time `gorm:"type:date"`
|
||||||
|
ExecutedAt *time.Time `gorm:"type:timestamptz"`
|
||||||
|
ExecutedBy *uint `gorm:"index"`
|
||||||
Notes string `gorm:"type:text"`
|
Notes string `gorm:"type:text"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
ExecutedUser *User `gorm:"foreignKey:ExecutedBy;references:Id"`
|
||||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
sProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
sProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||||
|
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
@@ -48,6 +49,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
productRepo := rProduct.NewProductRepository(db)
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
chickinRepo := rChickin.NewChickinRepository(db)
|
chickinRepo := rChickin.NewChickinRepository(db)
|
||||||
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
|
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
|
||||||
|
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
||||||
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
||||||
@@ -184,6 +186,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
productionStandardService,
|
productionStandardService,
|
||||||
projectFlockService,
|
projectFlockService,
|
||||||
chickinService,
|
chickinService,
|
||||||
|
transferLayingRepo,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
sProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
sProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
|
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -52,6 +53,7 @@ type recordingService struct {
|
|||||||
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
||||||
ProjectFlockSvc sProjectFlock.ProjectflockService
|
ProjectFlockSvc sProjectFlock.ProjectflockService
|
||||||
ChickinSvc sChickin.ChickinService
|
ChickinSvc sChickin.ChickinService
|
||||||
|
TransferLayingRepo rTransferLaying.TransferLayingRepository
|
||||||
FifoSvc commonSvc.FifoService
|
FifoSvc commonSvc.FifoService
|
||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,7 @@ func NewRecordingService(
|
|||||||
productionStandardSvc sProductionStandard.ProductionStandardService,
|
productionStandardSvc sProductionStandard.ProductionStandardService,
|
||||||
projectFlockSvc sProjectFlock.ProjectflockService,
|
projectFlockSvc sProjectFlock.ProjectflockService,
|
||||||
chickinSvc sChickin.ChickinService,
|
chickinSvc sChickin.ChickinService,
|
||||||
|
transferLayingRepo rTransferLaying.TransferLayingRepository,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) RecordingService {
|
) RecordingService {
|
||||||
return &recordingService{
|
return &recordingService{
|
||||||
@@ -82,6 +85,7 @@ func NewRecordingService(
|
|||||||
ProductionStandardSvc: productionStandardSvc,
|
ProductionStandardSvc: productionStandardSvc,
|
||||||
ProjectFlockSvc: projectFlockSvc,
|
ProjectFlockSvc: projectFlockSvc,
|
||||||
ChickinSvc: chickinSvc,
|
ChickinSvc: chickinSvc,
|
||||||
|
TransferLayingRepo: transferLayingRepo,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
StockLogRepo: stockLogRepo,
|
StockLogRepo: stockLogRepo,
|
||||||
}
|
}
|
||||||
@@ -287,6 +291,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
category := strings.ToUpper(pfk.ProjectFlock.Category)
|
category := strings.ToUpper(pfk.ProjectFlock.Category)
|
||||||
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
||||||
|
|
||||||
|
if err := s.enforceTransferRecordingRoute(ctx, pfk, recordTime); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.ProjectFlockSvc.EnsureProjectFlockApproved(ctx, pfk.ProjectFlockId); err != nil {
|
if err := s.ProjectFlockSvc.EnsureProjectFlockApproved(ctx, pfk.ProjectFlockId); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -897,6 +905,91 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) enforceTransferRecordingRoute(
|
||||||
|
ctx context.Context,
|
||||||
|
pfk *entity.ProjectFlockKandang,
|
||||||
|
recordTime time.Time,
|
||||||
|
) error {
|
||||||
|
if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDate := normalizeDateOnlyUTC(recordTime)
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
|
||||||
|
switch category {
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
||||||
|
transfer, err := s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve approved transfer by target kandang %d: %+v", pfk.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveDate := effectiveTransferDate(transfer)
|
||||||
|
if effectiveDate.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordDate.Before(effectiveDate) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Recording kandang laying hanya bisa dimulai pada %s. Sebelumnya gunakan kandang growing", effectiveDate.Format("2006-01-02")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Transfer laying %s sudah efektif pada %s tetapi belum dieksekusi. Eksekusi transfer terlebih dahulu", transfer.TransferNumber, effectiveDate.Format("2006-01-02")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
||||||
|
transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve approved transfer by source kandang %d: %+v", pfk.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveDate := effectiveTransferDate(transfer)
|
||||||
|
if effectiveDate.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !recordDate.Before(effectiveDate) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Recording kandang growing hanya diperbolehkan sampai %s. Gunakan kandang laying mulai %s", effectiveDate.AddDate(0, 0, -1).Format("2006-01-02"), effectiveDate.Format("2006-01-02")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectiveTransferDate(transfer *entity.LayingTransfer) time.Time {
|
||||||
|
if transfer == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
|
||||||
|
return normalizeDateOnlyUTC(*transfer.EffectiveMoveDate)
|
||||||
|
}
|
||||||
|
if !transfer.TransferDate.IsZero() {
|
||||||
|
return normalizeDateOnlyUTC(transfer.TransferDate)
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
|
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
|
||||||
idSet := make(map[uint]struct{})
|
idSet := make(map[uint]struct{})
|
||||||
|
|
||||||
|
|||||||
+22
@@ -186,6 +186,28 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) Execute(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.TransferLayingService.Execute(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Execute transfer laying successfully",
|
||||||
|
Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, result.LatestApproval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
||||||
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type TransferLayingRelationDTO struct {
|
type TransferLayingRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
TransferNumber string `json:"transfer_number"`
|
TransferNumber string `json:"transfer_number"`
|
||||||
TransferDate time.Time `json:"transfer_date"`
|
TransferDate time.Time `json:"transfer_date"`
|
||||||
Notes string `json:"notes"`
|
EffectiveMoveDate *time.Time `json:"effective_move_date,omitempty"`
|
||||||
|
ExecutedAt *time.Time `json:"executed_at,omitempty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockKandangWithKandangDTO struct {
|
type ProjectFlockKandangWithKandangDTO struct {
|
||||||
@@ -47,6 +49,8 @@ type TransferLayingListDTO struct {
|
|||||||
ToProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"to_project_flock,omitempty"`
|
ToProjectFlock *projectFlockDTO.ProjectFlockRelationDTO `json:"to_project_flock,omitempty"`
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
|
ExecutedBy *uint `json:"executed_by,omitempty"`
|
||||||
|
ExecutedUser *userDTO.UserRelationDTO `json:"executed_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -88,10 +92,12 @@ type MaxTargetQtyForTransferDTO struct {
|
|||||||
|
|
||||||
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO {
|
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO {
|
||||||
return TransferLayingRelationDTO{
|
return TransferLayingRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
TransferNumber: e.TransferNumber,
|
TransferNumber: e.TransferNumber,
|
||||||
TransferDate: e.TransferDate,
|
TransferDate: e.TransferDate,
|
||||||
Notes: e.Notes,
|
EffectiveMoveDate: e.EffectiveMoveDate,
|
||||||
|
ExecutedAt: e.ExecutedAt,
|
||||||
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +196,12 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
|
|||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var executedUser *userDTO.UserRelationDTO
|
||||||
|
if e.ExecutedUser != nil && e.ExecutedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(*e.ExecutedUser)
|
||||||
|
executedUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
var approval *approvalDTO.ApprovalRelationDTO
|
var approval *approvalDTO.ApprovalRelationDTO
|
||||||
if e.LatestApproval != nil {
|
if e.LatestApproval != nil {
|
||||||
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
@@ -219,6 +231,8 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
|
|||||||
ToProjectFlock: toProjectFlock,
|
ToProjectFlock: toProjectFlock,
|
||||||
CreatedBy: e.CreatedBy,
|
CreatedBy: e.CreatedBy,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
|
ExecutedBy: e.ExecutedBy,
|
||||||
|
ExecutedUser: executedUser,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
Approval: approval,
|
Approval: approval,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
|||||||
|
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||||
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
|
|
||||||
// daftarin jadi stockable
|
// daftarin jadi stockable
|
||||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
@@ -91,6 +92,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
|||||||
warehouseRepo,
|
warehouseRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
fifoService,
|
fifoService,
|
||||||
|
fifoStockV2Service,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|||||||
+58
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ type TransferLayingRepository interface {
|
|||||||
repository.BaseRepository[entity.LayingTransfer]
|
repository.BaseRepository[entity.LayingTransfer]
|
||||||
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
|
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
|
GetLatestApprovedBySourceKandang(ctx context.Context, sourceProjectFlockKandangID uint) (*entity.LayingTransfer, error)
|
||||||
|
GetLatestApprovedByTargetKandang(ctx context.Context, targetProjectFlockKandangID uint) (*entity.LayingTransfer, error)
|
||||||
|
|
||||||
// Tambah method baru untuk query dengan filter lengkap
|
// Tambah method baru untuk query dengan filter lengkap
|
||||||
GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error)
|
GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error)
|
||||||
@@ -164,6 +167,7 @@ func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, of
|
|||||||
Preload("FromProjectFlock").
|
Preload("FromProjectFlock").
|
||||||
Preload("ToProjectFlock").
|
Preload("ToProjectFlock").
|
||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
|
Preload("ExecutedUser").
|
||||||
Preload("Sources").
|
Preload("Sources").
|
||||||
Preload("Sources.SourceProjectFlockKandang").
|
Preload("Sources.SourceProjectFlockKandang").
|
||||||
Preload("Sources.SourceProjectFlockKandang.Kandang").
|
Preload("Sources.SourceProjectFlockKandang.Kandang").
|
||||||
@@ -180,3 +184,57 @@ func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, of
|
|||||||
|
|
||||||
return records, total, nil
|
return records, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *TransferLayingRepositoryImpl) GetLatestApprovedBySourceKandang(ctx context.Context, sourceProjectFlockKandangID uint) (*entity.LayingTransfer, error) {
|
||||||
|
if sourceProjectFlockKandangID == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var transfer entity.LayingTransfer
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.LayingTransfer{}).
|
||||||
|
Joins("JOIN laying_transfer_sources lts ON lts.laying_transfer_id = laying_transfers.id AND lts.deleted_at IS NULL").
|
||||||
|
Where("lts.source_project_flock_kandang_id = ?", sourceProjectFlockKandangID).
|
||||||
|
Where("laying_transfers.deleted_at IS NULL").
|
||||||
|
Where(`(
|
||||||
|
SELECT a.action
|
||||||
|
FROM approvals a
|
||||||
|
WHERE a.approvable_type = ?
|
||||||
|
AND a.approvable_id = laying_transfers.id
|
||||||
|
ORDER BY a.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
) = ?`, string(utils.ApprovalWorkflowTransferToLaying), entity.ApprovalActionApproved).
|
||||||
|
Order("laying_transfers.id DESC").
|
||||||
|
First(&transfer).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &transfer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransferLayingRepositoryImpl) GetLatestApprovedByTargetKandang(ctx context.Context, targetProjectFlockKandangID uint) (*entity.LayingTransfer, error) {
|
||||||
|
if targetProjectFlockKandangID == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var transfer entity.LayingTransfer
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.LayingTransfer{}).
|
||||||
|
Joins("JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = laying_transfers.id AND ltt.deleted_at IS NULL").
|
||||||
|
Where("ltt.target_project_flock_kandang_id = ?", targetProjectFlockKandangID).
|
||||||
|
Where("laying_transfers.deleted_at IS NULL").
|
||||||
|
Where(`(
|
||||||
|
SELECT a.action
|
||||||
|
FROM approvals a
|
||||||
|
WHERE a.approvable_type = ?
|
||||||
|
AND a.approvable_id = laying_transfers.id
|
||||||
|
ORDER BY a.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
) = ?`, string(utils.ApprovalWorkflowTransferToLaying), entity.ApprovalActionApproved).
|
||||||
|
Order("laying_transfers.id DESC").
|
||||||
|
First(&transfer).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &transfer, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
|
|||||||
route.Patch("/:id", m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne)
|
route.Patch("/:id", m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne)
|
||||||
route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
|
route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
|
||||||
route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
|
route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
|
||||||
|
route.Post("/:id/execute", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Execute)
|
||||||
route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
|
route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
|
||||||
route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang)
|
route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang)
|
||||||
}
|
}
|
||||||
|
|||||||
+380
-139
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
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"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
@@ -34,6 +35,7 @@ type TransferLayingService interface {
|
|||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
|
||||||
|
Execute(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
|
||||||
GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
|
GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error)
|
||||||
GetMaxTargetQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (map[uint]float64, error)
|
GetMaxTargetQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (map[uint]float64, error)
|
||||||
}
|
}
|
||||||
@@ -52,8 +54,14 @@ type transferLayingService struct {
|
|||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
ApprovalService commonSvc.ApprovalService
|
ApprovalService commonSvc.ApprovalService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoSvc commonSvc.FifoService
|
||||||
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
transferToLayingFlagGroupCode = "AYAM"
|
||||||
|
transferToLayingOutFunctionCode = "TRANSFER_TO_LAYING_OUT"
|
||||||
|
)
|
||||||
|
|
||||||
func NewTransferLayingService(
|
func NewTransferLayingService(
|
||||||
repo repository.TransferLayingRepository,
|
repo repository.TransferLayingRepository,
|
||||||
layingTransferSourceRepo repository.LayingTransferSourceRepository,
|
layingTransferSourceRepo repository.LayingTransferSourceRepository,
|
||||||
@@ -65,6 +73,7 @@ func NewTransferLayingService(
|
|||||||
warehouseRepo rWarehouse.WarehouseRepository,
|
warehouseRepo rWarehouse.WarehouseRepository,
|
||||||
approvalService commonSvc.ApprovalService,
|
approvalService commonSvc.ApprovalService,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoSvc commonSvc.FifoService,
|
||||||
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) TransferLayingService {
|
) TransferLayingService {
|
||||||
return &transferLayingService{
|
return &transferLayingService{
|
||||||
@@ -81,12 +90,14 @@ func NewTransferLayingService(
|
|||||||
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
||||||
ApprovalService: approvalService,
|
ApprovalService: approvalService,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db.
|
return db.
|
||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
|
Preload("ExecutedUser").
|
||||||
Preload("FromProjectFlock").
|
Preload("FromProjectFlock").
|
||||||
Preload("ToProjectFlock").
|
Preload("ToProjectFlock").
|
||||||
Preload("Sources").
|
Preload("Sources").
|
||||||
@@ -744,13 +755,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
repoTx := s.Repository.WithTx(dbTransaction)
|
repoTx := s.Repository.WithTx(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction)
|
|
||||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||||
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
|
||||||
stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)
|
|
||||||
|
|
||||||
for _, approvableID := range approvableIDs {
|
for _, approvableID := range approvableIDs {
|
||||||
transfer, err := repoTx.GetByID(c.Context(), approvableID, nil)
|
_, err := repoTx.GetByID(c.Context(), approvableID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
|
||||||
@@ -771,148 +779,21 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if action == entity.ApprovalActionApproved {
|
if action == entity.ApprovalActionApproved {
|
||||||
|
|
||||||
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
|
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil sources transfer")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil sources transfer")
|
||||||
}
|
}
|
||||||
|
effectiveMoveDate, err := s.calculateEffectiveMoveDate(c.Context(), sources)
|
||||||
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil targets transfer")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
totalTargetQty := 0.0
|
if err := repoTx.PatchOne(c.Context(), approvableID, map[string]any{
|
||||||
for _, target := range targets {
|
"effective_move_date": effectiveMoveDate,
|
||||||
totalTargetQty += target.TotalQty
|
"executed_at": nil,
|
||||||
}
|
"executed_by": nil,
|
||||||
|
}, nil); err != nil {
|
||||||
totalSourceRequested := 0.0
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menyimpan tanggal efektif transfer laying")
|
||||||
for _, source := range sources {
|
|
||||||
totalSourceRequested += source.RequestedQty
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, source := range sources {
|
|
||||||
if source.ProductWarehouseId == nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID))
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceShare := (source.RequestedQty / totalSourceRequested) * totalTargetQty
|
|
||||||
|
|
||||||
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
|
||||||
UsableKey: fifo.UsableKeyTransferToLayingOut,
|
|
||||||
UsableID: source.Id,
|
|
||||||
ProductWarehouseID: *source.ProductWarehouseId,
|
|
||||||
Quantity: sourceShare,
|
|
||||||
AllowPending: false,
|
|
||||||
Tx: dbTransaction,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sourceRepoTx.PatchOne(c.Context(), source.Id, map[string]interface{}{
|
|
||||||
"usage_qty": source.UsageQty + consumeResult.UsageQuantity,
|
|
||||||
"pending_usage_qty": consumeResult.PendingQuantity,
|
|
||||||
}, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
|
|
||||||
}
|
|
||||||
|
|
||||||
targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare)
|
|
||||||
|
|
||||||
for i, target := range targets {
|
|
||||||
roundedQty := math.Round(targetShares[i])
|
|
||||||
if roundedQty <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mappingAllocation := &entity.StockAllocation{
|
|
||||||
StockableType: fifo.UsableKeyTransferToLayingOut.String(),
|
|
||||||
StockableId: source.Id,
|
|
||||||
UsableType: fifo.StockableKeyTransferToLayingIn.String(),
|
|
||||||
UsableId: target.Id,
|
|
||||||
ProductWarehouseId: *source.ProductWarehouseId,
|
|
||||||
Qty: roundedQty,
|
|
||||||
Status: entity.StockAllocationStatusActive,
|
|
||||||
}
|
|
||||||
if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stockLogDecrease := &entity.StockLog{
|
|
||||||
ProductWarehouseId: *source.ProductWarehouseId,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
Increase: 0,
|
|
||||||
Decrease: sourceShare,
|
|
||||||
LoggableType: string(utils.StockLogTypeTransferLaying),
|
|
||||||
LoggableId: approvableID,
|
|
||||||
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
|
|
||||||
}
|
|
||||||
stockLogs, err := stockLogRepoTx.GetByProductWarehouse(c.Context(), *source.ProductWarehouseId, 1)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to get stock logs: %+v", err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
|
||||||
}
|
|
||||||
if len(stockLogs) > 0 {
|
|
||||||
latestStockLog := stockLogs[0]
|
|
||||||
stockLogDecrease.Stock = latestStockLog.Stock - stockLogDecrease.Decrease
|
|
||||||
} else {
|
|
||||||
stockLogDecrease.Stock -= stockLogDecrease.Decrease
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, target := range targets {
|
|
||||||
if target.ProductWarehouseId == nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
|
|
||||||
}
|
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
|
||||||
_, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
|
||||||
StockableID: target.Id,
|
|
||||||
ProductWarehouseID: *target.ProductWarehouseId,
|
|
||||||
Quantity: target.TotalQty,
|
|
||||||
Note: ¬e,
|
|
||||||
Tx: dbTransaction,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
|
|
||||||
"total_qty": target.TotalQty,
|
|
||||||
}, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
|
|
||||||
}
|
|
||||||
|
|
||||||
stockLogIncrease := &entity.StockLog{
|
|
||||||
ProductWarehouseId: *target.ProductWarehouseId,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
Increase: target.TotalQty,
|
|
||||||
Decrease: 0,
|
|
||||||
LoggableType: string(utils.StockLogTypeTransferLaying),
|
|
||||||
LoggableId: approvableID,
|
|
||||||
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
|
|
||||||
}
|
|
||||||
stockLogs, err := stockLogRepoTx.GetByProductWarehouse(c.Context(), *target.ProductWarehouseId, 1)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to get stock logs: %+v", err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
|
||||||
}
|
|
||||||
if len(stockLogs) > 0 {
|
|
||||||
latestStockLog := stockLogs[0]
|
|
||||||
stockLogIncrease.Stock = latestStockLog.Stock + stockLogIncrease.Increase
|
|
||||||
} else {
|
|
||||||
stockLogIncrease.Stock += stockLogIncrease.Increase
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogIncrease, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -939,6 +820,362 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) Execute(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
|
||||||
|
if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
repoTx := s.Repository.WithTx(dbTransaction)
|
||||||
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||||
|
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
||||||
|
approvalRepoTx := commonRepo.NewApprovalRepository(dbTransaction)
|
||||||
|
|
||||||
|
transfer, err := repoTx.GetByID(c.Context(), id, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Transfer laying tidak ditemukan")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if transfer.ExecutedAt != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := approvalRepoTx.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if latestApproval == nil || latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Transfer laying harus disetujui sebelum dieksekusi")
|
||||||
|
}
|
||||||
|
|
||||||
|
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), transfer.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil sumber transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), transfer.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil target transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
if transfer.EffectiveMoveDate == nil || transfer.EffectiveMoveDate.IsZero() {
|
||||||
|
effectiveMoveDate, calcErr := s.calculateEffectiveMoveDate(c.Context(), sources)
|
||||||
|
if calcErr != nil {
|
||||||
|
return calcErr
|
||||||
|
}
|
||||||
|
if patchErr := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
|
||||||
|
"effective_move_date": effectiveMoveDate,
|
||||||
|
}, nil); patchErr != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menyimpan tanggal efektif transfer laying")
|
||||||
|
}
|
||||||
|
transfer.EffectiveMoveDate = &effectiveMoveDate
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveMoveDate := normalizeDateOnlyUTC(*transfer.EffectiveMoveDate)
|
||||||
|
today := normalizeDateOnlyUTC(time.Now().UTC())
|
||||||
|
if today.Before(effectiveMoveDate) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying baru bisa dieksekusi mulai tanggal %s", effectiveMoveDate.Format("2006-01-02")))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.executeApprovedTransferMovement(c.Context(), dbTransaction, transfer, actorID, sources, targets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
executedAt := time.Now().UTC()
|
||||||
|
if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
|
||||||
|
"executed_at": executedAt,
|
||||||
|
"executed_by": actorID,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menyimpan status eksekusi transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengeksekusi transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer, _, err := s.GetOne(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return transfer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) executeApprovedTransferMovement(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
transfer *entity.LayingTransfer,
|
||||||
|
actorID uint,
|
||||||
|
sources []entity.LayingTransferSource,
|
||||||
|
targets []entity.LayingTransferTarget,
|
||||||
|
) error {
|
||||||
|
if transfer == nil || transfer.Id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Transfer laying tidak valid")
|
||||||
|
}
|
||||||
|
if len(sources) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Transfer laying belum memiliki sumber")
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Transfer laying belum memiliki target")
|
||||||
|
}
|
||||||
|
|
||||||
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
||||||
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(tx)
|
||||||
|
targetRepoTx := repository.NewLayingTransferTargetRepository(tx)
|
||||||
|
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
||||||
|
|
||||||
|
totalTargetQty := 0.0
|
||||||
|
for _, target := range targets {
|
||||||
|
totalTargetQty += target.TotalQty
|
||||||
|
}
|
||||||
|
if totalTargetQty <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Total kuantitas target transfer laying harus lebih dari 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSourceRequested := 0.0
|
||||||
|
for _, source := range sources {
|
||||||
|
totalSourceRequested += source.RequestedQty
|
||||||
|
}
|
||||||
|
if totalSourceRequested <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Total kuantitas sumber transfer laying harus lebih dari 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, source := range sources {
|
||||||
|
if source.ProductWarehouseId == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||||
|
}
|
||||||
|
if source.RequestedQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceShare := (source.RequestedQty / totalSourceRequested) * totalTargetQty
|
||||||
|
if sourceShare <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
usageQty := 0.0
|
||||||
|
pendingQty := 0.0
|
||||||
|
if s.FifoStockV2Svc != nil {
|
||||||
|
allowPending := false
|
||||||
|
reflowResult, err := s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: transferToLayingFlagGroupCode,
|
||||||
|
ProductWarehouseID: *source.ProductWarehouseId,
|
||||||
|
Usable: commonSvc.FifoStockV2Ref{
|
||||||
|
ID: source.Id,
|
||||||
|
LegacyTypeKey: fifo.UsableKeyTransferToLayingOut.String(),
|
||||||
|
FunctionCode: transferToLayingOutFunctionCode,
|
||||||
|
},
|
||||||
|
DesiredQty: sourceShare,
|
||||||
|
AllowOverConsume: &allowPending,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal consume FIFO v2 stock: %v", err))
|
||||||
|
}
|
||||||
|
usageQty = reflowResult.Allocate.AllocatedQty
|
||||||
|
pendingQty = reflowResult.Allocate.PendingQty
|
||||||
|
} else {
|
||||||
|
consumeResult, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||||
|
UsableKey: fifo.UsableKeyTransferToLayingOut,
|
||||||
|
UsableID: source.Id,
|
||||||
|
ProductWarehouseID: *source.ProductWarehouseId,
|
||||||
|
Quantity: sourceShare,
|
||||||
|
AllowPending: false,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err))
|
||||||
|
}
|
||||||
|
usageQty = consumeResult.UsageQuantity
|
||||||
|
pendingQty = consumeResult.PendingQuantity
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sourceRepoTx.PatchOne(ctx, source.Id, map[string]any{
|
||||||
|
"usage_qty": source.UsageQty + usageQty,
|
||||||
|
"pending_usage_qty": pendingQty,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pendingQty > 0 || usageQty < sourceShare-1e-6 {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Stok sumber tidak mencukupi untuk mengeksekusi transfer laying %s", transfer.TransferNumber),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
movedQty := sourceShare
|
||||||
|
targetShares := distributeProportionalWithRounding(targets, totalTargetQty, movedQty)
|
||||||
|
for i, target := range targets {
|
||||||
|
roundedQty := math.Round(targetShares[i])
|
||||||
|
if roundedQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mappingAllocation := &entity.StockAllocation{
|
||||||
|
StockableType: fifo.UsableKeyTransferToLayingOut.String(),
|
||||||
|
StockableId: source.Id,
|
||||||
|
UsableType: fifo.StockableKeyTransferToLayingIn.String(),
|
||||||
|
UsableId: target.Id,
|
||||||
|
ProductWarehouseId: *source.ProductWarehouseId,
|
||||||
|
Qty: roundedQty,
|
||||||
|
Status: entity.StockAllocationStatusActive,
|
||||||
|
}
|
||||||
|
if err := stockAllocationRepo.CreateOne(ctx, mappingAllocation, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stockLogDecrease := &entity.StockLog{
|
||||||
|
ProductWarehouseId: *source.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: 0,
|
||||||
|
Decrease: movedQty,
|
||||||
|
LoggableType: string(utils.StockLogTypeTransferLaying),
|
||||||
|
LoggableId: transfer.Id,
|
||||||
|
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
|
||||||
|
}
|
||||||
|
stockLogs, err := stockLogRepoTx.GetByProductWarehouse(ctx, *source.ProductWarehouseId, 1)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get stock logs: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||||
|
}
|
||||||
|
if len(stockLogs) > 0 {
|
||||||
|
latestStockLog := stockLogs[0]
|
||||||
|
stockLogDecrease.Stock = latestStockLog.Stock - stockLogDecrease.Decrease
|
||||||
|
} else {
|
||||||
|
stockLogDecrease.Stock -= stockLogDecrease.Decrease
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stockLogRepoTx.CreateOne(ctx, stockLogDecrease, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
stockLogIncrease := &entity.StockLog{
|
||||||
|
ProductWarehouseId: *target.ProductWarehouseId,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: target.TotalQty,
|
||||||
|
Decrease: 0,
|
||||||
|
LoggableType: string(utils.StockLogTypeTransferLaying),
|
||||||
|
LoggableId: transfer.Id,
|
||||||
|
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
|
||||||
|
}
|
||||||
|
stockLogs, err := stockLogRepoTx.GetByProductWarehouse(ctx, *target.ProductWarehouseId, 1)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get stock logs: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||||
|
}
|
||||||
|
if len(stockLogs) > 0 {
|
||||||
|
latestStockLog := stockLogs[0]
|
||||||
|
stockLogIncrease.Stock = latestStockLog.Stock + stockLogIncrease.Increase
|
||||||
|
} else {
|
||||||
|
stockLogIncrease.Stock += stockLogIncrease.Increase
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stockLogRepoTx.CreateOne(ctx, stockLogIncrease, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) calculateEffectiveMoveDate(ctx context.Context, sources []entity.LayingTransferSource) (time.Time, error) {
|
||||||
|
if len(sources) == 0 {
|
||||||
|
return time.Time{}, fiber.NewError(fiber.StatusBadRequest, "Sumber transfer laying tidak ditemukan")
|
||||||
|
}
|
||||||
|
|
||||||
|
maxGrowingWeek := config.TransferToLayingGrowingMaxWeek
|
||||||
|
if maxGrowingWeek <= 0 {
|
||||||
|
maxGrowingWeek = 19
|
||||||
|
}
|
||||||
|
|
||||||
|
var baselineChickInDate time.Time
|
||||||
|
for _, source := range sources {
|
||||||
|
chickInDate, err := s.resolveSourceChickInDate(ctx, source.SourceProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
if baselineChickInDate.IsZero() || chickInDate.Before(baselineChickInDate) {
|
||||||
|
baselineChickInDate = chickInDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if baselineChickInDate.IsZero() {
|
||||||
|
return time.Time{}, fiber.NewError(fiber.StatusBadRequest, "Tanggal chick in sumber transfer laying tidak ditemukan")
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveMoveDate := baselineChickInDate.AddDate(0, 0, maxGrowingWeek*7)
|
||||||
|
return normalizeDateOnlyUTC(effectiveMoveDate), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) resolveSourceChickInDate(ctx context.Context, sourceProjectFlockKandangID uint) (time.Time, error) {
|
||||||
|
if sourceProjectFlockKandangID == 0 {
|
||||||
|
return time.Time{}, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sumber tidak valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, sourceProjectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var earliestChickInDate time.Time
|
||||||
|
for _, population := range populations {
|
||||||
|
if population.ProjectChickin == nil || population.ProjectChickin.ChickInDate.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
chickInDate := normalizeDateOnlyUTC(population.ProjectChickin.ChickInDate)
|
||||||
|
if earliestChickInDate.IsZero() || chickInDate.Before(earliestChickInDate) {
|
||||||
|
earliestChickInDate = chickInDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if earliestChickInDate.IsZero() {
|
||||||
|
return time.Time{}, fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Tanggal chick in untuk kandang sumber %d tidak ditemukan", sourceProjectFlockKandangID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return earliestChickInDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error {
|
func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error {
|
||||||
if transferLayingID == 0 || actorID == 0 {
|
if transferLayingID == 0 || actorID == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -1053,6 +1290,10 @@ func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFl
|
|||||||
return kandangMaxTargetQty, nil
|
return kandangMaxTargetQty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 distributeProportionalWithRounding(targets []entity.LayingTransferTarget, totalTargetQty, sourceShare float64) []float64 {
|
func distributeProportionalWithRounding(targets []entity.LayingTransferTarget, totalTargetQty, sourceShare float64) []float64 {
|
||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
return []float64{}
|
return []float64{}
|
||||||
|
|||||||
Reference in New Issue
Block a user