mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
init adjustment recording
This commit is contained in:
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type RecordingService interface {
|
||||
@@ -365,6 +366,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
}
|
||||
}
|
||||
|
||||
var stockOwnerProjectFlockKandangID *uint
|
||||
if len(req.Stocks) > 0 {
|
||||
stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfk, recordTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
day, err := s.computeRecordingDay(ctx, pfk.Id, recordTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -441,12 +450,12 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
return err
|
||||
}
|
||||
|
||||
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
||||
stockDesired := resetStockQuantitiesForFIFO(mappedStocks)
|
||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||
return err
|
||||
}
|
||||
mappedStocks := recordingutil.MapStocks(createdRecording.Id, stockOwnerProjectFlockKandangID, req.Stocks)
|
||||
stockDesired := resetStockQuantitiesForFIFO(mappedStocks)
|
||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range mappedStocks {
|
||||
if i >= len(stockDesired) {
|
||||
@@ -510,10 +519,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
s.Log.Errorf("Failed to compute recording metrics: %+v", err)
|
||||
return err
|
||||
}
|
||||
if err := s.recalculateFrom(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to recalculate recordings after create: %+v", err)
|
||||
return err
|
||||
}
|
||||
if err := s.recalculateFrom(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to recalculate recordings after create: %+v", err)
|
||||
return err
|
||||
}
|
||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to sync farm depreciation manual input after create: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionCreated
|
||||
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
|
||||
@@ -574,8 +587,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return err
|
||||
}
|
||||
|
||||
recordingEntity = recording
|
||||
pfkForRoute := recordingEntity.ProjectFlockKandang
|
||||
recordingEntity = recording
|
||||
pfkForRoute := recordingEntity.ProjectFlockKandang
|
||||
if pfkForRoute == nil || pfkForRoute.Id == 0 {
|
||||
fetchedPfk, fetchErr := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recordingEntity.ProjectFlockKandangId)
|
||||
if fetchErr != nil {
|
||||
@@ -586,35 +599,43 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return fetchErr
|
||||
}
|
||||
pfkForRoute = fetchedPfk
|
||||
}
|
||||
routePayload := buildRecordingRoutePayloadFromUpdate(req)
|
||||
if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.tryAutoExecuteTransferForRecordingCreate(c, pfkForRoute, recordingEntity.RecordDatetime); err != nil {
|
||||
return err
|
||||
}
|
||||
routePayload := buildRecordingRoutePayloadFromUpdate(req)
|
||||
if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasStockChanges := req.Stocks != nil
|
||||
hasDepletionChanges := req.Depletions != nil
|
||||
hasEggChanges := req.Eggs != nil
|
||||
|
||||
var existingStocks []entity.RecordingStock
|
||||
var existingDepletions []entity.RecordingDepletion
|
||||
var existingEggs []entity.RecordingEgg
|
||||
var mappedDepletions []entity.RecordingDepletion
|
||||
var existingStocks []entity.RecordingStock
|
||||
var existingDepletions []entity.RecordingDepletion
|
||||
var existingEggs []entity.RecordingEgg
|
||||
var mappedDepletions []entity.RecordingDepletion
|
||||
var stockOwnerProjectFlockKandangID *uint
|
||||
|
||||
note := recordingutil.RecordingNote("Edit", recordingEntity.Id)
|
||||
|
||||
if hasStockChanges {
|
||||
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||
return err
|
||||
}
|
||||
if hasStockChanges {
|
||||
stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfkForRoute, recordingEntity.RecordDatetime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||
return err
|
||||
}
|
||||
existingUsage := recordingutil.StockUsageByWarehouse(existingStocks)
|
||||
incomingUsage := recordingutil.StockUsageByWarehouseReq(req.Stocks)
|
||||
match := recordingutil.FloatMapsEqual(existingUsage, incomingUsage)
|
||||
if match {
|
||||
hasStockChanges = false
|
||||
} else {
|
||||
match := recordingutil.FloatMapsEqual(existingUsage, incomingUsage) && recordingStocksAllOwnedBy(existingStocks, stockOwnerProjectFlockKandangID)
|
||||
if match {
|
||||
hasStockChanges = false
|
||||
} else {
|
||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -622,11 +643,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.reflowSyncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, note, actorID); err != nil {
|
||||
return err
|
||||
if err := s.reflowSyncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, stockOwnerProjectFlockKandangID, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasDepletionChanges {
|
||||
existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
|
||||
@@ -788,16 +809,22 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return err
|
||||
}
|
||||
|
||||
if hasStockChanges || hasDepletionChanges || hasEggChanges {
|
||||
if hasStockChanges || hasDepletionChanges || hasEggChanges {
|
||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||
return err
|
||||
}
|
||||
if err := s.recalculateFrom(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to recalculate recordings after update: %+v", err)
|
||||
return err
|
||||
if err := s.recalculateFrom(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to recalculate recordings after update: %+v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if hasStockChanges {
|
||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to sync farm depreciation manual input after update: %+v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionUpdated
|
||||
actorID := recordingEntity.CreatedBy
|
||||
@@ -1055,11 +1082,15 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
||||
if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err)
|
||||
return err
|
||||
}
|
||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
||||
s.Log.Errorf("Failed to sync farm depreciation manual input after delete: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -1435,12 +1466,13 @@ func (s *recordingService) tryAutoExecuteTransferForRecordingCreate(c *fiber.Ctx
|
||||
return nil
|
||||
}
|
||||
|
||||
businessDate := recordDate
|
||||
physicalMoveDate := transferPhysicalMoveDate(transfer)
|
||||
if physicalMoveDate.IsZero() || recordDate.Before(physicalMoveDate) {
|
||||
return nil
|
||||
if !physicalMoveDate.IsZero() && businessDate.Before(physicalMoveDate) {
|
||||
businessDate = physicalMoveDate
|
||||
}
|
||||
|
||||
if _, err := s.TransferLayingSvc.ExecuteWithBusinessDate(c, transfer.Id, recordDate); err != nil {
|
||||
if _, err := s.TransferLayingSvc.ExecuteWithBusinessDate(c, transfer.Id, businessDate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1453,102 +1485,10 @@ func (s *recordingService) enforceTransferRecordingRoute(
|
||||
recordTime time.Time,
|
||||
payload recordingRoutePayload,
|
||||
) 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")
|
||||
}
|
||||
|
||||
physicalMoveDate, economicCutoffDate := transferRecordingWindow(transfer)
|
||||
if physicalMoveDate.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if recordDate.Before(physicalMoveDate) {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Recording kandang laying hanya bisa dimulai pada %s (tanggal pindah fisik). Sebelumnya gunakan kandang growing", physicalMoveDate.Format("2006-01-02")),
|
||||
)
|
||||
}
|
||||
|
||||
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Transfer laying %s dengan tanggal pindah fisik %s belum dieksekusi. Eksekusi transfer terlebih dahulu", transfer.TransferNumber, physicalMoveDate.Format("2006-01-02")),
|
||||
)
|
||||
}
|
||||
|
||||
if recordDate.Before(economicCutoffDate) && payload.StockCount > 0 {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"Periode transisi transfer laying %s (%s s.d. %s): input PAKAN/OVK harus dicatat di kandang growing hingga %s. Recording kandang laying pada periode ini hanya untuk deplesi (dan telur bila ada).",
|
||||
transfer.TransferNumber,
|
||||
physicalMoveDate.Format("2006-01-02"),
|
||||
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
||||
economicCutoffDate.AddDate(0, 0, -1).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")
|
||||
}
|
||||
|
||||
physicalMoveDate, economicCutoffDate := transferRecordingWindow(transfer)
|
||||
if physicalMoveDate.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if recordDate.Before(physicalMoveDate) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Transfer laying %s sudah memasuki tanggal pindah fisik %s namun belum dieksekusi. Eksekusi transfer lalu lakukan recording transisi sesuai aturan", transfer.TransferNumber, physicalMoveDate.Format("2006-01-02")),
|
||||
)
|
||||
}
|
||||
|
||||
if !recordDate.Before(economicCutoffDate) {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Recording kandang growing hanya diperbolehkan sampai %s. Gunakan kandang laying mulai %s", economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"), economicCutoffDate.Format("2006-01-02")),
|
||||
)
|
||||
}
|
||||
|
||||
if payload.DepletionCount > 0 {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"Periode transisi transfer laying %s (%s s.d. %s): deplesi harus dicatat di kandang laying tujuan agar mapping tidak ambigu. Kandang growing pada periode ini hanya untuk PAKAN/OVK.",
|
||||
transfer.TransferNumber,
|
||||
physicalMoveDate.Format("2006-01-02"),
|
||||
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_ = ctx
|
||||
_ = pfk
|
||||
_ = recordTime
|
||||
_ = payload
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1658,6 +1598,356 @@ func boolPtr(value bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
func recordingStocksAllOwnedBy(stocks []entity.RecordingStock, owner *uint) bool {
|
||||
for _, stock := range stocks {
|
||||
if !uintPtrEqual(stock.ProjectFlockKandangId, owner) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func uintPtrEqual(a *uint, b *uint) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func (s *recordingService) resolveRecordingStockOwnerProjectFlockKandangID(
|
||||
ctx context.Context,
|
||||
pfk *entity.ProjectFlockKandang,
|
||||
recordTime time.Time,
|
||||
) (*uint, error) {
|
||||
if pfk == nil || pfk.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||
if category == "" {
|
||||
loaded, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, pfk.Id)
|
||||
if err == nil && loaded != nil {
|
||||
pfk = loaded
|
||||
category = strings.ToUpper(strings.TrimSpace(loaded.ProjectFlock.Category))
|
||||
}
|
||||
}
|
||||
|
||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||
owner := pfk.Id
|
||||
return &owner, nil
|
||||
}
|
||||
|
||||
if s.TransferLayingRepo == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
transfer, err := s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
s.Log.Errorf("Failed to resolve transfer laying for recording stock owner (target_pfk=%d): %+v", pfk.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan atribusi recording stock")
|
||||
}
|
||||
if transfer == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sourceProjectFlockKandangID, err := s.resolveTransferSourceProjectFlockKandangID(ctx, transfer)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to resolve transfer source kandang for transfer %d: %+v", transfer.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan sumber kandang growing")
|
||||
}
|
||||
if sourceProjectFlockKandangID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sourceChickinDate, err := s.getEarliestChickInDateByProjectFlockKandangID(ctx, sourceProjectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to resolve earliest chick-in date for source kandang %d: %+v", sourceProjectFlockKandangID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan umur ayam dari growing")
|
||||
}
|
||||
if sourceChickinDate == nil || sourceChickinDate.IsZero() {
|
||||
owner := sourceProjectFlockKandangID
|
||||
return &owner, nil
|
||||
}
|
||||
|
||||
thresholdDay, err := s.resolveLayingDepreciationThresholdDay(ctx, pfk)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to resolve laying threshold day for kandang %d: %+v", pfk.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menentukan standar umur laying")
|
||||
}
|
||||
if thresholdDay <= 0 {
|
||||
thresholdDay = commonSvc.DepreciationStartAgeDay(resolveHouseType(pfk))
|
||||
}
|
||||
if thresholdDay <= 0 {
|
||||
thresholdDay = commonSvc.DepreciationStartAgeDay("close_house")
|
||||
}
|
||||
|
||||
recordDate := normalizeDateOnlyUTC(recordTime)
|
||||
chickinDate := normalizeDateOnlyUTC(*sourceChickinDate)
|
||||
ageDay := commonSvc.FlockAgeDay(chickinDate, recordDate)
|
||||
if ageDay < thresholdDay {
|
||||
owner := sourceProjectFlockKandangID
|
||||
return &owner, nil
|
||||
}
|
||||
|
||||
owner := pfk.Id
|
||||
return &owner, nil
|
||||
}
|
||||
|
||||
func resolveHouseType(pfk *entity.ProjectFlockKandang) string {
|
||||
if pfk == nil || pfk.Kandang.HouseType == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*pfk.Kandang.HouseType)
|
||||
}
|
||||
|
||||
func (s *recordingService) resolveLayingDepreciationThresholdDay(ctx context.Context, pfk *entity.ProjectFlockKandang) (int, error) {
|
||||
houseType := commonSvc.NormalizeDepreciationHouseType(resolveHouseType(pfk))
|
||||
if houseType == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var row struct {
|
||||
StandardWeek int `gorm:"column:standard_week"`
|
||||
}
|
||||
err := s.Repository.DB().WithContext(ctx).
|
||||
Table("house_depreciation_standards").
|
||||
Select("standard_week").
|
||||
Where("house_type::text = ?", houseType).
|
||||
Where("standard_week > 0").
|
||||
Order("effective_date DESC NULLS LAST").
|
||||
Order("id DESC").
|
||||
Limit(1).
|
||||
Scan(&row).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if row.StandardWeek <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return (row.StandardWeek * 7) + 1, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) resolveTransferSourceProjectFlockKandangID(ctx context.Context, transfer *entity.LayingTransfer) (uint, error) {
|
||||
if transfer == nil || transfer.Id == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if transfer.SourceProjectFlockKandangId != nil && *transfer.SourceProjectFlockKandangId != 0 {
|
||||
return *transfer.SourceProjectFlockKandangId, nil
|
||||
}
|
||||
|
||||
var row struct {
|
||||
SourceProjectFlockKandangID uint `gorm:"column:source_project_flock_kandang_id"`
|
||||
}
|
||||
err := s.Repository.DB().WithContext(ctx).
|
||||
Table("laying_transfer_sources").
|
||||
Select("source_project_flock_kandang_id").
|
||||
Where("laying_transfer_id = ?", transfer.Id).
|
||||
Where("deleted_at IS NULL").
|
||||
Where("source_project_flock_kandang_id > 0").
|
||||
Order("id ASC").
|
||||
Limit(1).
|
||||
Take(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return row.SourceProjectFlockKandangID, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getEarliestChickInDateByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*time.Time, error) {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var row struct {
|
||||
ChickInDate *time.Time `gorm:"column:chick_in_date"`
|
||||
}
|
||||
err := s.Repository.DB().WithContext(ctx).
|
||||
Table("project_chickins").
|
||||
Select("MIN(chick_in_date) AS chick_in_date").
|
||||
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("deleted_at IS NULL").
|
||||
Scan(&row).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row.ChickInDate, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) syncFarmDepreciationManualInputFromRecordingStocks(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
projectFlockKandangID uint,
|
||||
fallbackCutoverDate time.Time,
|
||||
) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetDB := s.Repository.DB()
|
||||
if tx != nil {
|
||||
targetDB = tx
|
||||
}
|
||||
|
||||
projectFlockID, err := s.resolveProjectFlockIDByProjectFlockKandangID(ctx, targetDB, projectFlockKandangID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if projectFlockID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalCost, err := s.sumNoTransferRecordingStockCostByProjectFlockID(ctx, targetDB, projectFlockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existing, err := s.getFarmDepreciationManualInputByProjectFlockID(ctx, targetDB, projectFlockID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cutoverDate := normalizeDateOnlyUTC(fallbackCutoverDate)
|
||||
if existing != nil && !existing.CutoverDate.IsZero() {
|
||||
cutoverDate = normalizeDateOnlyUTC(existing.CutoverDate)
|
||||
}
|
||||
if cutoverDate.IsZero() {
|
||||
earliestDate, dateErr := s.getEarliestNoTransferRecordingDateByProjectFlockID(ctx, targetDB, projectFlockID)
|
||||
if dateErr != nil {
|
||||
return dateErr
|
||||
}
|
||||
if earliestDate != nil && !earliestDate.IsZero() {
|
||||
cutoverDate = normalizeDateOnlyUTC(*earliestDate)
|
||||
}
|
||||
}
|
||||
if cutoverDate.IsZero() {
|
||||
cutoverDate = normalizeDateOnlyUTC(time.Now().UTC())
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
row := entity.FarmDepreciationManualInput{
|
||||
ProjectFlockId: projectFlockID,
|
||||
TotalCost: totalCost,
|
||||
CutoverDate: cutoverDate,
|
||||
}
|
||||
if existing != nil {
|
||||
row.Note = existing.Note
|
||||
}
|
||||
|
||||
return targetDB.WithContext(ctx).
|
||||
Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "project_flock_id"}},
|
||||
DoUpdates: clause.Assignments(map[string]any{
|
||||
"total_cost": row.TotalCost,
|
||||
"cutover_date": row.CutoverDate,
|
||||
"updated_at": now,
|
||||
}),
|
||||
}).
|
||||
Create(&row).Error
|
||||
}
|
||||
|
||||
func (s *recordingService) resolveProjectFlockIDByProjectFlockKandangID(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (uint, error) {
|
||||
var row struct {
|
||||
ProjectFlockID uint `gorm:"column:project_flock_id"`
|
||||
}
|
||||
err := db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Select("project_flock_id").
|
||||
Where("id = ?", projectFlockKandangID).
|
||||
Take(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return row.ProjectFlockID, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) sumNoTransferRecordingStockCostByProjectFlockID(ctx context.Context, db *gorm.DB, projectFlockID uint) (float64, error) {
|
||||
if projectFlockID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var total float64
|
||||
err := db.WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)").
|
||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins(
|
||||
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||
fifo.UsableKeyRecordingStock.String(),
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("rs.project_flock_kandang_id IS NULL").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getFarmDepreciationManualInputByProjectFlockID(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
projectFlockID uint,
|
||||
) (*entity.FarmDepreciationManualInput, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var row entity.FarmDepreciationManualInput
|
||||
err := db.WithContext(ctx).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Take(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getEarliestNoTransferRecordingDateByProjectFlockID(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
projectFlockID uint,
|
||||
) (*time.Time, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var row struct {
|
||||
RecordDate *time.Time `gorm:"column:record_date"`
|
||||
}
|
||||
err := db.WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select("MIN(r.record_datetime) AS record_date").
|
||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("rs.project_flock_kandang_id IS NULL").
|
||||
Scan(&row).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row.RecordDate, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) resolveEggRequestsToFarmWarehouses(
|
||||
ctx context.Context,
|
||||
pfk *entity.ProjectFlockKandang,
|
||||
@@ -2588,6 +2878,7 @@ func (s *recordingService) reflowSyncRecordingStocks(
|
||||
recordingID uint,
|
||||
existing []entity.RecordingStock,
|
||||
incoming []validation.Stock,
|
||||
ownerProjectFlockKandangID *uint,
|
||||
note string,
|
||||
actorID uint,
|
||||
) error {
|
||||
@@ -2608,21 +2899,32 @@ func (s *recordingService) reflowSyncRecordingStocks(
|
||||
if len(list) > 0 {
|
||||
stock = list[0]
|
||||
existingByWarehouse[item.ProductWarehouseId] = list[1:]
|
||||
} else {
|
||||
zero := 0.0
|
||||
stock = entity.RecordingStock{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
UsageQty: &zero,
|
||||
PendingQty: &zero,
|
||||
} else {
|
||||
zero := 0.0
|
||||
stock = entity.RecordingStock{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
ProjectFlockKandangId: ownerProjectFlockKandangID,
|
||||
UsageQty: &zero,
|
||||
PendingQty: &zero,
|
||||
}
|
||||
if err := s.Repository.CreateStock(tx, &stock); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.Repository.CreateStock(tx, &stock); err != nil {
|
||||
return err
|
||||
stock.ProjectFlockKandangId = ownerProjectFlockKandangID
|
||||
if stock.Id != 0 {
|
||||
if err := tx.Model(&entity.RecordingStock{}).
|
||||
Where("id = ?", stock.Id).
|
||||
Updates(map[string]any{
|
||||
"project_flock_kandang_id": ownerProjectFlockKandangID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
desired := item.Qty
|
||||
stock.UsageQty = &desired
|
||||
desired := item.Qty
|
||||
stock.UsageQty = &desired
|
||||
zero := 0.0
|
||||
stock.PendingQty = &zero
|
||||
stocksToApply = append(stocksToApply, stock)
|
||||
|
||||
@@ -231,6 +231,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.validateTargetSourceLineage(c.Context(), sourceDetail.ProjectFlockKandangId, targetKandangIDs, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transferDate, err := utils.ParseDateString(req.TransferDate)
|
||||
if err != nil {
|
||||
@@ -451,6 +454,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.validateTargetSourceLineage(c.Context(), sourceDetail.ProjectFlockKandangId, targetKandangIDs, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transferDate, err := time.Parse("2006-01-02", req.TransferDate)
|
||||
if err != nil {
|
||||
@@ -1611,6 +1617,80 @@ func (s *transferLayingService) validateKandangOwnership(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *transferLayingService) validateTargetSourceLineage(
|
||||
ctx context.Context,
|
||||
sourceProjectFlockKandangID uint,
|
||||
targetKandangIDs []uint,
|
||||
excludeTransferID uint,
|
||||
) error {
|
||||
if sourceProjectFlockKandangID == 0 || len(targetKandangIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[uint]struct{}, len(targetKandangIDs))
|
||||
for _, targetKandangID := range targetKandangIDs {
|
||||
if targetKandangID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[targetKandangID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[targetKandangID] = struct{}{}
|
||||
|
||||
existingTransfer, err := s.Repository.GetLatestApprovedByTargetKandang(ctx, targetKandangID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
s.Log.Errorf("Failed to validate transfer lineage for target kandang %d: %+v", targetKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi sumber transfer ke laying")
|
||||
}
|
||||
if existingTransfer == nil {
|
||||
continue
|
||||
}
|
||||
if excludeTransferID != 0 && existingTransfer.Id == excludeTransferID {
|
||||
continue
|
||||
}
|
||||
|
||||
existingSourceID := uint(0)
|
||||
if existingTransfer.SourceProjectFlockKandangId != nil && *existingTransfer.SourceProjectFlockKandangId != 0 {
|
||||
existingSourceID = *existingTransfer.SourceProjectFlockKandangId
|
||||
}
|
||||
if existingSourceID == 0 && s.LayingTransferSourceRepo != nil {
|
||||
sources, sourceErr := s.LayingTransferSourceRepo.GetByLayingTransferId(ctx, existingTransfer.Id)
|
||||
if sourceErr != nil {
|
||||
s.Log.Errorf("Failed to resolve transfer sources for lineage validation transfer=%d: %+v", existingTransfer.Id, sourceErr)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi sumber transfer ke laying")
|
||||
}
|
||||
for _, source := range sources {
|
||||
if source.SourceProjectFlockKandangId != 0 {
|
||||
existingSourceID = source.SourceProjectFlockKandangId
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if existingSourceID == 0 {
|
||||
continue
|
||||
}
|
||||
if existingSourceID == sourceProjectFlockKandangID {
|
||||
continue
|
||||
}
|
||||
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"Kandang tujuan %d sudah memiliki lineage sumber kandang %d dari transfer %s. Tidak boleh ganti ke sumber kandang %d.",
|
||||
targetKandangID,
|
||||
existingSourceID,
|
||||
existingTransfer.TransferNumber,
|
||||
sourceProjectFlockKandangID,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFlockID uint) (map[uint]float64, error) {
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
|
||||
Reference in New Issue
Block a user