feat[BE-384]: enhance reporting by adding chickin quantity and egg production weight calculations; refactor HPP calculations to consider product categories

This commit is contained in:
aguhh18
2025-12-18 17:56:18 +07:00
parent c95f90f0b9
commit e551995c66
6 changed files with 157 additions and 110 deletions
@@ -1,14 +1,15 @@
package dto
import (
"fmt"
"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"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/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"
)
type RepportMarketingItemDTO struct {
@@ -45,7 +46,7 @@ type RepportMarketingResponseDTO struct {
Total *Summary `json:"total,omitempty"`
}
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO {
func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO {
soDate := time.Time{}
agingDays := 0
if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 {
@@ -58,11 +59,17 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
realizationDate = *mdp.DeliveryDate
}
doNumber := generateDoNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId)
doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId)
totalWeightKg := mdp.Qty * mdp.AvgWeight
salesAmount := totalWeightKg * mdp.UnitPrice
hppAmount := totalWeightKg * hppPricePerKg
var hpp float64
var hppAmount float64
if isProductEligibleForHpp(mdp, category) {
hpp = hppPricePerKg
hppAmount = totalWeightKg * hppPricePerKg
}
item := RepportMarketingItemDTO{
ID: int(mdp.Id),
@@ -70,12 +77,12 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
RealizationDate: realizationDate,
AgingDays: agingDays,
DoNumber: doNumber,
MarketingType: "ayam",
MarketingType: getMarketingType(mdp),
Qty: mdp.Qty,
AverageWeightKg: mdp.AvgWeight,
TotalWeightKg: totalWeightKg,
SalesPricePerKg: mdp.UnitPrice,
HppPricePerKg: hppPricePerKg,
HppPricePerKg: hpp,
SalesAmount: salesAmount,
HppAmount: hppAmount,
}
@@ -105,10 +112,10 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
return item
}
func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO {
func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) []RepportMarketingItemDTO {
items := make([]RepportMarketingItemDTO, 0, len(mdps))
for _, mdp := range mdps {
items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg))
items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg, category))
}
return items
}
@@ -117,23 +124,72 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct
items := make([]RepportMarketingItemDTO, 0, len(mdps))
for _, mdp := range mdps {
hppPerKg := float64(0)
category := ""
if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists {
hppPerKg = hpp
}
category = projectFlockKandang.ProjectFlock.Category
}
items = append(items, ToRepportMarketingItemDTO(mdp, hppPerKg))
item := ToRepportMarketingItemDTO(mdp, hppPerKg, category)
items = append(items, item)
}
return items
}
func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary {
func getMarketingType(mdp entity.MarketingDeliveryProduct) string {
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if hasAyam {
return "ayam"
}
if hasTelur {
return "telur"
}
return "trading"
}
func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) {
if len(flags) == 0 {
return false, false
}
for _, flag := range flags {
ft := utils.FlagType(flag.Name)
if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati ||
ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer {
hasAyam = true
}
if ft == utils.FlagTelur || ft == utils.FlagTelurUtuh || ft == utils.FlagTelurPecah ||
ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak {
hasTelur = true
}
}
return hasAyam, hasTelur
}
func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool {
hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags)
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
return hasAyam
}
return hasAyam || hasTelur
}
func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) *Summary {
if len(mdps) == 0 {
return nil
}
totalQty := 0
totalWeightKg := 0.0
totalEligibleWeightKg := 0.0
totalSalesAmount := int64(0)
totalHppAmount := int64(0)
@@ -142,12 +198,16 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S
totalQty += int(mdp.Qty)
totalWeightKg += calculatedTotalWeight
totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice)
totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg)
if isProductEligibleForHpp(mdp, category) {
totalEligibleWeightKg += calculatedTotalWeight
totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg)
}
}
totalHppPricePerKg := float64(0)
if totalWeightKg > 0 {
totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg
if totalEligibleWeightKg > 0 {
totalHppPricePerKg = float64(totalHppAmount) / totalEligibleWeightKg
}
return &Summary{
@@ -159,14 +219,6 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S
}
}
func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string {
dateStr := ""
if deliveryDate != nil {
dateStr = deliveryDate.Format("20060102")
}
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId)
}
func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
if len(items) == 0 {
return nil
@@ -198,9 +250,9 @@ func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary {
}
}
func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO {
items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg)
total := ToSummary(mdps, hppPricePerKg)
func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingResponseDTO {
items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg, category)
total := ToSummary(mdps, hppPricePerKg, category)
return RepportMarketingResponseDTO{
Items: items,
+3 -1
View File
@@ -11,6 +11,7 @@ import (
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
)
@@ -22,11 +23,12 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db)
marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db)
purchaseRepository := purchaseRepo.NewPurchaseRepository(db)
chickinRepository := chickinRepo.NewChickinRepository(db)
recordingRepository := recordingRepo.NewRecordingRepository(db)
approvalRepository := commonRepo.NewApprovalRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, recordingRepository, approvalSvc)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc)
RepportRoutes(router, repportService)
}
@@ -3,7 +3,6 @@ package service
import (
"context"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -12,6 +11,7 @@ import (
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
@@ -32,17 +32,19 @@ type repportService struct {
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
PurchaseRepo purchaseRepo.PurchaseRepository
ChickinRepo chickinRepo.ProjectChickinRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService
}
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService {
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, chickinRepo chickinRepo.ProjectChickinRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService {
return &repportService{
Log: utils.Log,
Validate: validate,
ExpenseRealizationRepo: expenseRealizationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo,
PurchaseRepo: purchaseRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc,
}
@@ -98,74 +100,63 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
return nil, 0, err
}
projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts)
hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts)
items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap)
projectFlockIDMap := make(map[uint]bool)
hppMap := make(map[uint]float64)
for _, dp := range deliveryProducts {
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
projectFlockID := projectFlockKandang.ProjectFlockId
if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] {
projectFlockIDMap[projectFlockID] = true
category := projectFlockKandang.ProjectFlock.Category
hppPerKg := s.calculateHppPricePerKg(c.Context(), projectFlockID, category)
hppMap[projectFlockID] = hppPerKg
}
}
}
items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap)
return items, total, nil
}
func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.MarketingDeliveryProduct) []uint {
projectFlockIDMap := make(map[uint]bool)
projectFlockIDs := make([]uint, 0)
for _, dp := range deliveryProducts {
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
if projectFlockKandang.ProjectFlockId > 0 && !projectFlockIDMap[projectFlockKandang.ProjectFlockId] {
projectFlockIDs = append(projectFlockIDs, projectFlockKandang.ProjectFlockId)
projectFlockIDMap[projectFlockKandang.ProjectFlockId] = true
}
}
}
return projectFlockIDs
}
func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 {
hppMap := make(map[uint]float64)
for _, projectFlockID := range projectFlockIDs {
category := s.getProjectFlockCategory(deliveryProducts, projectFlockID)
hppPerKg := s.calculateHppByCategory(ctx, category, projectFlockID, deliveryProducts)
hppMap[projectFlockID] = hppPerKg
}
return hppMap
}
func (s *repportService) calculateHppByCategory(ctx context.Context, category string, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 {
switch utils.ProjectFlockCategory(category) {
case utils.ProjectFlockCategoryGrowing:
return s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts)
case utils.ProjectFlockCategoryLaying:
return 0
default:
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 {
totalCost := s.getTotalProjectCost(ctx, projectFlockID)
if totalCost == 0 {
return 0
}
}
func (s *repportService) getProjectFlockCategory(deliveryProducts []entity.MarketingDeliveryProduct, projectFlockID uint) string {
for _, dp := range deliveryProducts {
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
if projectFlockKandang.ProjectFlockId == projectFlockID {
return projectFlockKandang.ProjectFlock.Category
}
}
chickinQty, _ := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID)
depletion, _ := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID)
avgWeight, _ := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID)
var totalWeight float64
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
totalWeight = (chickinQty - depletion) * avgWeight
} else {
eggWeight, _ := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID)
totalWeight = (chickinQty-depletion)*avgWeight + eggWeight
}
return ""
if totalWeight == 0 {
return 0
}
return totalCost / totalWeight
}
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 {
func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 {
if projectFlockID == 0 {
return 0
}
purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID)
purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("GetItemsByProjectFlockID error: %v", err)
}
costPurchase := float64(0)
for _, item := range purchaseItems {
costPurchase += item.TotalPrice
cost := float64(0)
for _, p := range purchases {
cost += p.TotalPrice
}
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID)
@@ -173,34 +164,11 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc
s.Log.Warnf("GetByProjectFlockID error: %v", err)
}
costBop := float64(0)
for _, realization := range realizations {
cost := realization.Price * realization.Qty
category := ""
if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
category = realization.ExpenseNonstock.Expense.Category
}
if category == "BOP" {
costBop += cost
for _, r := range realizations {
if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil &&
r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) {
cost += r.Price * r.Qty
}
}
totalActualCost := costPurchase + costBop
if totalActualCost == 0 {
return 0
}
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
}
if totalWeightProduced == 0 {
return 0
}
hppPerKg := totalActualCost / totalWeightProduced
return hppPerKg
return cost
}