Compare commits

..

3 Commits

Author SHA1 Message Date
giovanni c3eab60f49 adjust list marketing 2026-06-06 08:38:38 +07:00
giovanni 255e6a16d3 add validate query param 2026-06-03 09:43:34 +07:00
giovanni 93ed89b4ef ini api per farm 2026-06-03 00:30:41 +07:00
14 changed files with 935 additions and 72 deletions
@@ -200,9 +200,12 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType) salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType)
} }
} }
var grandTotalSO float64 var grandTotalSO, grandTotalDO float64
for _, p := range marketing.Products { for _, p := range marketing.Products {
grandTotalSO += p.TotalPrice grandTotalSO += p.TotalPrice
if p.DeliveryProduct != nil && p.DeliveryProduct.DeliveryDate != nil {
grandTotalDO += p.DeliveryProduct.TotalPrice
}
} }
return MarketingListDTO{ return MarketingListDTO{
@@ -211,7 +214,7 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
SalesPerson: salesPerson, SalesPerson: salesPerson,
SoDocs: marketing.SoDocs, SoDocs: marketing.SoDocs,
GrandTotalSO: grandTotalSO, GrandTotalSO: grandTotalSO,
GrandTotalDO: marketing.GrandTotal, GrandTotalDO: grandTotalDO,
SalesOrder: salesOrderProducts, SalesOrder: salesOrderProducts,
DeliveryOrder: extractDeliveryGroupsFromProducts(marketing), DeliveryOrder: extractDeliveryGroupsFromProducts(marketing),
CreatedUser: createdUser, CreatedUser: createdUser,
@@ -172,23 +172,6 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
}) })
} }
func (u *ChickinController) UpdateChickInDate(c *fiber.Ctx) error {
req := new(validation.UpdateChickInDate)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.ChickinService.UpdateChickInDate(c, req); err != nil {
return err
}
return c.Status(fiber.StatusOK).JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Chick in date berhasil diperbarui",
})
}
func (u *ChickinController) Approval(c *fiber.Ctx) error { func (u *ChickinController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve) req := new(validation.Approve)
@@ -2,7 +2,6 @@ package repository
import ( import (
"context" "context"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -19,7 +18,6 @@ type ProjectChickinRepository interface {
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error
UpdateChickInDateByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, pfkID uint, newDate time.Time) error
} }
type ChickinRepositoryImpl struct { type ChickinRepositoryImpl struct {
@@ -136,10 +134,3 @@ func (r *ChickinRepositoryImpl) UpdateUsageFields(ctx context.Context, tx *gorm.
"pending_usage_qty": pendingUsageQty, "pending_usage_qty": pendingUsageQty,
}).Error }).Error
} }
func (r *ChickinRepositoryImpl) UpdateChickInDateByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, pfkID uint, newDate time.Time) error {
return tx.WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id = ? AND deleted_at IS NULL", pfkID).
Update("chick_in_date", newDate).Error
}
@@ -17,7 +17,6 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
route.Get("/", m.RequirePermissions(m.P_ChickinsGetAll), ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_ChickinsGetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne)
route.Patch("/chick-in-date", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.UpdateChickInDate)
route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne) route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne)
// route.Patch("/:id", ctrl.UpdateOne) // route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
@@ -48,7 +48,6 @@ type ChickinService interface {
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error
UpdateChickInDate(ctx *fiber.Ctx, req *validation.UpdateChickInDate) error
} }
type chickinService struct { type chickinService struct {
@@ -2111,38 +2110,3 @@ func (s chickinService) EnsureChickInExists(ctx context.Context, projectFlockKan
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording") return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
} }
func (s chickinService) UpdateChickInDate(ctx *fiber.Ctx, req *validation.UpdateChickInDate) error {
if err := s.Validate.Struct(req); err != nil {
return err
}
newDate, err := time.Parse("2006-01-02", req.ChickInDate)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Format tanggal tidak valid, gunakan YYYY-MM-DD")
}
_, err = s.ProjectflockKandangRepo.GetByID(ctx.Context(), req.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Project flock kandang tidak ditemukan")
}
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
if err := s.Repository.UpdateChickInDateByProjectFlockKandangID(ctx.Context(), tx, req.ProjectFlockKandangId, newDate); err != nil {
return err
}
return tx.Exec(`
UPDATE recordings
SET day = GREATEST(0, (record_datetime::date - ?::date)::int),
updated_at = NOW()
WHERE project_flock_kandangs_id = ?
AND deleted_at IS NULL
`, req.ChickInDate, req.ProjectFlockKandangId).Error
}); err != nil {
return err
}
s.invalidateDepreciationSnapshots(ctx.Context(), nil, []uint{req.ProjectFlockKandangId}, newDate)
return nil
}
@@ -27,8 +27,3 @@ type Approve struct {
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type UpdateChickInDate struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,gt=0"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
}
@@ -457,6 +457,29 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(resp) return ctx.Status(fiber.StatusOK).JSON(resp)
} }
func (c *RepportController) GetHppPerFarm(ctx *fiber.Ctx) error {
data, meta, err := c.RepportService.GetHppPerFarm(ctx)
if err != nil {
return err
}
resp := struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Meta dto.HppPerFarmMetaDTO `json:"meta"`
Data dto.HppPerFarmResponseData `json:"data"`
}{
Code: fiber.StatusOK,
Status: "success",
Message: "Get HPP per farm successfully",
Meta: *meta,
Data: *data,
}
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
var customerIDs []uint var customerIDs []uint
if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" { if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" {
@@ -0,0 +1,79 @@
package dto
type HppPerFarmFiltersDTO struct {
AreaID string `json:"area_id"`
LocationID string `json:"location_id"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
type HppPerFarmMetaDTO struct {
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int64 `json:"total_pages"`
TotalResults int64 `json:"total_results"`
Filters HppPerFarmFiltersDTO `json:"filters"`
}
type HppPerFarmResponseData struct {
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Rows []HppPerFarmRowDTO `json:"rows"`
Summary HppPerFarmSummaryDTO `json:"summary"`
}
// HppPerFarmRowDTO is one farm (location) row, aggregating all LAYING project
// flocks within the same location over the selected date range.
type HppPerFarmRowDTO struct {
Location HppPerKandangLocationDTO `json:"location"`
// total_cost_rp = depreciation + pakan + ovk + bop (+ other production cost).
// DOC/pullet is NOT included here (it is expensed through depreciation);
// average_doc_price_rp is provided for information only.
TotalCostRp float64 `json:"total_cost_rp"`
FeedCostRp float64 `json:"feed_cost_rp"`
OvkCostRp float64 `json:"ovk_cost_rp"`
BopCostRp float64 `json:"bop_cost_rp"`
DepreciationRp float64 `json:"depreciation_rp"`
OtherCostRp float64 `json:"other_cost_rp"`
EggWeightRecordingKg float64 `json:"egg_weight_recording_kg"`
EggWeightDoKg float64 `json:"egg_weight_do_kg"`
HppPerKgProduction float64 `json:"hpp_per_kg_production"`
HppPerKgSales float64 `json:"hpp_per_kg_sales"`
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
Flocks []HppPerFarmFlockDTO `json:"flocks"`
}
// HppPerFarmFlockDTO is the per-project-flock breakdown inside a farm row.
type HppPerFarmFlockDTO struct {
ProjectFlockID int64 `json:"project_flock_id"`
FlockName string `json:"flock_name"`
TotalCostRp float64 `json:"total_cost_rp"`
FeedCostRp float64 `json:"feed_cost_rp"`
OvkCostRp float64 `json:"ovk_cost_rp"`
BopCostRp float64 `json:"bop_cost_rp"`
DepreciationRp float64 `json:"depreciation_rp"`
OtherCostRp float64 `json:"other_cost_rp"`
EggWeightRecordingKg float64 `json:"egg_weight_recording_kg"`
EggWeightDoKg float64 `json:"egg_weight_do_kg"`
HppPerKgProduction float64 `json:"hpp_per_kg_production"`
HppPerKgSales float64 `json:"hpp_per_kg_sales"`
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
}
type HppPerFarmSummaryDTO struct {
TotalCostRp float64 `json:"total_cost_rp"`
TotalEggWeightRecordingKg float64 `json:"total_egg_weight_recording_kg"`
TotalEggWeightDoKg float64 `json:"total_egg_weight_do_kg"`
AverageHppPerKgProduction float64 `json:"average_hpp_per_kg_production"`
AverageHppPerKgSales float64 `json:"average_hpp_per_kg_sales"`
}
func NewHppPerFarmFiltersDTO(area, location, startDate, endDate string) HppPerFarmFiltersDTO {
return HppPerFarmFiltersDTO{
AreaID: area,
LocationID: location,
StartDate: startDate,
EndDate: endDate,
}
}
+2
View File
@@ -37,6 +37,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db) debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
hppPerFarmRepository := repportRepo.NewHppPerFarmRepository(db)
expenseDepreciationRepository := repportRepo.NewExpenseDepreciationRepository(db) expenseDepreciationRepository := repportRepo.NewExpenseDepreciationRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db)
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db) customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
@@ -65,6 +66,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
purchaseSupplierRepository, purchaseSupplierRepository,
debtSupplierRepository, debtSupplierRepository,
hppPerKandangRepository, hppPerKandangRepository,
hppPerFarmRepository,
productionResultRepository, productionResultRepository,
customerPaymentRepository, customerPaymentRepository,
balanceMonitoringRepository, balanceMonitoringRepository,
@@ -0,0 +1,233 @@
package repositories
import (
"context"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
// HppPerFarmFlockMetaRow describes a LAYING project flock and the farm
// (location) it belongs to. Farm identity is project_flocks.location_id.
type HppPerFarmFlockMetaRow struct {
ProjectFlockID uint
FlockName string
LocationID uint
LocationName string
AreaID uint
}
// HppPerFarmDocRow holds the DOC/pullet acquisition cost trace per flock.
// Used only as an informational field (average_doc_price_rp); it is NOT part
// of total_cost because the pullet cost is expensed through depreciation.
type HppPerFarmDocRow struct {
ProjectFlockID uint
DocCost float64
DocQty float64
}
type HppPerFarmRepository interface {
GetCandidateFlocks(ctx context.Context, start time.Time, areaIDs, locationIDs []int64) ([]HppPerFarmFlockMetaRow, error)
SumRecordingEggWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error)
SumMarketingDoTelurWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error)
GetDocCostByFlock(ctx context.Context, projectFlockIDs []uint) (map[uint]HppPerFarmDocRow, error)
DB() *gorm.DB
}
type hppPerFarmRepository struct {
db *gorm.DB
}
func NewHppPerFarmRepository(db *gorm.DB) HppPerFarmRepository {
return &hppPerFarmRepository{db: db}
}
func (r *hppPerFarmRepository) DB() *gorm.DB {
return r.db
}
// GetCandidateFlocks returns the LAYING project flocks (with their farm/location
// metadata) that are still active on or after the range start, scoped by area
// and location. Mirrors ExpenseDepreciationRepository.GetCandidateFarms but adds
// location info so flocks can be grouped per farm.
func (r *hppPerFarmRepository) GetCandidateFlocks(ctx context.Context, start time.Time, areaIDs, locationIDs []int64) ([]HppPerFarmFlockMetaRow, error) {
rows := make([]HppPerFarmFlockMetaRow, 0)
query := r.db.WithContext(ctx).
Table("project_flocks AS pf").
Select(`
DISTINCT pf.id AS project_flock_id,
pf.flock_name AS flock_name,
pf.location_id AS location_id,
loc.name AS location_name,
pf.area_id AS area_id`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins("JOIN locations AS loc ON loc.id = pf.location_id").
Where("pf.deleted_at IS NULL").
Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
Where("(pfk.closed_at IS NULL OR DATE(pfk.closed_at) >= DATE(?))", start)
if len(areaIDs) > 0 {
query = query.Where("pf.area_id IN ?", areaIDs)
}
if len(locationIDs) > 0 {
query = query.Where("pf.location_id IN ?", locationIDs)
}
if err := query.Order("pf.location_id ASC, pf.id ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
// SumRecordingEggWeightByFlock sums recording_eggs.weight (kg) per project flock
// for non-rejected recordings whose record_datetime falls inside [start, endExclusive).
func (r *hppPerFarmRepository) SumRecordingEggWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error) {
result := make(map[uint]float64)
if len(projectFlockIDs) == 0 {
return result, nil
}
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
type eggRow struct {
ProjectFlockID uint
Weight float64
}
rows := make([]eggRow, 0)
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
pfk.project_flock_id AS project_flock_id,
COALESCE(SUM(re.weight), 0) AS weight`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("pfk.project_flock_id IN ?", projectFlockIDs).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, endExclusive).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Group("pfk.project_flock_id")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
result[row.ProjectFlockID] = row.Weight
}
return result, nil
}
// SumMarketingDoTelurWeightByFlock sums delivered TELUR weight (marketing_delivery_products.total_weight)
// per project flock, for delivery_date inside [start, endExclusive). A delivery product that is
// attributed to multiple flocks is prorated by each flock's allocated qty share, so that
// the farm total equals the sum of its flocks.
func (r *hppPerFarmRepository) SumMarketingDoTelurWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error) {
result := make(map[uint]float64)
if len(projectFlockIDs) == 0 {
return result, nil
}
telurFlags := []string{
string(utils.FlagTelur),
string(utils.FlagTelurUtuh),
string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
}
// allocated qty per (marketing_delivery_product, project_flock)
attrByFlock := r.db.WithContext(ctx).
Table("(?) AS mda", commonRepo.MarketingDeliveryAttributionRowsQuery(r.db.WithContext(ctx))).
Select(`
mda.marketing_delivery_product_id AS mdp_id,
mda.project_flock_id AS project_flock_id,
SUM(mda.allocated_qty) AS flock_qty`).
Group("mda.marketing_delivery_product_id, mda.project_flock_id")
// prorate each delivery product's total_weight across its attributed flocks.
// Use EXISTS for the TELUR flag filter (not a JOIN) so a product carrying
// multiple egg flags does not fan out and double-count the weight share.
shareQuery := r.db.WithContext(ctx).
Table("(?) AS a", attrByFlock).
Select(`
a.project_flock_id AS project_flock_id,
mdp.total_weight * a.flock_qty / NULLIF(SUM(a.flock_qty) OVER (PARTITION BY a.mdp_id), 0) AS weight_share`).
Joins("JOIN marketing_delivery_products AS mdp ON mdp.id = a.mdp_id").
Joins("JOIN marketing_products AS mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN product_warehouses AS pw ON pw.id = mp.product_warehouse_id").
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, telurFlags).
Where("mdp.delivery_date >= ? AND mdp.delivery_date < ?", start, endExclusive)
type doRow struct {
ProjectFlockID uint
Weight float64
}
rows := make([]doRow, 0)
query := r.db.WithContext(ctx).
Table("(?) AS s", shareQuery).
Select(`
s.project_flock_id AS project_flock_id,
COALESCE(SUM(s.weight_share), 0) AS weight`).
Where("s.project_flock_id IN ?", projectFlockIDs).
Group("s.project_flock_id")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
result[row.ProjectFlockID] = row.Weight
}
return result, nil
}
// GetDocCostByFlock returns the DOC acquisition cost (qty * purchase price) and qty
// traced to chick-in per project flock. Informational only.
func (r *hppPerFarmRepository) GetDocCostByFlock(ctx context.Context, projectFlockIDs []uint) (map[uint]HppPerFarmDocRow, error) {
result := make(map[uint]HppPerFarmDocRow)
if len(projectFlockIDs) == 0 {
return result, nil
}
rows := make([]HppPerFarmDocRow, 0)
query := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select(`
pfk.project_flock_id AS project_flock_id,
COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
COALESCE(SUM(sa.qty), 0) AS doc_qty`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyProjectChickin.String(), fifo.StockableKeyPurchaseItems.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("pfk.project_flock_id IN ?", projectFlockIDs).
Group("pfk.project_flock_id")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
result[row.ProjectFlockID] = row
}
return result, nil
}
+1
View File
@@ -23,6 +23,7 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier)
route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang) route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang)
route.Get("/hpp-per-farm", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerFarm)
route.Get("/hpp-v2-breakdown", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppV2Breakdown) route.Get("/hpp-v2-breakdown", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppV2Breakdown)
route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult) route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult)
route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment) route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment)
@@ -0,0 +1,93 @@
package service
import (
"math"
"testing"
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
)
// production-scope total should sum only parts tagged production_cost (a part
// tagged with both scopes still counts once).
func TestHppPerFarmProductionScopeTotalPartLevelScopes(t *testing.T) {
comp := &approvalService.HppV2Component{
Code: "PAKAN",
Parts: []approvalService.HppV2ComponentPart{
{Total: 100, Scopes: []string{"production_cost"}},
{Total: 50, Scopes: []string{"pullet_cost"}},
{Total: 25, Scopes: []string{"production_cost", "pullet_cost"}},
},
}
if got := hppPerFarmProductionScopeTotal(comp); got != 125 {
t.Fatalf("expected 125, got %v", got)
}
}
// when parts carry no scopes, fall back to the component-level scope.
func TestHppPerFarmProductionScopeTotalComponentLevelFallback(t *testing.T) {
prod := &approvalService.HppV2Component{
Code: "DIRECT_PULLET_PURCHASE",
Scopes: []string{"production_cost"},
Total: 300,
Parts: []approvalService.HppV2ComponentPart{{Total: 300}},
}
if got := hppPerFarmProductionScopeTotal(prod); got != 300 {
t.Fatalf("expected 300 component fallback, got %v", got)
}
// DOC/pullet is pullet-scope only -> contributes 0 to production cost,
// which is exactly why it must not be added to total_cost (depreciation
// already expenses the pullet).
pulletOnly := &approvalService.HppV2Component{
Code: "DOC_CHICKIN",
Scopes: []string{"pullet_cost"},
Total: 999,
Parts: []approvalService.HppV2ComponentPart{{Total: 999}},
}
if got := hppPerFarmProductionScopeTotal(pulletOnly); got != 0 {
t.Fatalf("expected 0 for pullet-only component, got %v", got)
}
}
func TestHppPerFarmProductionScopeTotalsByCode(t *testing.T) {
b := &approvalService.HppV2Breakdown{
Components: []approvalService.HppV2Component{
{Code: "PAKAN", Parts: []approvalService.HppV2ComponentPart{{Total: 100, Scopes: []string{"production_cost"}}}},
{Code: "OVK", Parts: []approvalService.HppV2ComponentPart{{Total: 40, Scopes: []string{"production_cost"}}}},
{Code: "DOC_CHICKIN", Scopes: []string{"pullet_cost"}, Total: 500, Parts: []approvalService.HppV2ComponentPart{{Total: 500}}},
{Code: "DEPRECIATION", Scopes: []string{"production_cost"}, Total: 30, Parts: []approvalService.HppV2ComponentPart{{Total: 30, Scopes: []string{"production_cost"}}}},
},
}
got := hppPerFarmProductionScopeTotalsByCode(b)
if got["PAKAN"] != 100 {
t.Fatalf("expected PAKAN 100, got %v", got["PAKAN"])
}
if got["OVK"] != 40 {
t.Fatalf("expected OVK 40, got %v", got["OVK"])
}
if got["DOC_CHICKIN"] != 0 {
t.Fatalf("expected DOC_CHICKIN production scope 0, got %v", got["DOC_CHICKIN"])
}
if got["DEPRECIATION"] != 30 {
t.Fatalf("expected DEPRECIATION 30, got %v", got["DEPRECIATION"])
}
}
func TestHppPerFarmSafeDiv(t *testing.T) {
cases := []struct {
num, den, want float64
}{
{100, 4, 25},
{100, 0, 0},
{100, -5, 0},
{0, 0, 0},
}
for _, c := range cases {
if got := hppPerFarmSafeDiv(c.num, c.den); got != c.want {
t.Fatalf("safeDiv(%v,%v)=%v want %v", c.num, c.den, got, c.want)
}
}
if got := hppPerFarmSafeDiv(math.Inf(1), 1); got != 0 {
t.Fatalf("expected 0 for inf numerator, got %v", got)
}
}
@@ -49,6 +49,7 @@ type RepportService interface {
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
GetHppPerFarm(ctx *fiber.Ctx) (*dto.HppPerFarmResponseData, *dto.HppPerFarmMetaDTO, error)
GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error)
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
@@ -73,6 +74,7 @@ type repportService struct {
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
DebtSupplierRepo repportRepo.DebtSupplierRepository DebtSupplierRepo repportRepo.DebtSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository HppPerKandangRepo repportRepo.HppPerKandangRepository
HppPerFarmRepo repportRepo.HppPerFarmRepository
ProductionResultRepo repportRepo.ProductionResultRepository ProductionResultRepo repportRepo.ProductionResultRepository
CustomerPaymentRepo repportRepo.CustomerPaymentRepository CustomerPaymentRepo repportRepo.CustomerPaymentRepository
BalanceMonitoringRepo repportRepo.BalanceMonitoringRepository BalanceMonitoringRepo repportRepo.BalanceMonitoringRepository
@@ -106,6 +108,7 @@ func NewRepportService(
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
debtSupplierRepo repportRepo.DebtSupplierRepository, debtSupplierRepo repportRepo.DebtSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository,
hppPerFarmRepo repportRepo.HppPerFarmRepository,
productionResultRepo repportRepo.ProductionResultRepository, productionResultRepo repportRepo.ProductionResultRepository,
customerPaymentRepo repportRepo.CustomerPaymentRepository, customerPaymentRepo repportRepo.CustomerPaymentRepository,
balanceMonitoringRepo repportRepo.BalanceMonitoringRepository, balanceMonitoringRepo repportRepo.BalanceMonitoringRepository,
@@ -130,6 +133,7 @@ func NewRepportService(
PurchaseSupplierRepo: purchaseSupplierRepo, PurchaseSupplierRepo: purchaseSupplierRepo,
DebtSupplierRepo: debtSupplierRepo, DebtSupplierRepo: debtSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo, HppPerKandangRepo: hppPerKandangRepo,
HppPerFarmRepo: hppPerFarmRepo,
ProductionResultRepo: productionResultRepo, ProductionResultRepo: productionResultRepo,
CustomerPaymentRepo: customerPaymentRepo, CustomerPaymentRepo: customerPaymentRepo,
BalanceMonitoringRepo: balanceMonitoringRepo, BalanceMonitoringRepo: balanceMonitoringRepo,
@@ -2945,6 +2949,490 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
return params, filters, nil return params, filters, nil
} }
const (
hppPerFarmProductionScope = "production_cost"
hppPerFarmComponentDepreciation = "DEPRECIATION"
hppPerFarmComponentPakan = "PAKAN"
hppPerFarmComponentOvk = "OVK"
hppPerFarmComponentBopRegular = "BOP_REGULAR"
hppPerFarmComponentBopEkspedisi = "BOP_EKSPEDISI"
hppPerFarmMaxRangeDays = 366
)
// GetHppPerFarm builds the HPP-per-farm report: it groups all LAYING project
// flocks by location/farm over [start_date, end_date] and reports, per farm,
// the total cost (pakan + ovk + bop + depreciation) and two cost-per-kg figures
// — one against egg weight produced (recording_eggs) and one against egg weight
// sold/delivered (marketing delivery orders). DOC/pullet cost is informational
// only (it is expensed through depreciation, so it is NOT added to total cost).
func (s *repportService) GetHppPerFarm(ctx *fiber.Ctx) (*dto.HppPerFarmResponseData, *dto.HppPerFarmMetaDTO, error) {
params, filters, err := s.parseHppPerFarmQuery(ctx)
if err != nil {
return nil, nil, err
}
if err := s.Validate.Struct(params); err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if s.HppPerFarmRepo == nil {
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "hpp per farm repository is not configured")
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
}
startDate, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "start_date must follow format YYYY-MM-DD")
}
endDate, err := time.ParseInLocation("2006-01-02", params.EndDate, location)
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "end_date must follow format YYYY-MM-DD")
}
if endDate.Before(startDate) {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
}
rangeDays := int(endDate.Sub(startDate).Hours()/24) + 1
if rangeDays > hppPerFarmMaxRangeDays {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "date range must not exceed 366 days")
}
startOfRange := time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, location)
endBreakdownDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, location)
endExclusive := endBreakdownDate.Add(24 * time.Hour)
startBreakdownDate := startOfRange.AddDate(0, 0, -1)
limit := params.Limit
if limit <= 0 {
limit = 10
}
flockRows, err := s.HppPerFarmRepo.GetCandidateFlocks(ctx.Context(), startOfRange, params.AreaIDs, params.LocationIDs)
if err != nil {
return nil, nil, err
}
if len(flockRows) == 0 {
meta := &dto.HppPerFarmMetaDTO{
Page: params.Page,
Limit: limit,
TotalPages: 1,
TotalResults: 0,
Filters: filters,
}
data := &dto.HppPerFarmResponseData{
StartDate: params.StartDate,
EndDate: params.EndDate,
Rows: []dto.HppPerFarmRowDTO{},
Summary: dto.HppPerFarmSummaryDTO{},
}
return data, meta, nil
}
flockIDs := make([]uint, 0, len(flockRows))
for _, row := range flockRows {
flockIDs = append(flockIDs, row.ProjectFlockID)
}
depByFlock, err := s.sumHppPerFarmDepreciationOverRange(ctx.Context(), startOfRange, endBreakdownDate, flockIDs)
if err != nil {
return nil, nil, err
}
recWeightByFlock, err := s.HppPerFarmRepo.SumRecordingEggWeightByFlock(ctx.Context(), startOfRange, endExclusive, flockIDs)
if err != nil {
return nil, nil, err
}
doWeightByFlock, err := s.HppPerFarmRepo.SumMarketingDoTelurWeightByFlock(ctx.Context(), startOfRange, endExclusive, flockIDs)
if err != nil {
return nil, nil, err
}
docByFlock, err := s.HppPerFarmRepo.GetDocCostByFlock(ctx.Context(), flockIDs)
if err != nil {
return nil, nil, err
}
type hppPerFarmAggregate struct {
locationID uint
locationName string
totalCost float64
feed float64
ovk float64
bop float64
depreciation float64
other float64
recWeight float64
doWeight float64
docCost float64
docQty float64
flocks []dto.HppPerFarmFlockDTO
}
farmOrder := make([]uint, 0)
farms := make(map[uint]*hppPerFarmAggregate)
for _, flock := range flockRows {
flockID := flock.ProjectFlockID
codeTotals, err := s.hppPerFarmFlockCostRange(ctx.Context(), flockID, startBreakdownDate, endBreakdownDate)
if err != nil {
return nil, nil, err
}
feed := codeTotals[hppPerFarmComponentPakan]
ovk := codeTotals[hppPerFarmComponentOvk]
bop := codeTotals[hppPerFarmComponentBopRegular] + codeTotals[hppPerFarmComponentBopEkspedisi]
nonDepreciation := 0.0
for _, value := range codeTotals {
nonDepreciation += value
}
other := nonDepreciation - feed - ovk - bop
depreciation := depByFlock[flockID]
totalCost := nonDepreciation + depreciation
recWeight := recWeightByFlock[flockID]
doWeight := doWeightByFlock[flockID]
averageDocPrice := int64(0)
if doc, ok := docByFlock[flockID]; ok && doc.DocQty > 0 {
averageDocPrice = int64(math.Round(doc.DocCost / doc.DocQty))
}
flockDTO := dto.HppPerFarmFlockDTO{
ProjectFlockID: int64(flockID),
FlockName: flock.FlockName,
TotalCostRp: totalCost,
FeedCostRp: feed,
OvkCostRp: ovk,
BopCostRp: bop,
DepreciationRp: depreciation,
OtherCostRp: other,
EggWeightRecordingKg: recWeight,
EggWeightDoKg: doWeight,
HppPerKgProduction: hppPerFarmSafeDiv(totalCost, recWeight),
HppPerKgSales: hppPerFarmSafeDiv(totalCost, doWeight),
AverageDocPriceRp: averageDocPrice,
}
farm, ok := farms[flock.LocationID]
if !ok {
farm = &hppPerFarmAggregate{
locationID: flock.LocationID,
locationName: flock.LocationName,
flocks: make([]dto.HppPerFarmFlockDTO, 0, 1),
}
farms[flock.LocationID] = farm
farmOrder = append(farmOrder, flock.LocationID)
}
farm.flocks = append(farm.flocks, flockDTO)
farm.totalCost += totalCost
farm.feed += feed
farm.ovk += ovk
farm.bop += bop
farm.depreciation += depreciation
farm.other += other
farm.recWeight += recWeight
farm.doWeight += doWeight
if doc, ok := docByFlock[flockID]; ok {
farm.docCost += doc.DocCost
farm.docQty += doc.DocQty
}
}
rows := make([]dto.HppPerFarmRowDTO, 0, len(farmOrder))
summary := dto.HppPerFarmSummaryDTO{}
for _, locID := range farmOrder {
farm := farms[locID]
averageDocPrice := int64(0)
if farm.docQty > 0 {
averageDocPrice = int64(math.Round(farm.docCost / farm.docQty))
}
rows = append(rows, dto.HppPerFarmRowDTO{
Location: dto.HppPerKandangLocationDTO{ID: int64(farm.locationID), Name: farm.locationName},
TotalCostRp: farm.totalCost,
FeedCostRp: farm.feed,
OvkCostRp: farm.ovk,
BopCostRp: farm.bop,
DepreciationRp: farm.depreciation,
OtherCostRp: farm.other,
EggWeightRecordingKg: farm.recWeight,
EggWeightDoKg: farm.doWeight,
HppPerKgProduction: hppPerFarmSafeDiv(farm.totalCost, farm.recWeight),
HppPerKgSales: hppPerFarmSafeDiv(farm.totalCost, farm.doWeight),
AverageDocPriceRp: averageDocPrice,
Flocks: farm.flocks,
})
summary.TotalCostRp += farm.totalCost
summary.TotalEggWeightRecordingKg += farm.recWeight
summary.TotalEggWeightDoKg += farm.doWeight
}
summary.AverageHppPerKgProduction = hppPerFarmSafeDiv(summary.TotalCostRp, summary.TotalEggWeightRecordingKg)
summary.AverageHppPerKgSales = hppPerFarmSafeDiv(summary.TotalCostRp, summary.TotalEggWeightDoKg)
totalResults := int64(len(rows))
totalPages := int64(1)
if totalResults > 0 {
totalPages = int64(math.Ceil(float64(totalResults) / float64(limit)))
}
offset := (params.Page - 1) * limit
if offset < 0 {
offset = 0
}
if offset > len(rows) {
offset = len(rows)
}
end := offset + limit
if end > len(rows) {
end = len(rows)
}
meta := &dto.HppPerFarmMetaDTO{
Page: params.Page,
Limit: limit,
TotalPages: totalPages,
TotalResults: totalResults,
Filters: filters,
}
data := &dto.HppPerFarmResponseData{
StartDate: params.StartDate,
EndDate: params.EndDate,
Rows: rows[offset:end],
Summary: summary,
}
return data, meta, nil
}
// hppPerFarmFlockCostRange returns the range-scoped production cost per component
// code for a project flock, EXCLUDING depreciation (which is summed separately
// from daily snapshots). Each non-depreciation production component is cumulative
// up to a date in the HPP v2 engine, so the range value is the difference between
// the cumulative breakdown at end and at the day before the range start.
func (s *repportService) hppPerFarmFlockCostRange(ctx context.Context, projectFlockID uint, startBreakdownDate, endBreakdownDate time.Time) (map[string]float64, error) {
if s.HppCostRepo == nil {
return nil, errors.New("hpp cost repository is not configured")
}
if s.HppV2Svc == nil {
return nil, errors.New("hpp v2 service is not configured")
}
codeTotals := make(map[string]float64)
pfkIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, projectFlockID)
if err != nil {
return nil, err
}
for _, pfkID := range pfkIDs {
endBreakdown, err := s.HppV2Svc.CalculateHppBreakdown(pfkID, &endBreakdownDate)
if err != nil {
return nil, err
}
startBreakdown, err := s.HppV2Svc.CalculateHppBreakdown(pfkID, &startBreakdownDate)
if err != nil {
return nil, err
}
endMap := hppPerFarmProductionScopeTotalsByCode(endBreakdown)
startMap := hppPerFarmProductionScopeTotalsByCode(startBreakdown)
seen := make(map[string]bool, len(endMap)+len(startMap))
for code := range endMap {
seen[code] = true
}
for code := range startMap {
seen[code] = true
}
for code := range seen {
if code == hppPerFarmComponentDepreciation {
continue
}
codeTotals[code] += endMap[code] - startMap[code]
}
}
return codeTotals, nil
}
// sumHppPerFarmDepreciationOverRange sums the daily depreciation_value from
// farm_depreciation_snapshots across [startDate, endDate] per project flock,
// computing (and persisting) any missing daily snapshot on demand — same lazy
// compute path the single-day depreciation report uses.
func (s *repportService) sumHppPerFarmDepreciationOverRange(ctx context.Context, startDate, endDate time.Time, projectFlockIDs []uint) (map[uint]float64, error) {
acc := make(map[uint]float64, len(projectFlockIDs))
if len(projectFlockIDs) == 0 {
return acc, nil
}
if s.ExpenseDepreciationRepo == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
}
for day := startDate; !day.After(endDate); day = day.AddDate(0, 0, 1) {
snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx, day, projectFlockIDs)
if err != nil {
return nil, err
}
byID := make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots))
for _, snapshot := range snapshots {
byID[snapshot.ProjectFlockId] = snapshot
}
missing := make([]uint, 0)
for _, id := range projectFlockIDs {
if _, ok := byID[id]; !ok {
missing = append(missing, id)
}
}
if len(missing) > 0 {
computed, err := s.computeExpenseDepreciationSnapshots(ctx, day, missing, nil)
if err != nil {
return nil, err
}
if len(computed) > 0 {
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx, computed); err != nil {
return nil, err
}
for _, snapshot := range computed {
byID[snapshot.ProjectFlockId] = snapshot
}
}
}
for id, snapshot := range byID {
acc[id] += snapshot.DepreciationValue
}
}
return acc, nil
}
func hppPerFarmProductionScopeTotalsByCode(breakdown *approvalService.HppV2Breakdown) map[string]float64 {
out := make(map[string]float64)
if breakdown == nil {
return out
}
for i := range breakdown.Components {
comp := &breakdown.Components[i]
out[comp.Code] += hppPerFarmProductionScopeTotal(comp)
}
return out
}
// hppPerFarmProductionScopeTotal mirrors the engine's componentScopeTotal for the
// production_cost scope (that helper is unexported in the common service package).
func hppPerFarmProductionScopeTotal(component *approvalService.HppV2Component) float64 {
if component == nil {
return 0
}
total := 0.0
hasPartScopes := false
for i := range component.Parts {
part := &component.Parts[i]
if len(part.Scopes) == 0 {
continue
}
hasPartScopes = true
for _, scope := range part.Scopes {
if scope == hppPerFarmProductionScope {
total += part.Total
break
}
}
}
if hasPartScopes {
return total
}
for _, scope := range component.Scopes {
if scope == hppPerFarmProductionScope {
return component.Total
}
}
return 0
}
func hppPerFarmSafeDiv(numerator, denominator float64) float64 {
if denominator <= 0 {
return 0
}
value := numerator / denominator
if math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return value
}
func (s *repportService) parseHppPerFarmQuery(ctx *fiber.Ctx) (*validation.HppPerFarmQuery, dto.HppPerFarmFiltersDTO, error) {
page := ctx.QueryInt("page", 1)
if page < 1 {
page = 1
}
limit := ctx.QueryInt("limit", 10)
if limit < 1 {
limit = 10
}
rawArea := ctx.Query("area_id", "")
rawLocation := ctx.Query("location_id", "")
startDate := ctx.Query("start_date", "")
endDate := ctx.Query("end_date", "")
if strings.TrimSpace(startDate) == "" {
return nil, dto.HppPerFarmFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "start_date is required")
}
if strings.TrimSpace(endDate) == "" {
return nil, dto.HppPerFarmFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "end_date is required")
}
if strings.TrimSpace(rawLocation) == "" {
return nil, dto.HppPerFarmFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
}
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
if err != nil {
return nil, dto.HppPerFarmFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
locationIDs, err := parseCommaSeparatedInt64s(rawLocation)
if err != nil {
return nil, dto.HppPerFarmFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB())
if err != nil {
return nil, dto.HppPerFarmFiltersDTO{}, err
}
areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB())
if err != nil {
return nil, dto.HppPerFarmFiltersDTO{}, err
}
if locationScope.Restrict {
allowed := toInt64Slice(locationScope.IDs)
if len(allowed) == 0 {
locationIDs = []int64{-1}
} else if len(locationIDs) > 0 {
locationIDs = intersectInt64(locationIDs, allowed)
} else {
locationIDs = allowed
}
}
if areaScope.Restrict {
allowed := toInt64Slice(areaScope.IDs)
if len(allowed) == 0 {
areaIDs = []int64{-1}
} else if len(areaIDs) > 0 {
areaIDs = intersectInt64(areaIDs, allowed)
} else {
areaIDs = allowed
}
}
params := &validation.HppPerFarmQuery{
Page: page,
Limit: limit,
StartDate: startDate,
EndDate: endDate,
AreaIDs: areaIDs,
LocationIDs: locationIDs,
}
filters := dto.NewHppPerFarmFiltersDTO(rawArea, rawLocation, startDate, endDate)
return params, filters, nil
}
func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validation.ExpenseDepreciationQuery, dto.ExpenseDepreciationFiltersDTO, error) { func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validation.ExpenseDepreciationQuery, dto.ExpenseDepreciationFiltersDTO, error) {
page := ctx.QueryInt("page", 1) page := ctx.QueryInt("page", 1)
if page < 1 { if page < 1 {
@@ -78,6 +78,15 @@ type HppPerKandangQuery struct {
WeightMax *float64 `query:"-"` WeightMax *float64 `query:"-"`
} }
type HppPerFarmQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
StartDate string `query:"start_date" validate:"required,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"required,datetime=2006-01-02"`
AreaIDs []int64 `query:"-"`
LocationIDs []int64 `query:"-"`
}
type HppV2BreakdownQuery struct { type HppV2BreakdownQuery struct {
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"required,gt=0"` ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"required,gt=0"`
Period string `query:"period" validate:"required,datetime=2006-01-02"` Period string `query:"period" validate:"required,datetime=2006-01-02"`