feat(BE): fixing wrong perhitungan biaya

This commit is contained in:
aguhh18
2026-01-09 09:15:53 +07:00
parent 76e65704d7
commit b11f03dfda
5 changed files with 62 additions and 49 deletions
@@ -10,6 +10,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"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"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -914,9 +915,8 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
var rows []ActualUsageCostRow var rows []ActualUsageCostRow
// Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll) purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
purchaseStockableKey := "PURCHASE_ITEMS" transferStockableKey := fifo.StockableKeyStockTransferIn.String()
transferStockableKey := "STOCK_TRANSFER_DETAILS"
recordingQuery := db. recordingQuery := db.
Table("recordings AS r"). Table("recordings AS r").
@@ -982,7 +982,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
return nil, err return nil, err
} }
// Part 2: Get usage from project_chickins (DOC, Pullet)
chickinQuery := db. chickinQuery := db.
Table("project_chickins AS pc"). Table("project_chickins AS pc").
Select(` Select(`
@@ -1006,7 +1005,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
return nil, err return nil, err
} }
// Merge results
rows = append(rows, chickinRows...) rows = append(rows, chickinRows...)
return rows, nil return rows, nil
@@ -413,18 +413,9 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove
} }
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) { commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return err == nil, err
}},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -439,13 +430,11 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
} }
// Get actual usage cost instead of purchase items
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID) actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
} }
// Convert actual usage rows to pseudo purchase items
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows) purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
@@ -39,11 +39,12 @@ type TransferExpenseReceivingPayload struct {
} }
type groupedTransferItem struct { type groupedTransferItem struct {
detail *entity.StockTransferDetail detail *entity.StockTransferDetail
payload TransferExpenseReceivingPayload payload TransferExpenseReceivingPayload
projectFK *uint projectFK *uint
kandangID *uint kandangID *uint
totalPrice float64 totalPrice float64
shippingCostTotal float64
} }
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string { func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
@@ -83,7 +84,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
expenseIDs := make(map[uint64]struct{}) expenseIDs := make(map[uint64]struct{})
expenseNonstockIDs := make([]uint64, 0) expenseNonstockIDs := make([]uint64, 0)
// Collect expense nonstock IDs from items
for _, item := range items { for _, item := range items {
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 { if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId) expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
@@ -91,7 +92,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
} }
if len(expenseNonstockIDs) > 0 { if len(expenseNonstockIDs) > 0 {
// Get expense IDs from expense nonstocks
for _, nsID := range expenseNonstockIDs { for _, nsID := range expenseNonstockIDs {
var expenseID uint64 var expenseID uint64
if err := tx.Model(&entity.ExpenseNonstock{}). if err := tx.Model(&entity.ExpenseNonstock{}).
@@ -105,13 +106,13 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
} }
} }
// Delete expense nonstocks
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil { if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
return err return err
} }
} }
// Check remaining expense nonstocks for each expense
approvalRepoTx := commonRepo.NewApprovalRepository(tx) approvalRepoTx := commonRepo.NewApprovalRepository(tx)
for expenseID := range expenseIDs { for expenseID := range expenseIDs {
var count int64 var count int64
@@ -121,7 +122,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
return err return err
} }
// If no more expense nonstocks, delete expense and approval
if count == 0 { if count == 0 {
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil { if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
return err return err
@@ -208,7 +209,7 @@ func (b *transferExpenseBridge) createExpenseViaService(
id := uint64(*kandangID) id := uint64(*kandangID)
expenseKandangID = &id expenseKandangID = &id
} else { } else {
// For transfer, use destination warehouse location
if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 { if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense") return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense")
} }
@@ -218,13 +219,16 @@ func (b *transferExpenseBridge) createExpenseViaService(
costItems := make([]expenseValidation.CostItem, 0, len(items)) costItems := make([]expenseValidation.CostItem, 0, len(items))
for _, gi := range items { for _, gi := range items {
note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id) note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)
price := gi.detail.TotalQty
price := gi.shippingCostTotal
if gi.payload.TransportPerItem != nil { if gi.payload.TransportPerItem != nil {
price = *gi.payload.TransportPerItem price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
} }
costItems = append(costItems, expenseValidation.CostItem{ costItems = append(costItems, expenseValidation.CostItem{
NonstockID: expeditionNonstockID, NonstockID: expeditionNonstockID,
Quantity: gi.payload.DeliveredQty, Quantity: 1,
Price: price, Price: price,
Notes: note, Notes: note,
}) })
@@ -247,7 +251,7 @@ func (b *transferExpenseBridge) createExpenseViaService(
return nil, err return nil, err
} }
// Mark approvals up to Finance so latest is Manager Finance
action := entity.ApprovalActionApproved action := entity.ApprovalActionApproved
actorID := uint(transfer.CreatedBy) actorID := uint(transfer.CreatedBy)
if actorID == 0 { if actorID == 0 {
@@ -324,12 +328,14 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
ctx := c.Context() ctx := c.Context()
// Load transfer with details
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB { transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
return db. return db.
Preload("Details"). Preload("Details").
Preload("Details.Product"). Preload("Details.Product").
Preload("Details.DestProductWarehouse"). Preload("Details.DestProductWarehouse").
Preload("Details.DeliveryItems").
Preload("Details.DeliveryItems.StockTransferDelivery").
Preload("ToWarehouse") Preload("ToWarehouse")
}) })
if err != nil { if err != nil {
@@ -337,8 +343,18 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
} }
detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details)) detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details))
shippingCostMap := make(map[uint64]float64) // detailID -> ShippingCostTotal
for i := range transfer.Details { for i := range transfer.Details {
detailMap[transfer.Details[i].Id] = &transfer.Details[i] detailMap[transfer.Details[i].Id] = &transfer.Details[i]
for _, deliveryItem := range transfer.Details[i].DeliveryItems {
if deliveryItem.StockTransferDelivery != nil {
shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
break
}
}
} }
groups := make(map[string][]groupedTransferItem) groups := make(map[string][]groupedTransferItem)
@@ -379,13 +395,17 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
} }
} }
pricePerItem := detail.TotalQty
if payload.TransportPerItem != nil { shippingCostTotal := shippingCostMap[detail.Id]
pricePerItem = *payload.TransportPerItem
}
totalPrice := pricePerItem * payload.DeliveredQty totalPrice := shippingCostTotal
if payload.TransportPerItem != nil {
totalPrice = *payload.TransportPerItem * payload.DeliveredQty
}
// Group by supplier:date:warehouse
warehouseID := uint(payload.WarehouseID) warehouseID := uint(payload.WarehouseID)
if warehouseID == 0 && transfer.ToWarehouse != nil { if warehouseID == 0 && transfer.ToWarehouse != nil {
warehouseID = uint(transfer.ToWarehouse.Id) warehouseID = uint(transfer.ToWarehouse.Id)
@@ -396,11 +416,12 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
key := groupingKey(uint(supplierID), deliveredDate, warehouseID) key := groupingKey(uint(supplierID), deliveredDate, warehouseID)
groups[key] = append(groups[key], groupedTransferItem{ groups[key] = append(groups[key], groupedTransferItem{
detail: detail, detail: detail,
payload: payload, payload: payload,
projectFK: projectFK, projectFK: projectFK,
kandangID: kandangID, kandangID: kandangID,
totalPrice: totalPrice, totalPrice: totalPrice,
shippingCostTotal: shippingCostTotal,
}) })
} }
@@ -20,6 +20,7 @@ type ProjectflockRepository interface {
GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error) GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error)
GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error) GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error)
GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error) GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error)
IdExists(ctx context.Context, id uint) (bool, error)
AreaExists(ctx context.Context, id uint) (bool, error) AreaExists(ctx context.Context, id uint) (bool, error)
FcrExists(ctx context.Context, id uint) (bool, error) FcrExists(ctx context.Context, id uint) (bool, error)
ProductionStandardExists(ctx context.Context, id uint) (bool, error) ProductionStandardExists(ctx context.Context, id uint) (bool, error)
@@ -161,6 +162,10 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
) )
} }
func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) { func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.DB(), id) return repository.Exists[entity.Area](ctx, r.DB(), id)
} }
@@ -107,21 +107,21 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs) recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String() transferStockableKey := fifo.StockableKeyStockTransferIn.String()
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select(` Select(`
k.id AS kandang_id, k.id AS kandang_id,
COALESCE(SUM(CASE COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0 ELSE 0
END), 0) AS feed_cost, END), 0) AS feed_cost,
COALESCE(SUM(CASE COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0 ELSE 0
END), 0) AS ovk_cost`, END), 0) AS ovk_cost`,
utils.FlagPakan, transferStockableKey, utils.FlagPakan, utils.FlagPakan, transferStockableKey, utils.FlagPakan,
utils.FlagOVK, transferStockableKey, utils.FlagOVK). utils.FlagOVK, transferStockableKey, utils.FlagOVK).