init adjustment recording

This commit is contained in:
giovanni
2026-04-21 22:43:18 +07:00
parent 5594c27108
commit 091f706276
15 changed files with 863 additions and 185 deletions
@@ -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(),