|
|
|
@@ -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)
|
|
|
|
|