Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/Rekapitulasi-hutang-supplier

This commit is contained in:
ragilap
2026-01-13 19:55:02 +07:00
22 changed files with 702 additions and 596 deletions
@@ -78,6 +78,36 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
})
}
func (u *ClosingController) GetOverheadByProjectFlockKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get overhead by project flock kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
@@ -108,12 +138,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
if err != nil {
return err
}
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
if err != nil {
return err
}
@@ -123,19 +148,60 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
})
}
func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan by project flock kandang successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
})
}
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
var projectFlockKandangID *uint
if kandangParam != "" {
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
projectFlockKandangID = &kandangID
}
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID)
if err != nil {
return err
}
@@ -79,10 +79,11 @@ type HppGroup struct {
}
type SummaryHpp struct {
Label string `json:"label"`
Comparison `json:"-"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
}
type HppPurchasesSection struct {
@@ -246,11 +247,9 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
summary := SummaryHpp{
Label: label,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
),
Label: label,
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
}
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
@@ -87,7 +87,7 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
return result
}
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
return PenjualanRealisasiResponseDTO{
@@ -1,6 +1,8 @@
package dto
import (
"encoding/json"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
@@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
return dto
}
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string)
@@ -82,9 +84,20 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
budgetQty := budgets[i].Qty
budgetPrice := budgets[i].Price
budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price)
// Budget division: per kandang view only
if isPerKandang && totalKandangCount > 0 {
budgetQty = budgetQty / float64(totalKandangCount)
budgetTotal = budgetTotal / float64(totalKandangCount)
}
overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice
overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal
}
for i := range realizations {
@@ -97,8 +110,40 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
}
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
qty := realizations[i].Qty
totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price)
// Farm-level expense division
if realizations[i].ExpenseNonstock.Expense != nil &&
realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil {
projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId)
if len(projectFlockIDs) > 0 {
totalKandangInAllProjects := 0
for _, pfID := range projectFlockIDs {
if count, exists := projectFlockKandangCountMap[pfID]; exists {
totalKandangInAllProjects += count
}
}
if totalKandangInAllProjects > 0 {
if isPerKandang {
qty = qty / float64(totalKandangInAllProjects)
totalAmount = totalAmount / float64(totalKandangInAllProjects)
} else {
// Overhead ALL: divide by total kandang then multiply by this project's kandang count
perKandangAmount := totalAmount / float64(totalKandangInAllProjects)
perKandangQty := qty / float64(totalKandangInAllProjects)
qty = perKandangQty * float64(totalKandangCount)
totalAmount = perKandangAmount * float64(totalKandangCount)
}
}
}
}
overheadsByNonstockID[nonstockID].ActualQuantity += qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount
if overheadsByNonstockID[nonstockID].ItemName == "" {
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
@@ -146,7 +191,26 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
}
}
// === Helper Functions ===
func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint {
if projectFlockJSON == "" {
return []uint{}
}
var projectFlocks []uint
if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil {
return []uint{}
}
return projectFlocks
}
func countProjectFlocksInJSON(projectFlockJSON string) int {
projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON)
if len(projectFlocks) == 0 {
return 1
}
return len(projectFlocks)
}
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
if nonstock != nil && nonstock.Id != 0 {
+1 -1
View File
@@ -37,7 +37,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
+3
View File
@@ -23,8 +23,10 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang)
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
@@ -32,4 +34,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
}
@@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"math"
"strconv"
@@ -32,9 +33,9 @@ import (
type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
@@ -46,6 +47,7 @@ type closingService struct {
Validate *validator.Validate
Repository repository.ClosingRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingRepo marketingRepository.MarketingRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService
@@ -56,12 +58,13 @@ type closingService struct {
RecordingRepo recordingRepository.RecordingRepository
}
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingRepo: marketingRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc,
@@ -129,24 +132,9 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
return projectFlock, nil
}
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
return db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
})
realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil {
return nil, err
}
@@ -154,16 +142,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
return []entity.MarketingDeliveryProduct{}, nil
}
filtered := make([]entity.MarketingDeliveryProduct, 0, len(realisasi))
for _, item := range realisasi {
if item.UsageQty != 0 || item.TotalWeight != 0 || item.AvgWeight != 0 ||
item.UnitPrice != 0 || item.TotalPrice != 0 {
filtered = append(filtered, item)
}
}
return filtered, nil
return realisasi, nil
}
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
@@ -379,35 +358,90 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
return statusProject, statusClosing, nil
}
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil {
return nil, err
}
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
totalKandangCount := len(projectFlockKandangs)
// Build kandang count map for farm expense division
projectFlockKandangCountMap := make(map[uint]int)
projectFlockKandangCountMap[projectFlockID] = totalKandangCount
involvedProjectFlocks := make(map[uint]bool)
for _, realization := range realizations {
if realization.ExpenseNonstock != nil &&
realization.ExpenseNonstock.Expense != nil &&
realization.ExpenseNonstock.Expense.ProjectFlockId != nil {
var projectFlockIDs []uint
if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil {
for _, pfID := range projectFlockIDs {
if pfID != projectFlockID {
involvedProjectFlocks[pfID] = true
}
}
}
}
}
for pfID := range involvedProjectFlocks {
if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil {
projectFlockKandangCountMap[pfID] = len(pfKandangs)
}
}
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
var totalChickinQty float64
for _, chickin := range chickins {
totalChickinQty += chickin.UsageQty
}
var totalDepletion float64
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
if projectFlockKandangID != nil {
for _, chickin := range chickins {
if chickin.ProjectFlockKandangId == *projectFlockKandangID {
totalChickinQty += chickin.UsageQty
}
}
var depletionResult float64
err = s.RecordingRepo.DB().WithContext(c.Context()).
Table("recording_depletions").
Select("COALESCE(SUM(recording_depletions.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID).
Scan(&depletionResult).Error
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err)
} else {
totalDepletion = depletionResult
}
} else {
for _, chickin := range chickins {
totalChickinQty += chickin.UsageQty
}
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
}
totalActualPopulation := totalChickinQty - totalDepletion
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
return &result, nil
}
@@ -98,12 +98,14 @@ func (s dashboardService) buildPerformanceStatistics(ctx context.Context, params
endDate := params.PeriodEnd
endExclusive := params.PeriodEndExclusive
hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, filter, startDate, endExclusive, endDate, location)
globalStartDate, globalEndDate, globalEndExclusive := currentPeriodDates(location)
hppCurrent, hppLast, err := s.calculateHppGlobal(ctx, globalStartDate, globalEndExclusive, globalEndDate, location)
if err != nil {
return nil, err
}
sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, filter, endDate, location)
sellingCurrent, sellingLast, err := s.calculateSellingPrice(ctx, globalEndDate, location)
if err != nil {
return nil, err
}
@@ -271,15 +273,15 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
weekFeed := weeklyFeedMap[week]
actFcr := 0.0
if weekFeed > 0 {
actFcr = weekEgg / weekFeed
if weekEgg > 0 {
actFcr = weekFeed / weekEgg
}
cumEgg += weekEgg
cumFeed += weekFeed
actFcrCum := 0.0
if cumFeed > 0 {
actFcrCum = cumEgg / cumFeed
if cumEgg > 0 {
actFcrCum = cumFeed / cumEgg
}
bodyWeightDataset = append(bodyWeightDataset, map[string]interface{}{
@@ -357,10 +359,10 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
},
"fcr": {
Series: []dto.DashboardChartSeriesDTO{
{Id: "act_fcr", Label: "Act. FCR", Unit: "%"},
{Id: "std_fcr", Label: "STD. FCR", Unit: "%"},
{Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "%"},
{Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "%"},
{Id: "act_fcr", Label: "Act. FCR", Unit: "kg/kg"},
{Id: "std_fcr", Label: "STD. FCR", Unit: "kg/kg"},
{Id: "act_fcr_cum", Label: "Act. FCR Cummulative", Unit: "kg/kg"},
{Id: "std_fcr_cum", Label: "STD. FCR Cummulative", Unit: "kg/kg"},
},
Dataset: fcrDataset,
},
@@ -843,12 +845,12 @@ func percentDelta(current, last float64) float64 {
return (current - last) / last
}
func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, filter)
func (s dashboardService) calculateHppGlobal(ctx context.Context, startDate, endExclusive, endDate time.Time, location *time.Location) (float64, float64, error) {
totalEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, startDate, endExclusive, nil)
if err != nil {
return 0, 0, err
}
totalCost, err := s.sumHppCost(ctx, filter, startDate, endExclusive)
totalCost, err := s.sumHppCost(ctx, nil, startDate, endExclusive)
if err != nil {
return 0, 0, err
}
@@ -859,11 +861,11 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *valida
}
lastMonthStart, lastMonthEndExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, filter)
lastEggKg, err := s.Repository.SumEggProductionWeightKg(ctx, lastMonthStart, lastMonthEndExclusive, nil)
if err != nil {
return 0, 0, err
}
lastCost, err := s.sumHppCost(ctx, filter, lastMonthStart, lastMonthEndExclusive)
lastCost, err := s.sumHppCost(ctx, nil, lastMonthStart, lastMonthEndExclusive)
if err != nil {
return 0, 0, err
}
@@ -876,16 +878,16 @@ func (s dashboardService) calculateHppGlobal(ctx context.Context, filter *valida
return hppCurrent, hppLast, nil
}
func (s dashboardService) calculateSellingPrice(ctx context.Context, filter *validation.DashboardFilter, endDate time.Time, location *time.Location) (float64, float64, error) {
func (s dashboardService) calculateSellingPrice(ctx context.Context, endDate time.Time, location *time.Location) (float64, float64, error) {
startPrevMonth, endPrevMonthExclusive := monthRange(endDate.AddDate(0, -1, 0), location)
currentEndExclusive := endDate.AddDate(0, 0, 1)
currentAvg, err := s.avgSellingPrice(ctx, filter, startPrevMonth, currentEndExclusive)
currentAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, currentEndExclusive)
if err != nil {
return 0, 0, err
}
lastAvg, err := s.avgSellingPrice(ctx, filter, startPrevMonth, endPrevMonthExclusive)
lastAvg, err := s.avgSellingPrice(ctx, nil, startPrevMonth, endPrevMonthExclusive)
if err != nil {
return 0, 0, err
}
@@ -935,11 +937,11 @@ func (s dashboardService) fcrValue(ctx context.Context, filter *validation.Dashb
}
feedUsageGrams := feedUsageToGrams(feedRows)
if feedUsageGrams <= 0 {
if eggWeightGrams <= 0 {
return 0, nil
}
return eggWeightGrams / feedUsageGrams, nil
return feedUsageGrams / eggWeightGrams, nil
}
func (s dashboardService) mortalityValue(ctx context.Context, filter *validation.DashboardFilter, startDate, endExclusive time.Time) (float64, error) {
@@ -1027,3 +1029,11 @@ func monthRange(t time.Time, location *time.Location) (time.Time, time.Time) {
endExclusive := start.AddDate(0, 1, 0)
return start, endExclusive
}
func currentPeriodDates(location *time.Location) (time.Time, time.Time, time.Time) {
now := time.Now().In(location)
startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location)
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
endExclusive := endDate.AddDate(0, 0, 1)
return startDate, endDate, endExclusive
}
@@ -2,6 +2,7 @@ package repository
import (
"context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -15,6 +16,7 @@ type ExpenseRealizationRepository interface {
IdExists(ctx context.Context, id uint64) (bool, error)
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
}
@@ -55,6 +57,40 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
return realizations, err
}
func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error) {
var realizations []entity.ExpenseRealization
db := r.DB().WithContext(ctx).
Preload("ExpenseNonstock").
Preload("ExpenseNonstock.Nonstock").
Preload("ExpenseNonstock.Nonstock.Uom").
Preload("ExpenseNonstock.Nonstock.Flags").
Preload("ExpenseNonstock.Expense").
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id").
Where("expenses.realization_date IS NOT NULL")
if projectFlockKandangID != nil {
db = db.Where(`(
expense_nonstocks.project_flock_kandang_id = ? OR
(expense_nonstocks.kandang_id = (SELECT kandang_id FROM project_flock_kandangs WHERE id = ?) AND
expense_nonstocks.project_flock_kandang_id IS NULL) OR
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
)`, *projectFlockKandangID, *projectFlockKandangID, fmt.Sprintf("[%d]", projectFlockID))
} else {
db = db.Where(`(
project_flock_kandangs.project_flock_id = ? OR
kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?) OR
(expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb)
)`, projectFlockID, projectFlockID, fmt.Sprintf("[%d]", projectFlockID))
}
err := db.Find(&realizations).Error
return realizations, err
}
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
var realizations []entity.ExpenseRealization
var total int64
@@ -14,6 +14,7 @@ import (
type MarketingDeliveryProductRepository interface {
repository.BaseRepository[entity.MarketingDeliveryProduct]
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
@@ -53,6 +54,43 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
db := r.DB().WithContext(ctx).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("marketing_delivery_products.delivery_date IS NOT NULL").
Distinct("marketing_delivery_products.*")
if projectFlockKandangID != nil {
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
}
db = db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
if err := db.Find(&deliveryProducts).Error; err != nil {
return nil, err
}
return deliveryProducts, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
var deliveryProducts []entity.MarketingDeliveryProduct
@@ -99,13 +137,14 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock")
}).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
Where("marketing_delivery_products.delivery_date IS NOT NULL")
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" {
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
}
if filters.ProductId > 0 || filters.Search != "" {
if filters.ProductId > 0 || filters.Search != "" || filters.MarketingType != "" {
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
}
@@ -139,6 +178,29 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
}
if filters.MarketingType != "" {
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Group("marketing_delivery_products.id")
switch filters.MarketingType {
case "ayam":
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
})
case "telur":
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih), string(utils.FlagTelurRetak),
})
case "trading":
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia),
string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher),
})
}
}
if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") {
if filters.FilterBy == "so_date" {
if filters.StartDate != "" {
@@ -14,6 +14,7 @@ import (
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
@@ -38,6 +39,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
productRepo := rProduct.NewProductRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
userRepo := rUser.NewUserRepository(db)
@@ -88,6 +90,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
kandangRepo,
warehouseRepo,
productWarehouseRepo,
productRepo,
projectFlockRepo,
projectflockkandangrepo,
projectflockpopulationrepo,
@@ -12,6 +12,7 @@ import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
@@ -44,6 +45,7 @@ type chickinService struct {
KandangRepo KandangRepo.KandangRepository
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
ProductRepo rProduct.ProductRepository
ProjectFlockRepo rProjectFlock.ProjectflockRepository
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
@@ -52,7 +54,7 @@ type chickinService struct {
StockLogRepo rStockLogs.StockLogRepository
}
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
return &chickinService{
Log: utils.Log,
Validate: validate,
@@ -60,6 +62,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
ProductRepo: productRepo,
ProjectFlockRepo: projectFlockRepo,
ProjectflockKandangRepo: projectflockkandangRepo,
ProjectflockPopulationRepo: projectflockpopulationRepo,
@@ -99,7 +102,6 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get chickins: %+v", err)
return nil, 0, err
}
return chickins, total, nil
@@ -347,7 +349,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
s.Log.Errorf("Failed to update chickin: %+v", err)
return nil, err
}
@@ -380,7 +381,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
return err
}
}
@@ -449,6 +449,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
if _, err := approvalSvc.CreateApproval(
@@ -479,39 +480,55 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
var targetFlag utils.FlagType
if category == string(utils.ProjectFlockCategoryGrowing) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
}
pfkID := approvableID
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
}
targetFlag = utils.FlagPullet
} else if category == string(utils.ProjectFlockCategoryLaying) {
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
targetFlag = utils.FlagLayer
} else {
continue
}
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id))
}
if populationExists {
continue
}
pfkID := approvableID
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID)
sourcePW, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse")
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for chickin %d", chickin.Id))
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
if err := s.autoAddFlagToProduct(c.Context(), dbTransaction, sourcePW.Product.Id, targetFlag); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to auto-add flag to product %d", sourcePW.Product.Id))
}
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: sourcePW.Id,
TotalQty: 0,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create population for chickin %d", chickin.Id))
}
if err := chickinRepoTx.PatchOne(c.Context(), chickin.Id, map[string]any{
"pending_usage_qty": 0,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset pending usage qty for chickin %d", chickin.Id))
}
if err := s.ReplenishChickinStocks(c.Context(), dbTransaction, &chickin, sourcePW, population, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock for chickin %d", chickin.Id))
}
}
}
@@ -534,7 +551,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for rejected chickin %d: %+v", chickin.Id, err)
return err
}
@@ -568,104 +584,35 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return updated, nil
}
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 {
existingPW := &products[0]
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
existingPW.ProjectFlockKandangId = projectFlockKandangId
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err)
}
}
return existingPW, nil
// autoAddFlagToProduct adds target flag to product if not already present (idempotent)
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
if s.ProductRepo == nil {
return nil
}
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
currentFlags, err := s.ProductRepo.GetFlags(ctx, productID)
if err != nil {
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
}
if product == nil {
return nil, fmt.Errorf("no %s product found in system", categoryCode)
return fmt.Errorf("failed to get product flags: %w", err)
}
newPW := &entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouseId,
ProjectFlockKandangId: projectFlockKandangId,
Quantity: 0,
hasTargetFlag := false
currentFlagNames := make([]string, 0, len(currentFlags))
for _, flag := range currentFlags {
currentFlagNames = append(currentFlagNames, flag.Name)
if flag.Name == string(targetFlag) {
hasTargetFlag = true
}
}
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
if hasTargetFlag {
return nil
}
return newPW, nil
}
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
if targetPW == nil || targetPW.Id == 0 {
return fmt.Errorf("invalid target product warehouse")
newFlags := append(currentFlagNames, string(targetFlag))
if err := s.ProductRepo.SyncFlags(ctx, tx, productID, newFlags); err != nil {
return fmt.Errorf("failed to sync flags: %w", err)
}
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
chickinRepoTx := s.Repository.WithTx(dbTransaction)
var totalQuantityAdded float64
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
if err != nil {
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
}
if populationExists {
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
continue
}
quantityToConvert := chickin.UsageQty
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: targetPW.Id,
TotalQty: 0, // Will be set by FIFO Replenish
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
return err
}
// Reset PendingUsageQty to 0 since population has been created
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
"pending_usage_qty": 0,
}, nil); err != nil {
return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err)
}
// Replenish stock to target ProductWarehouse based on source flag
// StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID
if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil {
s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err)
return err
}
totalQuantityAdded += quantityToConvert
}
// NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
// yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
// - DOC → replenish ke PULLET
// - PULLET → replenish ke LAYER
// - LAYER → tidak perlu replenish (sudah final)
// - DOC+PULLET+LAYER → replenish ke dirinya sendiri
return nil
}
@@ -674,9 +621,6 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
return nil
}
s.Log.Infof("ConsumeChickinStocks: chickin_id=%d, product_warehouse_id=%d, desired_qty=%.3f",
chickin.Id, chickin.ProductWarehouseId, desiredQty)
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
@@ -686,13 +630,9 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err)
return err
}
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return err
}
@@ -706,10 +646,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
}
if err := s.StockLogRepo.CreateOne(ctx, decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
}
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
}
return nil
@@ -720,93 +657,17 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
return nil
}
sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: targetPW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
return err
}
if sourcePW == nil || sourcePW.Product.Id == 0 {
return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id)
}
sourceFlags := sourcePW.Product.Flags
if len(sourceFlags) == 0 {
s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id)
return nil
}
hasDoc := false
hasPullet := false
hasLayer := false
for _, flag := range sourceFlags {
flagName := utils.FlagType(flag.Name)
if flagName == utils.FlagDOC {
hasDoc = true
} else if flagName == utils.FlagPullet {
hasPullet = true
} else if flagName == utils.FlagLayer {
hasLayer = true
}
}
if hasDoc && hasPullet && hasLayer {
s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id)
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: sourcePW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err)
return err
}
return nil
}
// LAYER only - no replenish needed
if hasLayer && !hasDoc && !hasPullet {
s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id)
return nil
}
if hasDoc && !hasPullet && !hasLayer {
s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id)
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: targetPW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err)
return err
}
return nil
}
if hasPullet && !hasDoc && !hasLayer {
s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id)
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: targetPW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err)
return err
}
return nil
}
// Other combinations (e.g., DOC + PULLET without LAYER) - skip for now
s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id)
return nil
}
@@ -825,7 +686,6 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
UsableID: chickin.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for chickin %d: %+v", chickin.Id, err)
return err
}
@@ -842,9 +702,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
}
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
}
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
}
return nil
@@ -82,6 +82,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
ProductId: int64(ctx.QueryInt("product_id", 0)),
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
MarketingType: ctx.Query("marketing_type", ""),
FilterBy: ctx.Query("filter_by", ""),
StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""),
@@ -1,12 +1,16 @@
package dto
import (
"encoding/json"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -22,7 +26,7 @@ type RepportMarketingItemDTO struct {
DoNumber string `json:"do_number"`
Sales *userDTO.UserRelationDTO `json:"sales,omitempty"`
VehicleNumber string `json:"vehicle_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Product *ProductRelationDTOFixed `json:"product,omitempty"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"`
AverageWeightKg float64 `json:"average_weight_kg"`
@@ -46,6 +50,12 @@ type RepportMarketingResponseDTO struct {
Total *Summary `json:"total,omitempty"`
}
type ProductRelationDTOFixed struct {
productDTO.ProductRelationDTO
ProductPrice float64 `json:"product_price"`
SellingPrice *float64 `json:"selling_price,omitempty"`
}
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
soDate := time.Time{}
agingDays := 0
@@ -106,7 +116,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 {
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
item.Product = &mapped
item.Product = newProductRelationDTOFixedPtr(&mapped)
}
return item
@@ -139,7 +149,7 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct
}
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
hasAyam, hasTelur, hasTrading := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if hasAyam {
return "ayam"
@@ -147,12 +157,15 @@ func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
if hasTelur {
return "telur"
}
return "trading"
if hasTrading {
return "trading"
}
return "trading" // default to trading if no flags found
}
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur, hasTrading bool) {
if len(flags) == 0 {
return false, false
return false, false, false
}
for _, flag := range flags {
@@ -167,13 +180,18 @@ func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
hasTelur = true
}
if ft == utils.FlagOVK || ft == utils.FlagObat || ft == utils.FlagVitamin || ft == utils.FlagKimia ||
ft == utils.FlagPakan || ft == utils.FlagPreStarter || ft == utils.FlagStarter || ft == utils.FlagFinisher {
hasTrading = true
}
}
return hasAyam, hasTelur
return hasAyam, hasTelur, hasTrading
}
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
hasAyam, hasTelur, _ := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
return hasAyam
@@ -259,3 +277,39 @@ func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPr
Total: total,
}
}
func newProductRelationDTOFixedPtr(original *productDTO.ProductRelationDTO) *ProductRelationDTOFixed {
if original == nil {
return nil
}
fixed := ProductRelationDTOFixed{
ProductRelationDTO: *original,
ProductPrice: original.ProductPrice,
SellingPrice: original.SellingPrice,
}
return &fixed
}
func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
type Alias struct {
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `json:"product_price"`
SellingPrice *float64 `json:"selling_price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
}
return json.Marshal(&Alias{
Id: p.ProductRelationDTO.Id,
Name: p.ProductRelationDTO.Name,
ProductPrice: p.ProductPrice,
SellingPrice: p.SellingPrice,
Uom: p.ProductRelationDTO.Uom,
Flags: p.ProductRelationDTO.Flags,
ProductCategory: p.ProductRelationDTO.ProductCategory,
Suppliers: p.ProductRelationDTO.Suppliers,
})
}
@@ -23,6 +23,7 @@ type MarketingQuery struct {
ProductId int64 `query:"product_id" validate:"omitempty"`
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`