Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into FEAT/BE/report_customer_payment

This commit is contained in:
aguhh18
2026-01-14 20:10:31 +07:00
35 changed files with 1249 additions and 526 deletions
@@ -0,0 +1,6 @@
-- Rollback: remove price from supplier relations
ALTER TABLE product_suppliers
DROP COLUMN IF EXISTS price;
ALTER TABLE nonstock_suppliers
DROP COLUMN IF EXISTS price;
@@ -0,0 +1,6 @@
-- Migration: add price to supplier relations
ALTER TABLE product_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE nonstock_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
+1
View File
@@ -5,6 +5,7 @@ import "time"
type NonstockSupplier struct {
NonstockId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
+1
View File
@@ -5,6 +5,7 @@ import "time"
type ProductSupplier struct {
ProductId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Product Product `gorm:"foreignKey:ProductId;references:Id"`
@@ -116,7 +116,17 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingSummary(c, uint(id), kandangID)
if err != nil {
return err
}
@@ -228,6 +238,14 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
@@ -404,7 +422,18 @@ func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id), kandangID)
if err != nil {
return err
}
+42 -16
View File
@@ -59,39 +59,65 @@ type ClosingSummaryDTO struct {
StatusClosing string `json:"closing_status"`
}
type ClosingSummaryKandangDTO struct {
FlockID uint `json:"flock_id"`
Period int `json:"period"`
LocationName string `json:"location_name"`
Population int `json:"population"`
PopulationFormatted string `json:"population_formatted"`
ProjectType string `json:"project_type"`
ClosingDate string `json:"closing_date"`
KandangName string `json:"kandang_name"`
ChickInDate string `json:"chick_in_date"`
PicName string `json:"pic_name"`
ApprovalDate string `json:"approval_date"`
ProjectStatus string `json:"project_status"`
}
type ClosingPurchaseDTO struct {
InitialPopulation int `json:"initial_population"`
ClaimCulling int `json:"claim_culling"`
FinalPopulation int `json:"final_population"`
FeedIn float64 `json:"feed_in"`
FeedUsed float64 `json:"feed_used"`
FeedUsedPerHead float64 `json:"feed_used_per_head"`
// FeedUsedPerHead float64 `json:"feed_used_per_head"`
}
type ClosingSalesDTO struct {
SalesPopulation int `json:"sales_population"`
SalesWeight float64 `json:"sales_weight"`
AverageWeight float64 `json:"average_weight"`
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
AverageWeight float64 `json:"avg_weight"`
AverageSellingPrice float64 `json:"avg_selling_price"`
}
type ClosingEggSalesDTO struct {
EggPieces int `json:"egg_pieces"`
EggMassKg float64 `json:"egg_mass_kg"`
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
AverageSellingPrice float64 `json:"egg_average_selling_price"`
EggMassKg float64 `json:"egg_mass"`
AverageEggWeightKg float64 `json:"avg_egg_weight"`
AverageSellingPrice float64 `json:"avg_selling_price"`
}
type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mortality_std"`
MortalityAct float64 `json:"mortality_act"`
DeffMortality float64 `json:"deff_mortality"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"deff_fcr"`
Awg float64 `json:"awg"`
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct *float64 `json:"hen_day_act,omitempty"`
HendayStd *float64 `json:"hen_day_std,omitempty"`
EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
}
type ClosingSalesGroupDTO struct {
@@ -164,7 +190,7 @@ func sumPopulation(history []entity.ProjectFlockKandang) float64 {
var total float64
for _, h := range history {
for _, chickin := range h.Chickins {
total += chickin.UsageQty + chickin.PendingUsageQty
total += chickin.UsageQty
}
}
return total
+4 -1
View File
@@ -11,6 +11,7 @@ import (
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -33,11 +34,13 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
@@ -18,6 +18,7 @@ type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
@@ -166,6 +167,23 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
"strings"
@@ -17,6 +18,7 @@ import (
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -34,9 +36,9 @@ 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, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, 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)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
@@ -56,9 +58,11 @@ type closingService struct {
ChickinRepo chickinRepository.ProjectChickinRepository
PurchaseRepo purchaseRepository.PurchaseRepository
RecordingRepo recordingRepository.RecordingRepository
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
}
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 {
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, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
@@ -73,6 +77,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ChickinRepo: chickinRepo,
PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
}
}
@@ -145,11 +151,15 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectF
return realisasi, nil
}
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if kandangID != nil {
return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID)
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
@@ -170,6 +180,124 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
return &summary, nil
}
func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*dto.ClosingSummaryKandangDTO, error) {
if projectFlockID == 0 || kandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id or kandang id")
}
db := s.Repository.DB().WithContext(ctx)
var kandang entity.ProjectFlockKandang
if err := db.
Preload("Kandang").
Preload("Kandang.Location").
Preload("Kandang.Pic").
Where("project_flock_id = ?", projectFlockID).
Where("kandang_id = ?", kandangID).
First(&kandang).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
s.Log.Errorf("Failed get project flock kandang %d/%d: %+v", projectFlockID, kandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
var project entity.ProjectFlock
if err := db.
Select("id", "category").
First(&project, projectFlockID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
s.Log.Errorf("Failed get project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
var population float64
if err := db.
Table("project_flock_populations pfp").
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("pc.project_flock_kandang_id = ?", kandang.Id).
Select("COALESCE(SUM(pfp.total_qty), 0)").
Scan(&population).Error; err != nil {
s.Log.Errorf("Failed to sum population for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
var chickInDate time.Time
if err := db.
Table("project_chickins").
Where("project_flock_kandang_id = ?", kandang.Id).
Select("MIN(chick_in_date)").
Scan(&chickInDate).Error; err != nil {
s.Log.Errorf("Failed to fetch chick in date for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chick in date")
}
statusProject := "Belum Selesai"
var approvalDate string
if s.ApprovalSvc != nil {
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "")
if err != nil {
s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data")
}
var (
minStep uint16
latestActionAt time.Time
)
for _, rec := range records {
if minStep == 0 || rec.StepNumber < minStep {
minStep = rec.StepNumber
}
if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) {
latestActionAt = rec.ActionAt
statusProject = rec.StepName
}
}
if statusProject == "" && minStep > 0 {
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlockKandang, approvalutils.ApprovalStep(minStep)); ok {
statusProject = label
}
}
if !latestActionAt.IsZero() {
approvalDate = latestActionAt.Format("2006-01-02")
}
}
closingDate := ""
if kandang.ClosedAt != nil {
closingDate = kandang.ClosedAt.Format("2006-01-02")
}
chickInDateStr := ""
if !chickInDate.IsZero() {
chickInDateStr = chickInDate.Format("2006-01-02")
}
populationInt := int(population)
return &dto.ClosingSummaryKandangDTO{
FlockID: projectFlockID,
Period: kandang.Period,
LocationName: kandang.Kandang.Location.Name,
Population: populationInt,
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
ProjectType: project.Category,
ClosingDate: closingDate,
KandangName: kandang.Kandang.Name,
ChickInDate: chickInDateStr,
PicName: kandang.Kandang.Pic.Name,
ApprovalDate: approvalDate,
ProjectStatus: statusProject,
}, nil
}
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
if projectFlockID == 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
@@ -210,7 +338,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
var projectFlockKandangIDs []uint
if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
@@ -290,12 +418,15 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
return ids, nil
}
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
var ids []uint
err := s.Repository.DB().WithContext(ctx).
query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID).
Pluck("id", &ids).Error
Where("project_flock_id = ?", projectFlockID)
if kandangID != nil {
query = query.Where("kandang_id = ?", *kandangID)
}
err := query.Order("id ASC").Pluck("id", &ids).Error
if err != nil {
return nil, err
}
@@ -554,12 +685,22 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
return result, nil
}
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) {
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
if len(projectFlockKandangIDs) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "No project flock kandang found")
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
@@ -568,19 +709,29 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
var population float64
for _, history := range project.KandangHistory {
for _, chickin := range history.Chickins {
population += chickin.UsageQty
}
population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil {
s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week")
}
targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing)
if err != nil {
s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch target metrics data")
}
var fcrActFromRecording *float64
if targetAverages.FcrCount > 0 {
fcrAvg := targetAverages.FcrAvg
fcrActFromRecording = &fcrAvg
}
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
@@ -589,6 +740,40 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
}
averageFeedIntake := targetAverages.FeedIntakeAvg
feedIntakeStd := 0.0
var mortalityStdFromGrowth *float64
if project.ProductionStandardId > 0 && currentWeek > 0 && s.StandardGrowthDetailRepo != nil {
growthDetail, growthErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if growthErr != nil {
if !errors.Is(growthErr, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch growth detail for project flock %d: %+v", projectFlockID, growthErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch growth standard data")
}
} else if growthDetail != nil {
if growthDetail.FeedIntake != nil {
feedIntakeStd = *growthDetail.FeedIntake
}
if growthDetail.MaxDepletion != nil {
mortalityStdFromGrowth = growthDetail.MaxDepletion
}
}
}
var productionStandardDetail *entity.ProductionStandardDetail
if project.ProductionStandardId > 0 && currentWeek > 0 && s.ProductionStandardDetailRepo != nil {
productionStandardDetail, err = s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
productionStandardDetail = nil
} else {
s.Log.Errorf("Failed to fetch production standard detail for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch production standard detail data")
}
}
}
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil {
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
@@ -611,10 +796,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
}
feedUsedPerHead := 0.0
if population > 0 {
feedUsedPerHead = feedUsed / population
}
// feedUsedPerHead := 0.0
// if population > 0 {
// feedUsedPerHead = feedUsed / population
// }
purchase := dto.ClosingPurchaseDTO{
InitialPopulation: int(population),
@@ -622,7 +807,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
FinalPopulation: int(finalPopulation),
FeedIn: feedIn,
FeedUsed: feedUsed,
FeedUsedPerHead: feedUsedPerHead,
// FeedUsedPerHead: feedUsedPerHead,
}
chickenFlagNames := []string{string(utils.FlagPullet)}
@@ -655,6 +840,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
}
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording
}
var eggSales *dto.ClosingEggSalesDTO
var eggPerformance *dto.ClosingPerformanceDTO
@@ -702,6 +890,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
}
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
if fcrActFromRecording != nil {
eggPerf.FcrAct = *fcrActFromRecording
}
eggPerformance = &eggPerf
}
@@ -718,15 +909,63 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
DeffMortality: chickenPerformance.DeffMortality,
}
if eggPerformance != nil {
performance.FcrStd = eggPerformance.FcrStd
// performance.FcrStd = eggPerformance.FcrStd
performance.FcrAct = eggPerformance.FcrAct
performance.DeffFcr = eggPerformance.DeffFcr
performance.Awg = eggPerformance.Awg
// performance.DeffFcr = eggPerformance.DeffFcr
performance.AwgAct = eggPerformance.AwgAct
} else {
performance.FcrStd = chickenPerformance.FcrStd
// performance.FcrStd = chickenPerformance.FcrStd
performance.FcrAct = chickenPerformance.FcrAct
performance.DeffFcr = chickenPerformance.DeffFcr
performance.Awg = chickenPerformance.Awg
// performance.DeffFcr = chickenPerformance.DeffFcr
performance.AwgAct = chickenPerformance.AwgAct
}
performance.FeedIntake = averageFeedIntake
performance.FeedIntakeStd = feedIntakeStd
if targetAverages.CumDepletionRateCount > 0 {
performance.MortalityAct = targetAverages.CumDepletionRateAvg
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if mortalityStdFromGrowth != nil {
performance.MortalityStd = *mortalityStdFromGrowth
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if !isGrowing {
if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = &henDayAct
}
if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = &henHouseAct
}
if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = &eggWeight
}
if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg
performance.EggMass = &eggMass
}
}
performance.DeffFcr = performance.FcrStd - performance.FcrAct
if productionStandardDetail != nil {
if productionStandardDetail.StandardFCR != nil {
performance.FcrStd = *productionStandardDetail.StandardFCR
}
if !isGrowing {
if productionStandardDetail.TargetHenDayProduction != nil {
performance.HendayStd = productionStandardDetail.TargetHenDayProduction
}
if productionStandardDetail.TargetHenHouseProduction != nil {
performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction
}
if productionStandardDetail.TargetEggWeight != nil {
performance.EggWeightStd = productionStandardDetail.TargetEggWeight
}
if productionStandardDetail.TargetEggMass != nil {
performance.EggMassStd = productionStandardDetail.TargetEggMass
}
}
}
result := dto.ClosingProductionReportDTO{
@@ -772,6 +1011,46 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
return totalAgeWeeks / totalQty, nil
}
func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
firstKandangID := projectFlockKandangIDs[0]
var chickin entity.ProjectChickin
if err := s.Repository.DB().WithContext(ctx).
Where("project_flock_kandang_id = ?", firstKandangID).
Order("chick_in_date ASC").
First(&chickin).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
return 0, err
}
recording, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx, firstKandangID)
if err != nil {
return 0, err
}
if recording == nil {
return 0, nil
}
if recording.RecordDatetime.Before(chickin.ChickInDate) {
return 0, nil
}
elapsed := recording.RecordDatetime.Sub(chickin.ChickInDate)
weekFloat := elapsed.Hours() / (24 * 7)
week := int(math.Ceil(weekFloat))
if week <= 0 {
week = 1
}
return week, nil
}
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
@@ -802,7 +1081,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
FcrStd: fcrStd,
FcrAct: fcrAct,
DeffFcr: deffFcr,
Awg: awg,
AwgAct: awg,
}
}
@@ -20,7 +20,8 @@ const (
)
type ClosingSapronakQuery struct {
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
}
@@ -4,7 +4,6 @@ import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
@@ -23,7 +22,7 @@ type NonstockListDTO struct {
Name string `json:"name"`
Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
Suppliers []NonstockSupplierDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -33,6 +32,14 @@ type NonstockDetailDTO struct {
NonstockListDTO
}
type NonstockSupplierDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
Price float64 `json:"price"`
}
// === Mapper Functions ===
func ToNonstockRelationDTO(e entity.Nonstock) NonstockRelationDTO {
@@ -99,21 +106,27 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
}
}
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []NonstockSupplierDTO {
if len(relations) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
return make([]NonstockSupplierDTO, 0)
}
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
result := make([]NonstockSupplierDTO, 0, len(relations))
for _, relation := range relations {
if relation.Supplier.Id == 0 {
continue
}
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
result = append(result, NonstockSupplierDTO{
Id: relation.Supplier.Id,
Name: relation.Supplier.Name,
Alias: relation.Supplier.Alias,
Category: relation.Supplier.Category,
Price: relation.Price,
})
}
if len(result) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
return make([]NonstockSupplierDTO, 0)
}
return result
@@ -12,7 +12,7 @@ import (
type NonstockRepository interface {
repository.BaseRepository[entity.Nonstock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error
UomExists(ctx context.Context, uomID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
@@ -40,13 +40,13 @@ func (r *NonstockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, e
return repository.Exists[entity.Nonstock](ctx, r.DB(), id)
}
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error {
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, suppliers []entity.NonstockSupplier) error {
db := tx
if db == nil {
db = r.DB()
}
if supplierIDs == nil {
if suppliers == nil {
return db.WithContext(ctx).
Where("nonstock_id = ?", nonstockID).
Delete(&entity.NonstockSupplier{}).
@@ -61,18 +61,31 @@ func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm
return err
}
existingMap := make(map[uint]struct{}, len(existing))
existingMap := make(map[uint]entity.NonstockSupplier, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierId] = struct{}{}
existingMap[rel.SupplierId] = rel
}
incomingMap := make(map[uint]struct{}, len(supplierIDs))
for _, id := range supplierIDs {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
incomingMap := make(map[uint]struct{}, len(suppliers))
for _, rel := range suppliers {
incomingMap[rel.SupplierId] = struct{}{}
if existingRel, exists := existingMap[rel.SupplierId]; exists {
if existingRel.Price != rel.Price {
if err := db.WithContext(ctx).
Model(&entity.NonstockSupplier{}).
Where("nonstock_id = ? AND supplier_id = ?", nonstockID, rel.SupplierId).
Update("price", rel.Price).
Error; err != nil {
return err
}
}
continue
}
record := entity.NonstockSupplier{NonstockId: nonstockID, SupplierId: id}
record := entity.NonstockSupplier{
NonstockId: nonstockID,
SupplierId: rel.SupplierId,
Price: rel.Price,
}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
@@ -111,8 +111,25 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
if len(supplierIDs) > 0 {
var (
supplierLinks []entity.NonstockSupplier
supplierIDs []uint
)
if len(req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(req.Suppliers))
supplierLinks = make([]entity.NonstockSupplier, 0, len(req.Suppliers))
supplierIDs = make([]uint, 0, len(req.Suppliers))
for _, supplier := range req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
@@ -155,7 +172,7 @@ func (s *nonstockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
})
if err != nil {
@@ -193,15 +210,27 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
updateBody["uom_id"] = *req.UomID
}
var supplierIDs []uint
var supplierLinks []entity.NonstockSupplier
var supplierUpdate bool
if req.SupplierIDs != nil {
if req.Suppliers != nil {
supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var supplierList []entity.Supplier
var supplierErr error
supplierList, supplierErr = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if len(*req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(*req.Suppliers))
supplierLinks = make([]entity.NonstockSupplier, 0, len(*req.Suppliers))
supplierIDs := make([]uint, 0, len(*req.Suppliers))
for _, supplier := range *req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.NonstockSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
supplierList, supplierErr := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if supplierErr != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", supplierErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
@@ -253,11 +282,7 @@ func (s nonstockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
}
if supplierUpdate {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
return err
}
}
@@ -1,16 +1,21 @@
package validation
type SupplierPrice struct {
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
Price float64 `json:"price" validate:"required,gte=0"`
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flags []string `json:"flags" validate:"dive,max=50"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
}
@@ -1,8 +1,10 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
@@ -22,6 +24,8 @@ type ProductionStandardService interface {
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
EnsureWeekStart(ctx context.Context, standardID uint, category string) error
EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error
}
type productionStandardService struct {
@@ -299,3 +303,80 @@ func (s productionStandardService) DeleteOne(c *fiber.Ctx, id uint) error {
}
return nil
}
func (s productionStandardService) EnsureWeekStart(ctx context.Context, standardID uint, category string) error {
if standardID == 0 || strings.TrimSpace(category) == "" {
return nil
}
switch strings.ToUpper(category) {
case string(utils.ProjectFlockCategoryLaying):
details, err := s.ProductionStandardDetailRepo.GetByProductionStandardID(ctx, standardID)
if err != nil {
return err
}
startWeek := 0
if len(details) > 0 {
startWeek = details[0].Week
}
if startWeek != 18 {
return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock")
}
case string(utils.ProjectFlockCategoryGrowing):
details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(ctx, standardID)
if err != nil {
return err
}
startWeek := 0
if len(details) > 0 {
startWeek = details[0].Week
}
if startWeek != 1 {
return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock")
}
}
return nil
}
func (s productionStandardService) EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error {
if standardID == 0 || day <= 0 {
return nil
}
upperCategory := strings.ToUpper(category)
weekBase := 1
if upperCategory == string(utils.ProjectFlockCategoryLaying) {
weekBase = 18
}
week := ((day - 1) / 7) + weekBase
if week <= 0 {
return nil
}
if upperCategory == string(utils.ProjectFlockCategoryLaying) {
detail, err := s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
return err
}
if detail == nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
}
growthDetail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
return err
}
if growthDetail == nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week))
}
return nil
}
@@ -5,7 +5,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/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"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
@@ -20,7 +19,7 @@ type ProductRelationDTO struct {
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
Suppliers []ProductSupplierDTO `json:"suppliers"`
}
type ProductListDTO struct {
@@ -35,7 +34,7 @@ type ProductListDTO struct {
Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
Suppliers []ProductSupplierDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -45,6 +44,14 @@ type ProductDetailDTO struct {
ProductListDTO
}
type ProductSupplierDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
Price float64 `json:"price"`
}
// === Mapper Functions ===
func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
@@ -134,21 +141,27 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
}
}
func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO {
func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO {
if len(relations) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
return make([]ProductSupplierDTO, 0)
}
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
result := make([]ProductSupplierDTO, 0, len(relations))
for _, relation := range relations {
if relation.Supplier.Id == 0 {
continue
}
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
result = append(result, ProductSupplierDTO{
Id: relation.Supplier.Id,
Name: relation.Supplier.Name,
Alias: relation.Supplier.Alias,
Category: relation.Supplier.Category,
Price: relation.Price,
})
}
if len(result) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
return make([]ProductSupplierDTO, 0)
}
return result
@@ -17,7 +17,7 @@ type ProductRepository interface {
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
GetFlags(ctx context.Context, productID uint) ([]entity.Flag, error)
@@ -102,13 +102,13 @@ func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productI
return count > 0, nil
}
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIds []uint) error {
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, suppliers []entity.ProductSupplier) error {
db := tx
if db == nil {
db = r.DB()
}
if supplierIds == nil {
if suppliers == nil {
return db.WithContext(ctx).
Where("product_id = ?", productID).
Delete(&entity.ProductSupplier{}).
@@ -123,18 +123,31 @@ func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.
return err
}
existingMap := make(map[uint]struct{}, len(existing))
existingMap := make(map[uint]entity.ProductSupplier, len(existing))
for _, rel := range existing {
existingMap[rel.SupplierId] = struct{}{}
existingMap[rel.SupplierId] = rel
}
incomingMap := make(map[uint]struct{}, len(supplierIds))
for _, id := range supplierIds {
incomingMap[id] = struct{}{}
if _, exists := existingMap[id]; exists {
incomingMap := make(map[uint]struct{}, len(suppliers))
for _, rel := range suppliers {
incomingMap[rel.SupplierId] = struct{}{}
if existingRel, exists := existingMap[rel.SupplierId]; exists {
if existingRel.Price != rel.Price {
if err := db.WithContext(ctx).
Model(&entity.ProductSupplier{}).
Where("product_id = ? AND supplier_id = ?", productID, rel.SupplierId).
Update("price", rel.Price).
Error; err != nil {
return err
}
}
continue
}
record := entity.ProductSupplier{ProductId: productID, SupplierId: id}
record := entity.ProductSupplier{
ProductId: productID,
SupplierId: rel.SupplierId,
Price: rel.Price,
}
if err := db.WithContext(ctx).Create(&record).Error; err != nil {
return err
}
@@ -138,9 +138,25 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err
}
supplierIDs := utils.UniqueUintSlice(req.SupplierIDs)
var err error
if len(supplierIDs) > 0 {
var (
supplierLinks []entity.ProductSupplier
supplierIDs []uint
)
if len(req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(req.Suppliers))
supplierLinks = make([]entity.ProductSupplier, 0, len(req.Suppliers))
supplierIDs = make([]uint, 0, len(req.Suppliers))
for _, supplier := range req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.ProductSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
@@ -180,7 +196,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
CreatedBy: 1,
}
err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(ctx, createBody, nil); err != nil {
@@ -191,7 +207,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return err
}
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierIDs)
return s.Repository.SyncSuppliersDiff(ctx, tx, createBody.Id, supplierLinks)
})
if err != nil {
@@ -276,15 +292,27 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
ctx := c.Context()
var suppliers []entity.Supplier
var supplierIDs []uint
var supplierLinks []entity.ProductSupplier
var supplierUpdate bool
if req.SupplierIDs != nil {
if req.Suppliers != nil {
supplierUpdate = true
supplierIDs = utils.UniqueUintSlice(*req.SupplierIDs)
if len(supplierIDs) > 0 {
var err error
suppliers, err = s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if len(*req.Suppliers) > 0 {
seen := make(map[uint]struct{}, len(*req.Suppliers))
supplierLinks = make([]entity.ProductSupplier, 0, len(*req.Suppliers))
supplierIDs := make([]uint, 0, len(*req.Suppliers))
for _, supplier := range *req.Suppliers {
if _, exists := seen[supplier.SupplierID]; exists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate supplier_id %d", supplier.SupplierID))
}
seen[supplier.SupplierID] = struct{}{}
supplierIDs = append(supplierIDs, supplier.SupplierID)
supplierLinks = append(supplierLinks, entity.ProductSupplier{
SupplierId: supplier.SupplierID,
Price: supplier.Price,
})
}
suppliers, err := s.Repository.GetSuppliersByIDs(ctx, supplierIDs)
if err != nil {
s.Log.Errorf("Failed to validate suppliers: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate suppliers")
@@ -336,11 +364,7 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
if supplierUpdate {
var ids []uint
if len(supplierIDs) > 0 {
ids = supplierIDs
}
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, ids); err != nil {
if err := s.Repository.SyncSuppliersDiff(ctx, tx, id, supplierLinks); err != nil {
return err
}
}
@@ -1,5 +1,10 @@
package validation
type SupplierPrice struct {
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
Price float64 `json:"price" validate:"required,gte=0"`
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
@@ -10,7 +15,7 @@ type Create struct {
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
}
@@ -24,7 +29,7 @@ type Update struct {
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
}
@@ -10,6 +10,7 @@ import (
type SupplierNonstockDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
}
@@ -42,6 +43,7 @@ func toSupplierNonstockDTOs(relations []entity.NonstockSupplier) []SupplierNonst
result = append(result, SupplierNonstockDTO{
Id: Nonstock.Id,
Name: Nonstock.Name,
Price: relation.Price,
Uom: uomRef,
Flags: flags,
})
@@ -8,12 +8,13 @@ import (
// === DTO Structs ===
type SupplierProductDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
Id uint `json:"id"`
Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
SupplierPrice float64 `json:"supplier_price"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
}
// === Mapper Functions ===
@@ -42,12 +43,13 @@ func toSupplierProductDTOs(relations []entity.ProductSupplier) []SupplierProduct
}
result = append(result, SupplierProductDTO{
Id: product.Id,
Name: product.Name,
ProductPrice: product.ProductPrice,
SellingPrice: product.SellingPrice,
Uom: uomRef,
Flags: flags,
Id: product.Id,
Name: product.Name,
ProductPrice: product.ProductPrice,
SellingPrice: product.SellingPrice,
SupplierPrice: relation.Price,
Uom: uomRef,
Flags: flags,
})
}
return result
@@ -14,44 +14,83 @@ import (
// === DTO Structs ===
type RecordingProjectFlockDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
FlockName string `json:"flock_name"`
ProjectFlockCategory string `json:"project_flock_category"`
Period int `json:"period"`
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
TotalChickQty float64 `json:"total_chick_qty"`
}
type RecordingProductionStandardDTO struct {
Id uint `json:"id"`
Week int `json:"week"`
Name string `json:"name"`
HenDayStd float64 `json:"hen_day_std"`
HenHouseStd float64 `json:"hen_house_std"`
FeedIntakeStd float64 `json:"feed_intake_std"`
MaxDepletionStd float64 `json:"max_depletion_std"`
EggMassStd float64 `json:"egg_mass_std"`
EggWeightStd float64 `json:"egg_weight_std"`
}
type RecordingFcrDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
FcrStd float64 `json:"fcr_std"`
}
type RecordingAreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type RecordingLocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
}
type RecordingWarehouseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Area *RecordingAreaDTO `json:"area,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
}
type RecordingRelationDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
RecordDatetime time.Time `json:"record_datetime"`
Day int `json:"day"`
ProjectFlockCategory string `json:"project_flock_category"`
TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"`
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"`
HenDay float64 `json:"hen_day"`
HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"`
EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"`
StandardHenDay *float64 `json:"hen_day_std,omitempty"`
StandardHenHouse *float64 `json:"hen_house_std,omitempty"`
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
StandardEggMass *float64 `json:"egg_mass_std,omitempty"`
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
StandardFcr *float64 `json:"fcr_std,omitempty"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
Id uint `json:"id"`
ProjectFlock RecordingProjectFlockDTO `json:"project_flock"`
RecordDatetime time.Time `json:"record_datetime"`
Day int `json:"day"`
TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"`
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
HenDay float64 `json:"hen_day"`
HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"`
EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type RecordingListDTO struct {
RecordingRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RecordingDetailDTO struct {
RecordingListDTO
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
ProductCategory string `json:"product_category"`
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
}
type RecordingDepletionDTO struct {
@@ -63,7 +102,7 @@ type RecordingDepletionDTO struct {
type RecordingStockDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageAmount float64 `json:"usage_amount"`
PendingQty *float64 `json:"pending_qty,omitempty"`
PendingQty float64 `json:"pending_qty"`
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
@@ -75,117 +114,10 @@ type RecordingEggDTO struct {
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
type RecordingProductWarehouseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
ProductName string `json:"product_name"`
WarehouseId uint `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
}
// === Mapper Functions ===
func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
var (
projectFlockCategory string
day int
totalDepletionQty float64
cumDepletionRate float64
cumIntake int
fcrValue float64
totalChickQty float64
henDay float64
henHouse float64
feedIntake float64
eggMass float64
eggWeight float64
)
if e.Day != nil {
day = *e.Day
}
if e.TotalDepletionQty != nil {
totalDepletionQty = *e.TotalDepletionQty
}
if e.CumDepletionRate != nil {
cumDepletionRate = *e.CumDepletionRate
}
if e.CumIntake != nil {
cumIntake = *e.CumIntake
}
if e.FcrValue != nil {
fcrValue = *e.FcrValue
}
if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty
}
if e.HenDay != nil {
henDay = *e.HenDay
}
if e.HenHouse != nil {
henHouse = *e.HenHouse
}
if e.FeedIntake != nil {
feedIntake = *e.FeedIntake
}
if e.EggMass != nil {
eggMass = *e.EggMass
}
if e.EggWeight != nil {
eggWeight = *e.EggWeight
}
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
category := e.ProjectFlockKandang.ProjectFlock.Category
projectFlockCategory = category
}
latestApproval := defaultRecordingLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
return RecordingRelationDTO{
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
RecordDatetime: e.RecordDatetime,
Day: day,
ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: totalDepletionQty,
CumDepletionRate: cumDepletionRate,
CumIntake: cumIntake,
FcrValue: fcrValue,
TotalChickQty: totalChickQty,
HenDay: henDay,
HenHouse: henHouse,
FeedIntake: feedIntake,
EggMass: eggMass,
EggWeight: eggWeight,
StandardHenDay: e.StandardHenDay,
StandardHenHouse: e.StandardHenHouse,
StandardFeedIntake: e.StandardFeedIntake,
StandardMaxDepletion: e.StandardMaxDepletion,
StandardEggMass: e.StandardEggMass,
StandardEggWeight: e.StandardEggWeight,
StandardFcr: e.StandardFcr,
Approval: latestApproval,
}
}
func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
return RecordingListDTO{
RecordingRelationDTO: ToRecordingRelationDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
return toRecordingListDTO(e)
}
func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
@@ -197,20 +129,15 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
}
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
listDTO := ToRecordingListDTO(e)
var eggs []RecordingEggDTO
if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) {
eggs = ToRecordingEggDTOs(e.Eggs)
} else if len(e.Eggs) > 0 {
eggs = ToRecordingEggDTOs(e.Eggs)
}
listDTO := toRecordingListDTO(e)
return RecordingDetailDTO{
RecordingListDTO: listDTO,
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: eggs,
Warehouse: recordingWarehouseDTO(e),
ProductCategory: recordingProductCategory(e),
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs),
}
}
@@ -233,11 +160,15 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
if s.UsageQty != nil {
usageAmount = *s.UsageQty
}
var pendingQty float64
if s.PendingQty != nil {
pendingQty = *s.PendingQty
}
result[i] = RecordingStockDTO{
ProductWarehouseId: s.ProductWarehouseId,
UsageAmount: usageAmount,
PendingQty: s.PendingQty,
PendingQty: pendingQty,
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
}
}
@@ -258,6 +189,184 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
return result
}
func toRecordingListDTO(e entity.Recording) RecordingListDTO {
relation := toRecordingRelationDTO(e)
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
return RecordingListDTO{
RecordingRelationDTO: relation,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
latestApproval := defaultRecordingLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
return RecordingRelationDTO{
Id: e.Id,
ProjectFlock: toRecordingProjectFlockDTO(e),
RecordDatetime: e.RecordDatetime,
Day: intValue(e.Day),
TotalDepletionQty: floatValue(e.TotalDepletionQty),
CumDepletionRate: floatValue(e.CumDepletionRate),
CumIntake: intValue(e.CumIntake),
FcrValue: floatValue(e.FcrValue),
HenDay: floatValue(e.HenDay),
HenHouse: floatValue(e.HenHouse),
FeedIntake: floatValue(e.FeedIntake),
EggMass: floatValue(e.EggMass),
EggWeight: floatValue(e.EggWeight),
Approval: latestApproval,
}
}
func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO {
result := RecordingProjectFlockDTO{
ProjectFlockKandangId: e.ProjectFlockKandangId,
}
pfk := e.ProjectFlockKandang
if pfk == nil {
return result
}
if pfk.ProjectFlock.Id != 0 {
result.FlockName = pfk.ProjectFlock.FlockName
if pfk.ProjectFlock.Category != "" {
result.ProjectFlockCategory = strings.ToUpper(pfk.ProjectFlock.Category)
}
}
result.Period = pfk.Period
if pfk.ProjectFlock.ProductionStandard.Id != 0 {
result.ProductionStandart = &RecordingProductionStandardDTO{
Id: pfk.ProjectFlock.ProductionStandard.Id,
Week: recordingWeekValue(e),
Name: pfk.ProjectFlock.ProductionStandard.Name,
HenDayStd: floatValue(e.StandardHenDay),
HenHouseStd: floatValue(e.StandardHenHouse),
FeedIntakeStd: floatValue(e.StandardFeedIntake),
MaxDepletionStd: floatValue(e.StandardMaxDepletion),
EggMassStd: floatValue(e.StandardEggMass),
EggWeightStd: floatValue(e.StandardEggWeight),
}
}
if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil {
result.Fcr = &RecordingFcrDTO{
Id: pfk.ProjectFlock.Fcr.Id,
Name: pfk.ProjectFlock.Fcr.Name,
FcrStd: floatValue(e.StandardFcr),
}
}
result.TotalChickQty = floatValue(e.TotalChickQty)
return result
}
func recordingWeekValue(e entity.Recording) int {
day := intValue(e.Day)
if day <= 0 {
return 0
}
weekBase := 1
if isLayingRecording(e) {
weekBase = 18
}
return ((day - 1) / 7) + weekBase
}
func isLayingRecording(e entity.Recording) bool {
if e.ProjectFlockKandang == nil {
return false
}
return strings.EqualFold(e.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying))
}
func recordingProductCategory(e entity.Recording) string {
if e.ProjectFlockKandang == nil {
return ""
}
project := e.ProjectFlockKandang.ProjectFlock
if project.Id == 0 {
return ""
}
if project.ProductionStandard.Id != 0 && project.ProductionStandard.ProjectCategory != "" {
return strings.ToUpper(project.ProductionStandard.ProjectCategory)
}
if project.Category != "" {
return strings.ToUpper(project.Category)
}
return ""
}
func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
pw := primaryProductWarehouse(e)
if pw == nil || pw.Warehouse.Id == 0 {
return nil
}
return mapWarehouseDTO(&pw.Warehouse)
}
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
if len(e.Stocks) > 0 {
pw := e.Stocks[0].ProductWarehouse
if pw.Id != 0 {
return &pw
}
}
if len(e.Depletions) > 0 {
pw := e.Depletions[0].ProductWarehouse
if pw.Id != 0 {
return &pw
}
}
if len(e.Eggs) > 0 {
pw := e.Eggs[0].ProductWarehouse
if pw.Id != 0 {
return &pw
}
}
return nil
}
func mapWarehouseDTO(wh *entity.Warehouse) *RecordingWarehouseDTO {
if wh == nil || wh.Id == 0 {
return nil
}
dto := &RecordingWarehouseDTO{
Id: wh.Id,
Name: wh.Name,
}
if wh.Area.Id != 0 {
dto.Area = &RecordingAreaDTO{
Id: wh.Area.Id,
Name: wh.Area.Name,
}
}
if wh.Location != nil && wh.Location.Id != 0 {
dto.Location = &RecordingLocationDTO{
Id: wh.Location.Id,
Name: wh.Location.Name,
Address: wh.Location.Address,
}
}
return dto
}
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
if pw == nil {
return productWarehouseDTO.ProductWarehouseDTO{}
@@ -271,6 +380,20 @@ func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.Pro
return *mapped
}
func floatValue(value *float64) float64 {
if value == nil {
return 0
}
return *value
}
func intValue(value *int) int {
if value == nil {
return 0
}
return *value
}
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
result := approvalDTO.ApprovalRelationDTO{}
@@ -11,6 +11,8 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
@@ -29,6 +31,16 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
productionStandardService := sProductionStandard.NewProductionStandardService(
productionStandardRepo,
productionStandardDetailRepo,
standardGrowthDetailRepo,
validate,
)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
@@ -63,6 +75,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalRepo,
approvalService,
fifoService,
productionStandardService,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
@@ -48,12 +48,30 @@ type RecordingRepository interface {
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error)
}
type RecordingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Recording]
}
type RecordingTargetAverages struct {
HenDayAvg float64
HenDayCount int64
HenHouseAvg float64
HenHouseCount int64
EggWeightAvg float64
EggWeightCount int64
EggMassAvg float64
EggMassCount int64
FeedIntakeAvg float64
FeedIntakeCount int64
FcrAvg float64
FcrCount int64
CumDepletionRateAvg float64
CumDepletionRateCount int64
}
func NewRecordingRepository(db *gorm.DB) RecordingRepository {
return &RecordingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
@@ -64,19 +82,28 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.Kandang").
Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
Preload("Depletions").
Preload("Depletions.ProductWarehouse").
Preload("Depletions.ProductWarehouse.Product").
Preload("Depletions.ProductWarehouse.Warehouse").
Preload("Depletions.ProductWarehouse.Warehouse.Area").
Preload("Depletions.ProductWarehouse.Warehouse.Location").
Preload("Stocks").
Preload("Stocks.ProductWarehouse").
Preload("Stocks.ProductWarehouse.Product").
Preload("Stocks.ProductWarehouse.Warehouse").
Preload("Stocks.ProductWarehouse.Warehouse.Area").
Preload("Stocks.ProductWarehouse.Warehouse.Location").
Preload("Eggs").
Preload("Eggs.ProductWarehouse").
Preload("Eggs.ProductWarehouse.Product").
Preload("Eggs.ProductWarehouse.Warehouse")
Preload("Eggs.ProductWarehouse.Warehouse").
Preload("Eggs.ProductWarehouse.Warehouse.Area").
Preload("Eggs.ProductWarehouse.Warehouse.Location")
}
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
@@ -433,6 +460,67 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct
return result, err
}
func (r *RecordingRepositoryImpl) GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) {
var row struct {
HenDayTotal float64
HenHouseTotal float64
EggWeightTotal float64
EggMassTotal float64
FeedIntakeTotal float64
FcrTotal float64
CumDepletionRateTotal float64
TotalCount int64
}
selectParts := []string{
"COALESCE(SUM(feed_intake), 0) AS feed_intake_total",
"COALESCE(SUM(fcr_value), 0) AS fcr_total",
"COALESCE(SUM(cum_depletion_rate), 0) AS cum_depletion_rate_total",
"COUNT(*) AS total_count",
}
if includeTargets {
selectParts = append([]string{
"COALESCE(SUM(hen_day), 0) AS hen_day_total",
"COALESCE(SUM(hen_house), 0) AS hen_house_total",
"COALESCE(SUM(egg_weight), 0) AS egg_weight_total",
"COALESCE(SUM(egg_mass), 0) AS egg_mass_total",
}, selectParts...)
}
if err := r.DB().WithContext(ctx).
Table("recordings").
Select(strings.Join(selectParts, ", ")).
Where("project_flock_kandangs_id = ? AND deleted_at IS NULL", projectFlockKandangID).
Scan(&row).Error; err != nil {
return RecordingTargetAverages{}, err
}
result := RecordingTargetAverages{
FeedIntakeCount: row.TotalCount,
FcrCount: row.TotalCount,
CumDepletionRateCount: row.TotalCount,
}
if includeTargets {
result.HenDayCount = row.TotalCount
result.HenHouseCount = row.TotalCount
result.EggWeightCount = row.TotalCount
result.EggMassCount = row.TotalCount
}
if row.TotalCount > 0 {
if includeTargets {
result.HenDayAvg = row.HenDayTotal / float64(row.TotalCount)
result.HenHouseAvg = row.HenHouseTotal / float64(row.TotalCount)
result.EggWeightAvg = row.EggWeightTotal / float64(row.TotalCount)
result.EggMassAvg = row.EggMassTotal / float64(row.TotalCount)
}
result.FeedIntakeAvg = row.FeedIntakeTotal / float64(row.TotalCount)
result.FcrAvg = row.FcrTotal / float64(row.TotalCount)
result.CumDepletionRateAvg = row.CumDepletionRateTotal
}
return result, nil
}
func nextRecordingDay(days []int) int {
if len(days) == 0 {
return 1
@@ -10,6 +10,7 @@ import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
@@ -53,6 +54,7 @@ type recordingService struct {
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ApprovalRepo commonRepo.ApprovalRepository
ApprovalSvc commonSvc.ApprovalService
ProductionStandardSvc sProductionStandard.ProductionStandardService
FifoSvc commonSvc.FifoService
}
@@ -64,6 +66,7 @@ func NewRecordingService(
approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
productionStandardSvc sProductionStandard.ProductionStandardService,
validate *validator.Validate,
) RecordingService {
return &recordingService{
@@ -75,6 +78,7 @@ func NewRecordingService(
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ApprovalRepo: approvalRepo,
ApprovalSvc: approvalSvc,
ProductionStandardSvc: productionStandardSvc,
FifoSvc: fifoSvc,
}
}
@@ -169,6 +173,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
}
ctx := c.Context()
recordTime := time.Now().UTC()
if req.RecordDate != nil && strings.TrimSpace(*req.RecordDate) != "" {
parsed, err := time.Parse("2006-01-02", strings.TrimSpace(*req.RecordDate))
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format")
}
recordTime = parsed.UTC()
}
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, req.ProjectFlockKandangId)
if err != nil {
@@ -188,6 +200,11 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if err := s.ensureChickInExists(ctx, pfk.Id); err != nil {
return nil, err
}
if s.ProductionStandardSvc != nil {
if err := s.ProductionStandardSvc.EnsureWeekStart(ctx, pfk.ProjectFlock.ProductionStandardId, category); err != nil {
return nil, err
}
}
if !isLaying && len(req.Eggs) > 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
@@ -210,8 +227,12 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
s.Log.Errorf("Failed to determine recording day: %+v", err)
return err
}
if s.ProductionStandardSvc != nil {
if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, nextDay); err != nil {
return err
}
}
recordTime := time.Now().UTC()
existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime)
if err != nil {
s.Log.Errorf("Failed to verify existing recording on date: %+v", err)
@@ -1157,7 +1178,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var fcrValue float64
if usageInGrams > 0 && totalEggWeightGrams > 0 {
fcrValue = totalEggWeightGrams / usageInGrams
fcrValue = usageInGrams / totalEggWeightGrams
updates["fcr_value"] = fcrValue
recording.FcrValue = &fcrValue
} else {
@@ -1330,12 +1351,16 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
return nil
}
week := ((int(*item.Day) - 1) / 7) + 1
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
weekBase := 1
if category == string(utils.ProjectFlockCategoryLaying) {
weekBase = 18
}
week := ((int(*item.Day) - 1) / 7) + weekBase
if week <= 0 {
return nil
}
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
db := s.Repository.DB()
standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
@@ -21,6 +21,7 @@ type (
type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions" validate:"dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
@@ -302,6 +302,16 @@ func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
}
suppliers := make([]supplierDTO.SupplierRelationDTO, len(p.ProductRelationDTO.Suppliers))
for i, ps := range p.ProductRelationDTO.Suppliers {
suppliers[i] = supplierDTO.SupplierRelationDTO{
Id: ps.Id,
Name: ps.Name,
Alias: ps.Alias,
Category: ps.Category,
}
}
return json.Marshal(&Alias{
Id: p.ProductRelationDTO.Id,
Name: p.ProductRelationDTO.Name,
@@ -310,6 +320,6 @@ func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) {
Uom: p.ProductRelationDTO.Uom,
Flags: p.ProductRelationDTO.Flags,
ProductCategory: p.ProductRelationDTO.ProductCategory,
Suppliers: p.ProductRelationDTO.Suppliers,
Suppliers: suppliers,
})
}
@@ -31,11 +31,9 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
func resolveDebtSupplierDateColumn(filterBy string) string {
switch strings.ToLower(strings.TrimSpace(filterBy)) {
case "receive_date":
return "purchases.receive_date"
case "po_date":
return "purchases.po_date"
case "do_date", "received_date", "":
case "received_date", "":
return "purchase_items.received_date"
default:
return "purchase_items.received_date"
@@ -130,7 +128,7 @@ func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context
Preload("Warehouse.Area").
Order("purchase_items.id ASC")
if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "do_date") || strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" {
if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" {
if filters.StartDate != "" {
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
db = db.Where("DATE(purchase_items.received_date) >= ?", dateFrom)
@@ -59,7 +59,6 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
dataQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Preload("BodyWeights").
Preload("Eggs", func(db *gorm.DB) *gorm.DB {
return db.Select("recording_eggs.*, f.name AS product_flag_name").
Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id").
@@ -858,7 +858,7 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
}
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") {
if params.FilterBy == "" {
params.FilterBy = "received_date"
}
@@ -897,25 +897,8 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
}
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
references := make([]string, 0)
seenRefs := make(map[string]struct{})
for _, purchase := range purchases {
supplierID := purchase.SupplierId
purchasesBySupplier[supplierID] = append(purchasesBySupplier[supplierID], purchase)
reference := purchase.PrNumber
if purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" {
reference = *purchase.PoNumber
}
if _, exists := seenRefs[reference]; !exists {
seenRefs[reference] = struct{}{}
references = append(references, reference)
}
}
paymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsByReferences(c.Context(), supplierIDs, references)
if err != nil {
return nil, 0, err
purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase)
}
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
@@ -940,6 +923,14 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
now := time.Now().In(location)
result := make([]dto.DebtSupplierDTO, 0, len(supplierIDs))
type debtSupplierRowItem struct {
Row dto.DebtSupplierRowDTO
SortTime time.Time
Order int
DeltaBalance float64
CountTotals bool
}
for _, supplierID := range supplierIDs {
supplier, exists := supplierMap[supplierID]
if !exists {
@@ -947,23 +938,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
}
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
items := purchasesBySupplier[supplierID]
paymentItems := paymentsBySupplier[supplierID]
rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems))
total := dto.DebtSupplierTotalDTO{}
type debtSupplierRowItem struct {
Row dto.DebtSupplierRowDTO
SortTime time.Time
Order int
DeltaBalance float64
CountTotals bool
}
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
for _, purchase := range items {
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
row := buildDebtSupplierRow(purchase, now, location)
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row,
@@ -996,6 +977,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
balance := initialBalance
for i := range combinedRows {
balance += combinedRows[i].DeltaBalance
combinedRows[i].Row.DebtPrice = balance
combinedRows[i].Row.Balance = balance
if combinedRows[i].CountTotals {
@@ -1004,13 +986,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
total.Aging = row.Aging
}
total.TotalPrice += row.TotalPrice
total.PaymentPrice += row.PaymentPrice
total.DebtPrice += row.DebtPrice
} else {
combinedRows[i].Row.DebtPrice = balance
total.PaymentPrice += combinedRows[i].Row.PaymentPrice
}
}
total.DebtPrice = balance
rows := make([]dto.DebtSupplierRowDTO, 0, len(combinedRows))
sortDesc := strings.EqualFold(params.SortOrder, "desc")
if sortDesc {
for i := len(combinedRows) - 1; i >= 0; i-- {
@@ -1039,18 +1021,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
return result, totalSuppliers, nil
}
func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]float64, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
prNumber := purchase.PrNumber
poNumber := ""
if purchase.PoNumber != nil {
poNumber = *purchase.PoNumber
}
reference := prNumber
if strings.TrimSpace(poNumber) != "" {
reference = poNumber
}
prDate := purchase.CreatedAt.In(loc)
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
@@ -1093,9 +1070,6 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
}
}
paymentPrice := paymentTotals[reference]
debtPrice := paymentPrice - totalPrice
dueDate := ""
dueStatus := "-"
if purchase.DueDate != nil && !purchase.DueDate.IsZero() {
@@ -1109,10 +1083,6 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
}
status := "Belum Lunas"
if debtPrice >= 0 {
status = "Lunas"
}
poDate := ""
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
poDate = purchase.PoDate.In(loc).Format("2006-01-02")
@@ -1129,10 +1099,11 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
DueDate: dueDate,
DueStatus: dueStatus,
TotalPrice: totalPrice,
PaymentPrice: paymentPrice,
DebtPrice: debtPrice,
PaymentPrice: 0,
DebtPrice: 0,
Status: status,
TravelNumber: travelNumber,
Balance: 0,
}
}
@@ -1162,32 +1133,30 @@ func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto
DebtPrice: 0,
Status: "Pembayaran",
TravelNumber: "-",
Balance: 0,
}
}
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
switch strings.ToLower(strings.TrimSpace(filterBy)) {
case "po_date":
if strings.EqualFold(strings.TrimSpace(filterBy), "po_date") {
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
return purchase.PoDate.In(loc)
}
case "pr_date":
return purchase.CreatedAt.In(loc)
default:
earliest := time.Time{}
for _, item := range purchase.Items {
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
continue
}
received := item.ReceivedDate.In(loc)
if earliest.IsZero() || received.Before(earliest) {
earliest = received
}
}
earliest := time.Time{}
for _, item := range purchase.Items {
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
continue
}
if !earliest.IsZero() {
return earliest
received := item.ReceivedDate.In(loc)
if earliest.IsZero() || received.Before(earliest) {
earliest = received
}
}
if !earliest.IsZero() {
return earliest
}
return purchase.CreatedAt.In(loc)
}
@@ -50,7 +50,7 @@ type DebtSupplierQuery struct {
SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date pr_date do_date"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
}