mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35d128cd52 | |||
| 824e96575a | |||
| d2f52b6901 |
-4
@@ -1,4 +0,0 @@
|
|||||||
-- Rollback: Remove requested_qty column from laying_transfer_sources table
|
|
||||||
|
|
||||||
ALTER TABLE laying_transfer_sources
|
|
||||||
DROP COLUMN IF EXISTS requested_qty;
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
-- Add requested_qty column to laying_transfer_sources table
|
|
||||||
-- This field stores the quantity requested by user during create/update
|
|
||||||
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
|
|
||||||
|
|
||||||
ALTER TABLE laying_transfer_sources
|
|
||||||
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
|
|
||||||
|
|
||||||
-- Add comment for documentation
|
|
||||||
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE recording_depletions
|
|
||||||
DROP COLUMN IF EXISTS pending_qty,
|
|
||||||
DROP COLUMN IF EXISTS source_product_warehouse_id;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
ALTER TABLE recording_depletions
|
|
||||||
ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0,
|
|
||||||
ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint;
|
|
||||||
|
|
||||||
UPDATE recording_depletions rd
|
|
||||||
SET source_product_warehouse_id = src.product_warehouse_id
|
|
||||||
FROM recordings r
|
|
||||||
JOIN LATERAL (
|
|
||||||
SELECT pfp.product_warehouse_id
|
|
||||||
FROM project_chickins pc
|
|
||||||
JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id
|
|
||||||
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
|
|
||||||
ORDER BY pfp.created_at ASC, pfp.id ASC
|
|
||||||
LIMIT 1
|
|
||||||
) AS src ON true
|
|
||||||
WHERE r.id = rd.recording_id
|
|
||||||
AND rd.source_product_warehouse_id IS NULL;
|
|
||||||
@@ -11,7 +11,6 @@ type LayingTransferSource struct {
|
|||||||
LayingTransferId uint `gorm:"index;not null"`
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
SourceProjectFlockKandangId uint `gorm:"not null"`
|
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint `gorm:""`
|
ProductWarehouseId *uint `gorm:""`
|
||||||
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
|
|
||||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||||
Note string `gorm:"type:text"`
|
Note string `gorm:"type:text"`
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
type RecordingDepletion struct {
|
type RecordingDepletion struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
|
Qty float64 `gorm:"column:qty;not null"`
|
||||||
Qty float64 `gorm:"column:qty;not null"`
|
|
||||||
PendingQty float64 `gorm:"column:pending_qty"`
|
|
||||||
|
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ const (
|
|||||||
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
|
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
|
||||||
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
||||||
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
|
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
|
||||||
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -14,16 +14,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingController struct {
|
type ClosingController struct {
|
||||||
ClosingService service.ClosingService
|
ClosingService service.ClosingService
|
||||||
SapronakService service.SapronakService
|
SapronakService service.SapronakService
|
||||||
ClosingKeuanganService service.ClosingKeuanganService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, closingKeuanganService service.ClosingKeuanganService) *ClosingController {
|
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
||||||
return &ClosingController{
|
return &ClosingController{
|
||||||
ClosingService: closingService,
|
ClosingService: closingService,
|
||||||
SapronakService: sapronakService,
|
SapronakService: sapronakService,
|
||||||
ClosingKeuanganService: closingKeuanganService,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,10 +234,9 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := &validation.ClosingSapronakQuery{
|
query := &validation.ClosingSapronakQuery{
|
||||||
Type: strings.ToLower(c.Query("type")),
|
Type: strings.ToLower(c.Query("type")),
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search"),
|
|
||||||
}
|
}
|
||||||
if raw := c.Query("kandang_id"); raw != "" {
|
if raw := c.Query("kandang_id"); raw != "" {
|
||||||
kandangInt, convErr := strconv.Atoi(raw)
|
kandangInt, convErr := strconv.Atoi(raw)
|
||||||
@@ -278,45 +275,6 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingSapronakSummary(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("projectFlockId")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &validation.ClosingSapronakQuery{
|
|
||||||
Type: strings.ToLower(c.Query("type")),
|
|
||||||
Search: c.Query("search"),
|
|
||||||
}
|
|
||||||
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.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingService.GetClosingSapronakSummary(c, uint(id), query)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Retrieved closing report (sapronak summary) successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("project_flock_id")
|
||||||
flag := c.Query("flag", "")
|
flag := c.Query("flag", "")
|
||||||
@@ -380,7 +338,7 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID))
|
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -394,34 +352,6 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetClosingKeuanganByKandang(c *fiber.Ctx) error {
|
|
||||||
projectParam := c.Params("project_flock_id")
|
|
||||||
kandangParam := c.Params("project_flock_kandang_id")
|
|
||||||
|
|
||||||
projectFlockID, err := strconv.Atoi(projectParam)
|
|
||||||
if err != nil || projectFlockID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
pfkID, err := strconv.Atoi(kandangParam)
|
|
||||||
if err != nil || pfkID <= 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get closing keuangan by kandang successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
|
|||||||
@@ -98,26 +98,26 @@ type ClosingEggSalesDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClosingPerformanceDTO struct {
|
type ClosingPerformanceDTO struct {
|
||||||
Depletion float64 `json:"depletion"`
|
Depletion float64 `json:"depletion"`
|
||||||
Age float64 `json:"age_day"`
|
Age float64 `json:"age_day"`
|
||||||
MortalityStd float64 `json:"mor_std"`
|
MortalityStd float64 `json:"mor_std"`
|
||||||
MortalityAct float64 `json:"mor_act"`
|
MortalityAct float64 `json:"mor_act"`
|
||||||
DeffMortality float64 `json:"mor_diff"`
|
DeffMortality float64 `json:"mor_diff"`
|
||||||
FcrStd float64 `json:"fcr_std"`
|
FcrStd float64 `json:"fcr_std"`
|
||||||
FcrAct float64 `json:"fcr_act"`
|
FcrAct float64 `json:"fcr_act"`
|
||||||
DeffFcr float64 `json:"fcr_diff"`
|
DeffFcr float64 `json:"fcr_diff"`
|
||||||
AwgAct float64 `json:"awg_act"`
|
AwgAct float64 `json:"awg_act"`
|
||||||
AwgStd float64 `json:"awg_std"`
|
AwgStd float64 `json:"awg_std"`
|
||||||
FeedIntake float64 `json:"feed_intake"`
|
FeedIntake float64 `json:"feed_intake"`
|
||||||
FeedIntakeStd float64 `json:"feed_intake_std"`
|
FeedIntakeStd float64 `json:"feed_intake_std"`
|
||||||
HenDayAct float64 `json:"hen_day_act,omitempty"`
|
HenDayAct *float64 `json:"hen_day_act,omitempty"`
|
||||||
HendayStd float64 `json:"hen_day_std"`
|
HendayStd *float64 `json:"hen_day_std,omitempty"`
|
||||||
EggMass float64 `json:"egg_mass,omitempty"`
|
EggMass *float64 `json:"egg_mass,omitempty"`
|
||||||
EggMassStd float64 `json:"egg_mass_std"`
|
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
|
||||||
EggWeight float64 `json:"egg_weight,omitempty"`
|
EggWeight *float64 `json:"egg_weight,omitempty"`
|
||||||
EggWeightStd float64 `json:"egg_weight_std"`
|
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
|
||||||
HenHouseAct float64 `json:"hen_housed_act,omitempty"`
|
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
|
||||||
HenHouseStd float64 `json:"hen_housed_std"`
|
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingSalesGroupDTO struct {
|
type ClosingSalesGroupDTO struct {
|
||||||
|
|||||||
@@ -1,103 +1,135 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
// === CLOSING KEUANGAN CODES ===
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
// Closing HPP Codes
|
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
type ClosingHPPCode string
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
const (
|
|
||||||
HPPCodePakan ClosingHPPCode = "PAKAN"
|
|
||||||
HPPCodeOVK ClosingHPPCode = "OVK"
|
|
||||||
HPPCodeDOC ClosingHPPCode = "DOC"
|
|
||||||
HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
|
|
||||||
HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
|
|
||||||
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Closing Profit Loss Codes
|
// === CONSTANTS ===
|
||||||
type ClosingProfitLossCode string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PLCodeSales ClosingProfitLossCode = "SALES"
|
HPPGroupPengeluaran = "HPP dan Pengeluaran"
|
||||||
PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
|
HPPGroupBahanBaku = "HPP dan Bahan Baku"
|
||||||
PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
|
HPPLabelOverhead = "Pengeluaran Overhead"
|
||||||
PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
|
HPPLabelEkspedisi = "Beban Ekspedisi"
|
||||||
|
HPPSummaryLabel = "HPP"
|
||||||
|
|
||||||
|
PLSalesTypeChicken = "Penjualan Ayam Besar"
|
||||||
|
PLSalesTypeEgg = "Penjualan Telur"
|
||||||
|
|
||||||
|
PLItemTypeSapronak = "Pembelian Sapronak"
|
||||||
|
PLItemTypeOverhead = "Pengeluaran Overhead"
|
||||||
|
PLItemTypeEkspedisi = "Beban Ekspedisi"
|
||||||
|
|
||||||
|
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
|
||||||
|
PLSummaryLabelSubTotal = "SUB TOTAL"
|
||||||
|
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
|
||||||
|
|
||||||
|
PurchaseLabelPrefix = "Pembelian "
|
||||||
)
|
)
|
||||||
|
|
||||||
// === NEW CLOSING KEUANGAN DTO ===
|
// === CONTEXT STRUCTS ===
|
||||||
|
|
||||||
|
type CalculationContext struct {
|
||||||
|
TotalPopulation float64
|
||||||
|
TotalWeightProduced float64
|
||||||
|
TotalEggWeightKg float64
|
||||||
|
TotalDepletion float64
|
||||||
|
TotalWeightSold float64
|
||||||
|
ActualPopulation float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingKeuanganInput struct {
|
||||||
|
ProjectFlockCategory string
|
||||||
|
PurchaseItems []entities.PurchaseItem
|
||||||
|
Budgets []entities.ProjectBudget
|
||||||
|
Realizations []entities.ExpenseRealization
|
||||||
|
DeliveryProducts []entities.MarketingDeliveryProduct
|
||||||
|
Chickins []entities.ProjectChickin
|
||||||
|
TotalWeightProduced float64
|
||||||
|
TotalEggWeightKg float64
|
||||||
|
TotalDepletion float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// === BASE METRICS ===
|
||||||
|
|
||||||
// FinancialMetrics represents financial metrics with per unit and total amounts
|
|
||||||
type FinancialMetrics struct {
|
type FinancialMetrics struct {
|
||||||
RpPerBird float64 `json:"rp_per_bird"`
|
RpPerBird float64 `json:"rp_per_bird"`
|
||||||
RpPerKg float64 `json:"rp_per_kg"`
|
RpPerKg float64 `json:"rp_per_kg"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HPPItem represents an item in HPP section
|
type Comparison struct {
|
||||||
type HPPItem struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
Category string `json:"category"` // "purchase" or "overhead"
|
|
||||||
Code string `json:"code"` // "PAKAN", "OVK", "DOC", "EKSPEDISI"
|
|
||||||
Label string `json:"label"`
|
|
||||||
Budgeting FinancialMetrics `json:"budgeting"`
|
Budgeting FinancialMetrics `json:"budgeting"`
|
||||||
Realization FinancialMetrics `json:"realization"`
|
Realization FinancialMetrics `json:"realization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HPPSummary represents summary for HPP section
|
// === HPP PURCHASES PACKAGE ===
|
||||||
type HPPSummary struct {
|
|
||||||
Label string `json:"label"`
|
type HppItem struct {
|
||||||
Budgeting FinancialMetrics `json:"budgeting"`
|
Type string `json:"type"`
|
||||||
Realization FinancialMetrics `json:"realization"`
|
Comparison
|
||||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
|
||||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HPPSection represents HPP data section
|
type HppGroup struct {
|
||||||
type HPPSection struct {
|
GroupName string `json:"group_name"`
|
||||||
Items []HPPItem `json:"items"`
|
Data []HppItem `json:"data"`
|
||||||
Summary HPPSummary `json:"summary"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossItem represents an item in Profit & Loss section
|
type SummaryHpp struct {
|
||||||
type ProfitLossItem struct {
|
Label string `json:"label"`
|
||||||
Code string `json:"code"` // "SALES", "PURCHASE_DOC", "OVERHEAD", "EKSPEDISI"
|
Budgeting FinancialMetrics `json:"budgeting"`
|
||||||
Label string `json:"label"`
|
Realization FinancialMetrics `json:"realization"`
|
||||||
Type string `json:"type"` // "income", "purchase", "overhead"
|
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||||
RpPerBird float64 `json:"rp_per_bird"`
|
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||||
RpPerKg float64 `json:"rp_per_kg"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossSummary represents summary for Profit & Loss section
|
type HppPurchasesSection struct {
|
||||||
type ProfitLossSummary struct {
|
Hpp []HppGroup `json:"hpp"`
|
||||||
GrossProfit FinancialMetrics `json:"gross_profit"`
|
SummaryHpp SummaryHpp `json:"summary_hpp"`
|
||||||
SubTotal FinancialMetrics `json:"sub_total"`
|
}
|
||||||
NetProfit FinancialMetrics `json:"net_profit"`
|
|
||||||
|
// === PROFIT LOSS PACKAGE ===
|
||||||
|
|
||||||
|
type PLItem struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
FinancialMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type PLSummaryItem struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
FinancialMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type PLSummaryGroup struct {
|
||||||
|
GrossProfit PLSummaryItem `json:"gross_profit"`
|
||||||
|
SubTotal PLSummaryItem `json:"sub_total"`
|
||||||
|
NetProfit PLSummaryItem `json:"net_profit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossData struct {
|
||||||
|
Penjualan []PLItem `json:"penjualan"`
|
||||||
|
Pembelian []PLItem `json:"pembelian"`
|
||||||
|
Overhead PLItem `json:"overhead"`
|
||||||
|
Ekspedisi PLItem `json:"ekspedisi"`
|
||||||
|
Summary PLSummaryGroup `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossSection represents Profit & Loss data section
|
|
||||||
type ProfitLossSection struct {
|
type ProfitLossSection struct {
|
||||||
Items []ProfitLossItem `json:"items"`
|
Data ProfitLossData `json:"data"`
|
||||||
Summary ProfitLossSummary `json:"summary"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClosingKeuanganData represents the main data structure
|
// === RESPONSE DTO (ROOT) ===
|
||||||
type ClosingKeuanganData struct {
|
|
||||||
HPP HPPSection `json:"hpp"`
|
|
||||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClosingKeuanganResponse represents the full API response
|
type ReportResponse struct {
|
||||||
type ClosingKeuanganResponse struct {
|
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
|
||||||
Code int `json:"code"`
|
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data ClosingKeuanganData `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === MAPPER FUNCTIONS ===
|
// === MAPPER FUNCTIONS ===
|
||||||
|
|
||||||
// ToFinancialMetrics creates FinancialMetrics from values
|
|
||||||
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
||||||
return FinancialMetrics{
|
return FinancialMetrics{
|
||||||
RpPerBird: rpPerBird,
|
RpPerBird: rpPerBird,
|
||||||
@@ -106,80 +138,451 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToHPPItem creates HPP item
|
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
|
||||||
func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
|
return Comparison{
|
||||||
return HPPItem{
|
|
||||||
ID: id,
|
|
||||||
Category: category,
|
|
||||||
Code: code,
|
|
||||||
Label: label,
|
|
||||||
Budgeting: budgeting,
|
Budgeting: budgeting,
|
||||||
Realization: realization,
|
Realization: realization,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToHPPSummary creates HPP summary
|
// === HPP PENGELUARAN (from Purchase Items) ===
|
||||||
func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
|
|
||||||
return HPPSummary{
|
func getFlagLabel(flagType utils.FlagType) string {
|
||||||
Label: label,
|
return PurchaseLabelPrefix + string(flagType)
|
||||||
Budgeting: budgeting,
|
}
|
||||||
Realization: realization,
|
|
||||||
EggBudgeting: eggBudgeting,
|
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
|
||||||
EggRealization: eggRealization,
|
flags := []utils.FlagType{
|
||||||
|
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
|
||||||
|
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
|
||||||
|
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []HppItem{}
|
||||||
|
seenFlags := make(map[utils.FlagType]bool)
|
||||||
|
|
||||||
|
for _, item := range purchaseItems {
|
||||||
|
if item.Product == nil || len(item.Product.Flags) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range item.Product.Flags {
|
||||||
|
flagType := utils.FlagType(flag.Name)
|
||||||
|
|
||||||
|
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
|
||||||
|
amount := sumPurchasesByFlag(purchaseItems, flagType)
|
||||||
|
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||||
|
|
||||||
|
items = append(items, HppItem{
|
||||||
|
Type: getFlagLabel(flagType),
|
||||||
|
Comparison: ToComparison(
|
||||||
|
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
||||||
|
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
seenFlags[flagType] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
|
||||||
|
|
||||||
|
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
|
||||||
|
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||||
|
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||||
|
|
||||||
|
return HppItem{
|
||||||
|
Type: HPPLabelOverhead,
|
||||||
|
Comparison: ToComparison(
|
||||||
|
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
|
||||||
|
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToHPPSection creates HPP section
|
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
|
||||||
func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
|
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||||
return HPPSection{
|
|
||||||
Items: items,
|
return HppItem{
|
||||||
Summary: summary,
|
Type: HPPLabelEkspedisi,
|
||||||
|
Comparison: ToComparison(
|
||||||
|
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
||||||
|
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToProfitLossItem creates Profit & Loss item
|
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
|
||||||
func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
|
items := []HppItem{}
|
||||||
return ProfitLossItem{
|
|
||||||
Code: code,
|
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
||||||
Label: label,
|
realizationAmount := getOperationalExpenses(realizations)
|
||||||
Type: itemType,
|
|
||||||
RpPerBird: rpPerBird,
|
if budgetAmount > 0 || realizationAmount > 0 {
|
||||||
RpPerKg: rpPerKg,
|
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
|
||||||
Amount: amount,
|
}
|
||||||
|
|
||||||
|
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
||||||
|
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
|
||||||
|
|
||||||
|
return HppGroup{
|
||||||
|
GroupName: HPPGroupBahanBaku,
|
||||||
|
Data: items,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToProfitLossSummary creates Profit & Loss summary
|
// === HPP SUMMARY ===
|
||||||
func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
|
|
||||||
return ProfitLossSummary{
|
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
|
||||||
GrossProfit: grossProfit,
|
purchaseTotal := sumPurchaseTotal(purchaseItems)
|
||||||
SubTotal: subTotal,
|
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
||||||
NetProfit: netProfit,
|
totalBudget := purchaseTotal + budgetTotal
|
||||||
|
|
||||||
|
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
|
||||||
|
|
||||||
|
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||||
|
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||||
|
|
||||||
|
summary := SummaryHpp{
|
||||||
|
Label: label,
|
||||||
|
Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||||
|
Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
||||||
|
}
|
||||||
|
|
||||||
|
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
|
||||||
|
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
|
||||||
|
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
|
||||||
|
|
||||||
|
summary.EggBudgeting = &FinancialMetrics{
|
||||||
|
RpPerBird: 0,
|
||||||
|
RpPerKg: budgetEggRpPerKg,
|
||||||
|
Amount: totalBudget,
|
||||||
|
}
|
||||||
|
summary.EggRealization = &FinancialMetrics{
|
||||||
|
RpPerBird: 0,
|
||||||
|
RpPerKg: realizationEggRpPerKg,
|
||||||
|
Amount: totalRealization,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
|
||||||
|
hppGroups := []HppGroup{
|
||||||
|
{
|
||||||
|
GroupName: HPPGroupPengeluaran,
|
||||||
|
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
|
||||||
|
},
|
||||||
|
ToHppBahanBakuGroup(budgets, realizations, ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
|
||||||
|
|
||||||
|
return HppPurchasesSection{
|
||||||
|
Hpp: hppGroups,
|
||||||
|
SummaryHpp: summaryHpp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToProfitLossSection creates Profit & Loss section
|
// === PROFIT & LOSS ===
|
||||||
func ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
|
|
||||||
|
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
|
||||||
|
return PLItem{
|
||||||
|
Type: itemType,
|
||||||
|
FinancialMetrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
|
||||||
|
return PLSummaryItem{
|
||||||
|
Label: label,
|
||||||
|
FinancialMetrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
|
||||||
|
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
|
||||||
|
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
|
||||||
|
for _, item := range items {
|
||||||
|
totalAmount += item.Amount
|
||||||
|
totalPerBird += item.RpPerBird
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
|
||||||
|
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
|
||||||
|
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
|
||||||
|
items := []PLItem{}
|
||||||
|
|
||||||
|
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
|
||||||
|
|
||||||
|
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
||||||
|
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
|
||||||
|
|
||||||
|
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
||||||
|
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
|
||||||
|
} else {
|
||||||
|
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
||||||
|
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||||
|
purchaseAmount := sumPurchaseTotal(purchases)
|
||||||
|
|
||||||
|
return []PLItem{
|
||||||
|
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||||
|
realizationAmount := getOperationalExpenses(realizations)
|
||||||
|
return []PLItem{
|
||||||
|
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||||
|
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
||||||
|
return []PLItem{
|
||||||
|
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
|
||||||
|
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
|
||||||
|
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
|
||||||
|
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
|
||||||
|
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
|
||||||
|
|
||||||
|
grossProfit := totalPenjualan - totalPembelian
|
||||||
|
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
|
||||||
|
|
||||||
|
totalOtherExpenses := totalOverhead + totalEkspedisi
|
||||||
|
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
|
||||||
|
|
||||||
|
netProfit := grossProfit - totalOtherExpenses
|
||||||
|
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
|
||||||
|
|
||||||
|
return PLSummaryGroup{
|
||||||
|
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
|
||||||
|
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
|
||||||
|
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
|
||||||
|
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
||||||
|
|
||||||
|
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
|
||||||
|
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
|
||||||
|
|
||||||
|
return ProfitLossData{
|
||||||
|
Penjualan: penjualanItems,
|
||||||
|
Pembelian: pembelianItems,
|
||||||
|
Overhead: totalOverhead,
|
||||||
|
Ekspedisi: totalEkspedisi,
|
||||||
|
Summary: summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
|
||||||
return ProfitLossSection{
|
return ProfitLossSection{
|
||||||
Items: items,
|
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
|
||||||
Summary: summary,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToClosingKeuanganData creates complete closing keuangan data
|
func aggregatePLItems(items []PLItem, label string) PLItem {
|
||||||
func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
|
totalAmount, totalPerBird := sumPLItems(items)
|
||||||
return ClosingKeuanganData{
|
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
|
||||||
HPP: hpp,
|
}
|
||||||
ProfitLoss: profitLoss,
|
|
||||||
|
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
|
||||||
|
return ReportResponse{
|
||||||
|
HppPurchases: hppPurchases,
|
||||||
|
ProfitLoss: profitLoss,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToSuccessClosingKeuanganResponse creates success response
|
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
|
||||||
func ToSuccessClosingKeuanganResponse(data ClosingKeuanganData) ClosingKeuanganResponse {
|
var totalPopulation float64
|
||||||
return ClosingKeuanganResponse{
|
var totalWeightSold float64
|
||||||
Code: 200,
|
|
||||||
Status: "success",
|
for _, chickin := range input.Chickins {
|
||||||
Message: "Get closing keuangan successfully",
|
totalPopulation += chickin.UsageQty
|
||||||
Data: data,
|
}
|
||||||
|
|
||||||
|
for _, delivery := range input.DeliveryProducts {
|
||||||
|
totalWeightSold += delivery.TotalWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := CalculationContext{
|
||||||
|
TotalPopulation: totalPopulation,
|
||||||
|
TotalWeightProduced: input.TotalWeightProduced,
|
||||||
|
TotalEggWeightKg: input.TotalEggWeightKg,
|
||||||
|
TotalDepletion: input.TotalDepletion,
|
||||||
|
TotalWeightSold: totalWeightSold,
|
||||||
|
ActualPopulation: totalPopulation - input.TotalDepletion,
|
||||||
|
}
|
||||||
|
|
||||||
|
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx)
|
||||||
|
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
|
||||||
|
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
|
||||||
|
overheadItems := ToOverheadItems(input.Realizations, ctx)
|
||||||
|
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
|
||||||
|
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
||||||
|
|
||||||
|
return ToReportResponse(hppSection, plSection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === HELPER FUNCTIONS ===
|
||||||
|
|
||||||
|
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
|
||||||
|
if totalPopulation > 0 {
|
||||||
|
rpPerBird = amount / totalPopulation
|
||||||
|
}
|
||||||
|
if totalWeightSold > 0 {
|
||||||
|
rpPerKg = amount / totalWeightSold
|
||||||
|
}
|
||||||
|
return rpPerBird, rpPerKg
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
|
||||||
|
for _, flag := range flags {
|
||||||
|
if strings.ToUpper(flag.Name) == string(flagType) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
|
||||||
|
return func(item *entities.PurchaseItem) bool {
|
||||||
|
if item.Product == nil || len(item.Product.Flags) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasProductFlag(item.Product.Flags, flagType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
||||||
|
return func(realization *entities.ExpenseRealization) bool {
|
||||||
|
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
||||||
|
hasFlag := filterRealizationByNonstockFlag(flagType)
|
||||||
|
return func(realization *entities.ExpenseRealization) bool {
|
||||||
|
return !hasFlag(realization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
|
||||||
|
amount := 0.0
|
||||||
|
for i := range items {
|
||||||
|
if filter(&items[i]) {
|
||||||
|
amount += extractor(&items[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
|
||||||
|
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
|
||||||
|
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
|
||||||
|
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
|
||||||
|
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
|
||||||
|
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
|
||||||
|
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isChickenProductFlag(flagType utils.FlagType) bool {
|
||||||
|
switch flagType {
|
||||||
|
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
|
||||||
|
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEggProductFlag(flagType utils.FlagType) bool {
|
||||||
|
switch flagType {
|
||||||
|
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
|
||||||
|
utils.FlagTelurPutih, utils.FlagTelurRetak:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSalesTypeFromProductFlags(product *entities.Product) string {
|
||||||
|
if product == nil || len(product.Flags) == 0 {
|
||||||
|
return PLSalesTypeChicken
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range product.Flags {
|
||||||
|
flagType := utils.FlagType(strings.ToUpper(flag.Name))
|
||||||
|
|
||||||
|
if isEggProductFlag(flagType) {
|
||||||
|
return PLSalesTypeEgg
|
||||||
|
}
|
||||||
|
if isChickenProductFlag(flagType) {
|
||||||
|
return PLSalesTypeChicken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PLSalesTypeChicken
|
||||||
|
}
|
||||||
|
|
||||||
|
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
|
||||||
|
categorized := make(map[string][]entities.MarketingDeliveryProduct)
|
||||||
|
|
||||||
|
for _, delivery := range deliveries {
|
||||||
|
product := delivery.MarketingProduct.ProductWarehouse.Product
|
||||||
|
salesType := getSalesTypeFromProductFlags(&product)
|
||||||
|
|
||||||
|
categorized[salesType] = append(categorized[salesType], delivery)
|
||||||
|
}
|
||||||
|
|
||||||
|
return categorized
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
|
||||||
|
amount := 0.0
|
||||||
|
for _, delivery := range deliveries {
|
||||||
|
amount += delivery.TotalPrice
|
||||||
|
}
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
// New Closing Keuangan Response DTO Structure
|
||||||
|
|
||||||
|
// Base metrics - digunakan di banyak tempat
|
||||||
|
type NewFinancialMetrics struct {
|
||||||
|
RpPerBird float64 `json:"rp_per_bird"`
|
||||||
|
RpPerKg float64 `json:"rp_per_kg"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparison untuk Budgeting vs Realization
|
||||||
|
type NewComparison struct {
|
||||||
|
Budgeting NewFinancialMetrics `json:"budgeting"`
|
||||||
|
Realization NewFinancialMetrics `json:"realization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HPP Purchase Section
|
||||||
|
type HppPurchase struct {
|
||||||
|
Pakan NewComparison `json:"pakan"`
|
||||||
|
OVK NewComparison `json:"OVK"`
|
||||||
|
DOC NewComparison `json:"DOC"`
|
||||||
|
Depresiasi NewComparison `json:"Depresiasi"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HPP Overhead Section
|
||||||
|
type HppOverhead struct {
|
||||||
|
Overhead NewComparison `json:"overhead"`
|
||||||
|
Ekspedisi NewComparison `json:"ekspedisi"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary HPP
|
||||||
|
type NewSummaryHpp struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Budgeting NewFinancialMetrics `json:"budgeting"`
|
||||||
|
Realization NewFinancialMetrics `json:"realization"`
|
||||||
|
EggBudgeting *NewFinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||||
|
EggRealization *NewFinancialMetrics `json:"egg_realization,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HPP wrapper
|
||||||
|
type NewHpp struct {
|
||||||
|
HppPurchase HppPurchase `json:"hpp_purchase"`
|
||||||
|
HppOverhead HppOverhead `json:"hpp_overhead"`
|
||||||
|
SummaryHpp NewSummaryHpp `json:"summary_hpp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purchase Cost (dengan type field, embedded NewFinancialMetrics)
|
||||||
|
type PurchaseCost struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
NewFinancialMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// PL Summary
|
||||||
|
type PLSummary struct {
|
||||||
|
GrossProfit NewFinancialMetrics `json:"gross_profit"`
|
||||||
|
SubTotal NewFinancialMetrics `json:"sub_total"`
|
||||||
|
NetProfit NewFinancialMetrics `json:"net_profit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profit Loss wrapper
|
||||||
|
type NewProfitLoss struct {
|
||||||
|
PenjualanTelur NewFinancialMetrics `json:"penjualan_telur"`
|
||||||
|
PurchaseCost PurchaseCost `json:"purchase_cost"`
|
||||||
|
Overhead NewFinancialMetrics `json:"overhead"`
|
||||||
|
Ekspedisi NewFinancialMetrics `json:"ekspedisi"`
|
||||||
|
Summary PLSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Data structure
|
||||||
|
type NewClosingKeuanganData struct {
|
||||||
|
Hpp NewHpp `json:"hpp"`
|
||||||
|
ProfitLoss NewProfitLoss `json:"profit_loss"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full Response DTO
|
||||||
|
type NewClosingKeuanganResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data NewClosingKeuanganData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MAPPER FUNCTIONS ===
|
||||||
|
|
||||||
|
// ToNewFinancialMetrics creates a new financial metrics
|
||||||
|
func ToNewFinancialMetrics(rpPerBird, rpPerKg, amount float64) NewFinancialMetrics {
|
||||||
|
return NewFinancialMetrics{
|
||||||
|
RpPerBird: rpPerBird,
|
||||||
|
RpPerKg: rpPerKg,
|
||||||
|
Amount: amount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNewComparison creates a new budgeting vs realization comparison
|
||||||
|
func ToNewComparison(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount, realizationRpPerBird, realizationRpPerKg, realizationAmount float64) NewComparison {
|
||||||
|
return NewComparison{
|
||||||
|
Budgeting: ToNewFinancialMetrics(budgetingRpPerBird, budgetingRpPerKg, budgetingAmount),
|
||||||
|
Realization: ToNewFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHppPurchase creates HPP purchase section
|
||||||
|
func ToHppPurchase(pakan, oVK, dOC, depresiasi NewComparison) HppPurchase {
|
||||||
|
return HppPurchase{
|
||||||
|
Pakan: pakan,
|
||||||
|
OVK: oVK,
|
||||||
|
DOC: dOC,
|
||||||
|
Depresiasi: depresiasi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHppOverhead creates HPP overhead section
|
||||||
|
func ToHppOverhead(overhead, ekspedisi NewComparison) HppOverhead {
|
||||||
|
return HppOverhead{
|
||||||
|
Overhead: overhead,
|
||||||
|
Ekspedisi: ekspedisi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNewSummaryHpp creates HPP summary
|
||||||
|
func ToNewSummaryHpp(label string, budgeting, realization NewFinancialMetrics, eggBudgeting, eggRealization *NewFinancialMetrics) NewSummaryHpp {
|
||||||
|
return NewSummaryHpp{
|
||||||
|
Label: label,
|
||||||
|
Budgeting: budgeting,
|
||||||
|
Realization: realization,
|
||||||
|
EggBudgeting: eggBudgeting,
|
||||||
|
EggRealization: eggRealization,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNewHpp creates complete HPP section
|
||||||
|
func ToNewHpp(hppPurchase HppPurchase, hppOverhead HppOverhead, summaryHpp NewSummaryHpp) NewHpp {
|
||||||
|
return NewHpp{
|
||||||
|
HppPurchase: hppPurchase,
|
||||||
|
HppOverhead: hppOverhead,
|
||||||
|
SummaryHpp: summaryHpp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPurchaseCost creates purchase cost item
|
||||||
|
func ToPurchaseCost(costType string, metrics NewFinancialMetrics) PurchaseCost {
|
||||||
|
return PurchaseCost{
|
||||||
|
Type: costType,
|
||||||
|
NewFinancialMetrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPLSummary creates profit loss summary
|
||||||
|
func ToPLSummary(grossProfit, subTotal, netProfit NewFinancialMetrics) PLSummary {
|
||||||
|
return PLSummary{
|
||||||
|
GrossProfit: grossProfit,
|
||||||
|
SubTotal: subTotal,
|
||||||
|
NetProfit: netProfit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNewProfitLoss creates complete profit loss section
|
||||||
|
func ToNewProfitLoss(penjualanTelur, overhead, ekspedisi NewFinancialMetrics, purchaseCost PurchaseCost, summary PLSummary) NewProfitLoss {
|
||||||
|
return NewProfitLoss{
|
||||||
|
PenjualanTelur: penjualanTelur,
|
||||||
|
PurchaseCost: purchaseCost,
|
||||||
|
Overhead: overhead,
|
||||||
|
Ekspedisi: ekspedisi,
|
||||||
|
Summary: summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToNewClosingKeuanganData creates complete closing keuangan data
|
||||||
|
func ToNewClosingKeuanganData(hpp NewHpp, profitLoss NewProfitLoss) NewClosingKeuanganData {
|
||||||
|
return NewClosingKeuanganData{
|
||||||
|
Hpp: hpp,
|
||||||
|
ProfitLoss: profitLoss,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSuccessNewClosingKeuanganResponse creates success response shortcut
|
||||||
|
func ToSuccessNewClosingKeuanganResponse(data NewClosingKeuanganData) NewClosingKeuanganResponse {
|
||||||
|
return NewClosingKeuanganResponse{
|
||||||
|
Code: 200,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get closing keuangan successfully",
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -114,17 +114,6 @@ type ClosingSapronakDTO struct {
|
|||||||
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingSapronakSummaryItemDTO struct {
|
|
||||||
Category string `json:"category"`
|
|
||||||
TotalQty int64 `json:"total_qty"`
|
|
||||||
Uom UomSummaryDTO `json:"uom"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UomSummaryDTO struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions for Aggregated Sapronak Response ===
|
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||||
|
|
||||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
@@ -212,48 +201,18 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
switch strings.ToLower(item.JenisTransaksi) {
|
switch strings.ToLower(item.JenisTransaksi) {
|
||||||
case "pembelian", "adjustment masuk", "mutasi masuk":
|
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||||
row.QtyIn += item.QtyMasuk
|
row.QtyIn += item.QtyMasuk
|
||||||
if row.UnitPrice == 0 {
|
row.TotalAmount += item.Nilai
|
||||||
if item.QtyMasuk > 0 && item.Nilai > 0 {
|
|
||||||
row.UnitPrice = item.Nilai / item.QtyMasuk
|
|
||||||
} else if item.Harga > 0 {
|
|
||||||
row.UnitPrice = item.Harga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
|
|
||||||
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
|
|
||||||
if strings.HasPrefix(ref, "TL-") {
|
|
||||||
row.Notes = "TRANSFER LAYING"
|
|
||||||
} else if strings.HasPrefix(ref, "ST-") {
|
|
||||||
row.Notes = "TRANSFER STOCK"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "pemakaian", "adjustment keluar":
|
case "pemakaian", "adjustment keluar":
|
||||||
price := row.UnitPrice
|
|
||||||
if price == 0 {
|
|
||||||
price = item.Harga
|
|
||||||
}
|
|
||||||
row.QtyUsed += item.QtyKeluar
|
row.QtyUsed += item.QtyKeluar
|
||||||
row.TotalAmount += item.QtyKeluar * price
|
case "mutasi keluar":
|
||||||
case "mutasi keluar", "penjualan":
|
|
||||||
price := row.UnitPrice
|
|
||||||
if price == 0 {
|
|
||||||
price = item.Harga
|
|
||||||
}
|
|
||||||
row.QtyOut += item.QtyKeluar
|
row.QtyOut += item.QtyKeluar
|
||||||
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
|
|
||||||
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
|
|
||||||
if strings.HasPrefix(ref, "TL-") {
|
|
||||||
row.Notes = "TRANSFER LAYING"
|
|
||||||
} else if strings.HasPrefix(ref, "ST-") {
|
|
||||||
row.Notes = "TRANSFER STOCK"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
row.QtyIn += item.QtyMasuk
|
row.QtyIn += item.QtyMasuk
|
||||||
row.TotalAmount += item.Nilai
|
row.TotalAmount += item.Nilai
|
||||||
if row.QtyIn > 0 {
|
}
|
||||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
|
||||||
}
|
if row.QtyIn > 0 {
|
||||||
|
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,8 +233,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
|||||||
total += r.TotalAmount
|
total += r.TotalAmount
|
||||||
}
|
}
|
||||||
avg := 0.0
|
avg := 0.0
|
||||||
if qtyUsed > 0 {
|
if qtyIn > 0 {
|
||||||
avg = total / qtyUsed
|
avg = total / qtyIn
|
||||||
}
|
}
|
||||||
cat.Total = SapronakCategoryTotalDTO{
|
cat.Total = SapronakCategoryTotalDTO{
|
||||||
Label: label,
|
Label: label,
|
||||||
|
|||||||
@@ -41,10 +41,9 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
|
closingService := sClosing.NewClosingService(closingRepo, closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
|
||||||
closingKeuanganService := sClosing.NewClosingKeuanganService(closingKeuanganRepo, projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo)
|
|
||||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService, sapronakService, closingKeuanganService)
|
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,12 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClosingRepository interface {
|
type ClosingRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlock]
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||||
GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error)
|
|
||||||
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
||||||
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||||
@@ -25,7 +23,7 @@ type ClosingRepository interface {
|
|||||||
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
||||||
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
|
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
|
||||||
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
|
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
|
||||||
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
|
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||||
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||||
@@ -34,8 +32,6 @@ type ClosingRepository interface {
|
|||||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
|
||||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingRepositoryImpl struct {
|
type ClosingRepositoryImpl struct {
|
||||||
@@ -61,30 +57,16 @@ type SapronakRow struct {
|
|||||||
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||||
Destination string `gorm:"column:destination"`
|
Destination string `gorm:"column:destination"`
|
||||||
Quantity float64 `gorm:"column:quantity"`
|
Quantity float64 `gorm:"column:quantity"`
|
||||||
UnitID uint `gorm:"column:unit_id"`
|
|
||||||
Unit string `gorm:"column:unit"`
|
Unit string `gorm:"column:unit"`
|
||||||
Notes string `gorm:"column:notes"`
|
Notes string `gorm:"column:notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SapronakSummaryRow struct {
|
|
||||||
Category string `gorm:"column:category"`
|
|
||||||
TotalQty int64 `gorm:"column:total_qty"`
|
|
||||||
UomID uint `gorm:"column:uom_id"`
|
|
||||||
UomName string `gorm:"column:uom_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpeditionHPPRow struct {
|
|
||||||
SupplierName string `gorm:"column:supplier_name"`
|
|
||||||
TotalAmount float64 `gorm:"column:total_amount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SapronakQueryParams struct {
|
type SapronakQueryParams struct {
|
||||||
Type string
|
Type string
|
||||||
WarehouseIDs []uint
|
WarehouseIDs []uint
|
||||||
ProjectFlockKandangIDs []uint
|
ProjectFlockKandangIDs []uint
|
||||||
Limit int
|
Limit int
|
||||||
Offset int
|
Offset int
|
||||||
Search string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||||
@@ -120,36 +102,14 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
|||||||
|
|
||||||
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||||
|
|
||||||
search := strings.TrimSpace(params.Search)
|
|
||||||
searchClause := ""
|
|
||||||
var searchArgs []any
|
|
||||||
if search != "" {
|
|
||||||
searchClause = `
|
|
||||||
WHERE (
|
|
||||||
reference_number ILIKE ?
|
|
||||||
OR product_name ILIKE ?
|
|
||||||
OR product_category ILIKE ?
|
|
||||||
OR source_warehouse ILIKE ?
|
|
||||||
OR destination_warehouse ILIKE ?
|
|
||||||
OR CAST(quantity AS TEXT) ILIKE ?
|
|
||||||
OR unit ILIKE ?
|
|
||||||
OR notes ILIKE ?
|
|
||||||
OR transaction_type ILIKE ?
|
|
||||||
)`
|
|
||||||
like := "%" + search + "%"
|
|
||||||
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalResults int64
|
var totalResults int64
|
||||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause)
|
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||||
countArgs := append(append([]any{}, args...), searchArgs...)
|
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||||
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
|
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dataArgs := append(append([]any{}, args...), searchArgs...)
|
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||||
dataArgs = append(dataArgs, params.Limit, params.Offset)
|
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||||
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
|
|
||||||
|
|
||||||
var rows []SapronakRow
|
var rows []SapronakRow
|
||||||
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||||
@@ -159,293 +119,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
|||||||
return rows, totalResults, nil
|
return rows, totalResults, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) {
|
|
||||||
db := r.DB().WithContext(ctx)
|
|
||||||
|
|
||||||
var (
|
|
||||||
unionParts []string
|
|
||||||
args []any
|
|
||||||
)
|
|
||||||
|
|
||||||
switch params.Type {
|
|
||||||
case validation.SapronakTypeIncoming:
|
|
||||||
if len(params.WarehouseIDs) == 0 {
|
|
||||||
return []SapronakSummaryRow{}, nil
|
|
||||||
}
|
|
||||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
|
||||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
|
||||||
case validation.SapronakTypeOutgoing:
|
|
||||||
if len(params.WarehouseIDs) > 0 {
|
|
||||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
|
||||||
args = append(args, params.WarehouseIDs)
|
|
||||||
}
|
|
||||||
if len(params.ProjectFlockKandangIDs) > 0 {
|
|
||||||
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
|
||||||
args = append(args, params.ProjectFlockKandangIDs)
|
|
||||||
}
|
|
||||||
if len(unionParts) == 0 {
|
|
||||||
return []SapronakSummaryRow{}, nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
|
||||||
|
|
||||||
search := strings.TrimSpace(params.Search)
|
|
||||||
searchClause := ""
|
|
||||||
var searchArgs []any
|
|
||||||
if search != "" {
|
|
||||||
searchClause = `
|
|
||||||
WHERE (
|
|
||||||
reference_number ILIKE ?
|
|
||||||
OR product_name ILIKE ?
|
|
||||||
OR product_category ILIKE ?
|
|
||||||
OR source_warehouse ILIKE ?
|
|
||||||
OR destination_warehouse ILIKE ?
|
|
||||||
OR CAST(quantity AS TEXT) ILIKE ?
|
|
||||||
OR unit ILIKE ?
|
|
||||||
OR notes ILIKE ?
|
|
||||||
OR transaction_type ILIKE ?
|
|
||||||
)`
|
|
||||||
like := "%" + search + "%"
|
|
||||||
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
|
|
||||||
}
|
|
||||||
|
|
||||||
querySQL := fmt.Sprintf(`
|
|
||||||
SELECT
|
|
||||||
product_category AS category,
|
|
||||||
CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty,
|
|
||||||
unit_id AS uom_id,
|
|
||||||
unit AS uom_name
|
|
||||||
FROM (%s) AS combined%s
|
|
||||||
GROUP BY product_category, unit_id, unit
|
|
||||||
ORDER BY product_category ASC, unit ASC
|
|
||||||
`, unionSQL, searchClause)
|
|
||||||
queryArgs := append(append([]any{}, args...), searchArgs...)
|
|
||||||
|
|
||||||
var rows []SapronakSummaryRow
|
|
||||||
if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
|
||||||
return 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var purchaseAgg struct {
|
|
||||||
TotalIn float64 `gorm:"column:total_in"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("purchase_items pi").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
|
|
||||||
Where("f.name = ?", "PAKAN").
|
|
||||||
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
|
|
||||||
Scan(&purchaseAgg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var usageAgg struct {
|
|
||||||
TotalUsed float64 `gorm:"column:total_used"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.DB().WithContext(ctx).
|
|
||||||
Table("recording_stocks rs").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name = ?", "PAKAN").
|
|
||||||
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
|
||||||
Scan(&usageAgg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
Total float64 `gorm:"column:total_culling"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("recording_depletions rd").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name = ?", utils.FlagAyamCulling).
|
|
||||||
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.Total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
|
||||||
return 0, 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
TotalWeight float64 `gorm:"column:total_weight"`
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
TotalPrice float64 `gorm:"column:total_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("marketing_products mp").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
|
||||||
return 0, 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
TotalWeight float64 `gorm:"column:total_weight"`
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
TotalPrice float64 `gorm:"column:total_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("marketing_products mp").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name IN ?", flagNames).
|
|
||||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var agg struct {
|
|
||||||
TotalQty float64 `gorm:"column:total_qty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("recording_eggs re").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
|
|
||||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
|
||||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("f.name IN ?", flagNames).
|
|
||||||
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
|
|
||||||
Scan(&agg).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return agg.TotalQty, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
|
|
||||||
if fcrID == 0 {
|
|
||||||
return []entity.FcrStandard{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var standards []entity.FcrStandard
|
|
||||||
if err := r.DB().WithContext(ctx).
|
|
||||||
Where("fcr_id = ?", fcrID).
|
|
||||||
Order("weight ASC").
|
|
||||||
Find(&standards).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return standards, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
|
|
||||||
db := r.DB().WithContext(ctx)
|
|
||||||
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid project flock id")
|
|
||||||
}
|
|
||||||
|
|
||||||
query := db.
|
|
||||||
Table("expense_realizations AS er").
|
|
||||||
Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id").
|
|
||||||
Joins("JOIN expenses e ON e.id = ens.expense_id").
|
|
||||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id").
|
|
||||||
Joins("JOIN nonstocks n ON n.id = ens.nonstock_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
|
||||||
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
|
||||||
Where("e.category = ?", "BOP").
|
|
||||||
Where("e.realization_date IS NOT NULL").
|
|
||||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
|
||||||
|
|
||||||
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
|
||||||
query = query.Where("pfk.id = ?", *projectFlockKandangID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []ExpeditionHPPRow
|
|
||||||
err := query.
|
|
||||||
Select(
|
|
||||||
"e.supplier_id AS supplier_id, " +
|
|
||||||
"s.name AS supplier_name, " +
|
|
||||||
"SUM(er.qty * er.price) AS total_amount",
|
|
||||||
).
|
|
||||||
Group("e.supplier_id, s.name").
|
|
||||||
Scan(&rows).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sapronakIncomingPurchasesSQL = `
|
sapronakIncomingPurchasesSQL = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -485,7 +158,6 @@ SELECT
|
|||||||
w.name AS destination_warehouse,
|
w.name AS destination_warehouse,
|
||||||
'' AS destination,
|
'' AS destination,
|
||||||
pi.total_qty AS quantity,
|
pi.total_qty AS quantity,
|
||||||
u.id AS unit_id,
|
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
COALESCE(p.notes, '') AS notes
|
COALESCE(p.notes, '') AS notes
|
||||||
FROM purchase_items pi
|
FROM purchase_items pi
|
||||||
@@ -534,7 +206,6 @@ SELECT
|
|||||||
COALESCE(tw.name, '') AS destination_warehouse,
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
'' AS destination,
|
'' AS destination,
|
||||||
std.usage_qty AS quantity,
|
std.usage_qty AS quantity,
|
||||||
u.id AS unit_id,
|
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
'Stock Refill' AS notes
|
'Stock Refill' AS notes
|
||||||
FROM stock_transfer_details std
|
FROM stock_transfer_details std
|
||||||
@@ -581,10 +252,9 @@ SELECT
|
|||||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
), '') AS product_sub_category,
|
), '') AS product_sub_category,
|
||||||
COALESCE(fw.name, '') AS source_warehouse,
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
COALESCE(tw.name, '') AS destination_warehouse,
|
'' AS destination_warehouse,
|
||||||
'' AS destination,
|
COALESCE(tw.name, '') AS destination,
|
||||||
std.usage_qty AS quantity,
|
std.usage_qty AS quantity,
|
||||||
u.id AS unit_id,
|
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
'Transfer to other unit' AS notes
|
'Transfer to other unit' AS notes
|
||||||
FROM stock_transfer_details std
|
FROM stock_transfer_details std
|
||||||
@@ -631,27 +301,18 @@ SELECT
|
|||||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
), '') AS product_sub_category,
|
), '') AS product_sub_category,
|
||||||
w.name AS source_warehouse,
|
w.name AS source_warehouse,
|
||||||
COALESCE(c.name, '') AS destination_warehouse,
|
'' AS destination_warehouse,
|
||||||
'' AS destination,
|
'RETAIL CUSTOMER' AS destination,
|
||||||
mp.qty AS quantity,
|
mp.qty AS quantity,
|
||||||
u.id AS unit_id,
|
|
||||||
u.name AS unit,
|
u.name AS unit,
|
||||||
m.notes AS notes
|
m.notes AS notes
|
||||||
FROM marketing_products mp
|
FROM marketing_products mp
|
||||||
JOIN marketings m ON m.id = mp.marketing_id
|
JOIN marketings m ON m.id = mp.marketing_id
|
||||||
LEFT JOIN customers c ON c.id = m.customer_id
|
|
||||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||||
JOIN products prod ON prod.id = pw.product_id
|
JOIN products prod ON prod.id = pw.product_id
|
||||||
JOIN uoms u ON u.id = prod.uom_id
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
WHERE pw.project_flock_kandang_id IN ?
|
WHERE pw.project_flock_kandang_id IN ?
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM flags f
|
|
||||||
WHERE f.flagable_id = pw.product_id
|
|
||||||
AND f.flagable_type = 'products'
|
|
||||||
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
|
|
||||||
)
|
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1007,156 +668,193 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
incomingQuery := r.withCtx(ctx).
|
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
|
||||||
Table("stock_transfer_details AS std").
|
|
||||||
Select(`
|
|
||||||
std.product_id AS product_id,
|
|
||||||
p.name AS product_name,
|
|
||||||
f.name AS flag,
|
|
||||||
st.transfer_date::timestamp AS date,
|
|
||||||
COALESCE(st.movement_number, '') AS reference,
|
|
||||||
COALESCE(std.total_qty, 0) AS qty_in,
|
|
||||||
0 AS qty_out,
|
|
||||||
COALESCE(p.product_price, 0) AS price
|
|
||||||
`).
|
|
||||||
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
|
||||||
Joins("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
|
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
|
||||||
Joins("JOIN products p ON p.id = std.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("w.kandang_id = ?", kandangID).
|
|
||||||
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
|
|
||||||
Where("f.name IN ?", sapronakFlagsAll)
|
|
||||||
incoming, err := scanAndGroupDetails(incomingQuery)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
||||||
incomingLayingQuery := r.withCtx(ctx).
|
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
||||||
Table("laying_transfer_targets AS ltt").
|
return ref
|
||||||
Select(`
|
}
|
||||||
pw.product_id AS product_id,
|
return fmt.Sprintf("TRF-%d", row.ID)
|
||||||
p.name AS product_name,
|
})
|
||||||
f.name AS flag,
|
return in, out, nil
|
||||||
lt.transfer_date::timestamp AS date,
|
|
||||||
COALESCE(lt.transfer_number, '') AS reference,
|
|
||||||
COALESCE(ltt.total_qty, 0) AS qty_in,
|
|
||||||
0 AS qty_out,
|
|
||||||
COALESCE(p.product_price, 0) AS price
|
|
||||||
`).
|
|
||||||
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
|
||||||
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
|
|
||||||
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
|
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
|
||||||
Joins("JOIN products p ON p.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("w.kandang_id = ?", kandangID).
|
|
||||||
Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
|
|
||||||
Where("f.name IN ?", sapronakFlagsAll)
|
|
||||||
incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
for pid, rows := range incomingLaying {
|
|
||||||
incoming[pid] = append(incoming[pid], rows...)
|
|
||||||
}
|
|
||||||
|
|
||||||
outgoingQuery := r.withCtx(ctx).
|
|
||||||
Table("stock_allocations AS sa").
|
|
||||||
Select(`
|
|
||||||
std.product_id AS product_id,
|
|
||||||
p.name AS product_name,
|
|
||||||
f.name AS flag,
|
|
||||||
st.transfer_date::timestamp AS date,
|
|
||||||
COALESCE(st.movement_number, '') AS reference,
|
|
||||||
0 AS qty_in,
|
|
||||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
|
||||||
COALESCE(p.product_price, 0) AS price
|
|
||||||
`).
|
|
||||||
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
|
|
||||||
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
|
||||||
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
|
||||||
Joins("JOIN products p ON p.id = std.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
|
||||||
Where("w.kandang_id = ?", kandangID).
|
|
||||||
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
|
||||||
Where("f.name IN ?", sapronakFlagsAll).
|
|
||||||
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
|
|
||||||
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
outgoingLayingQuery := r.withCtx(ctx).
|
|
||||||
Table("stock_allocations AS sa").
|
|
||||||
Select(`
|
|
||||||
pw.product_id AS product_id,
|
|
||||||
p.name AS product_name,
|
|
||||||
f.name AS flag,
|
|
||||||
lt.transfer_date::timestamp AS date,
|
|
||||||
COALESCE(lt.transfer_number, '') AS reference,
|
|
||||||
0 AS qty_in,
|
|
||||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
|
||||||
COALESCE(p.product_price, 0) AS price
|
|
||||||
`).
|
|
||||||
Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
|
||||||
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
|
|
||||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
|
|
||||||
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
|
||||||
Joins("JOIN products p ON p.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
|
||||||
Where("w.kandang_id = ?", kandangID).
|
|
||||||
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
|
||||||
Where("f.name IN ?", sapronakFlagsAll).
|
|
||||||
Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
|
|
||||||
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
for pid, rows := range outgoingLaying {
|
|
||||||
outgoing[pid] = append(outgoing[pid], rows...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return incoming, outgoing, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
// === CLOSING DATA PRODUKSI METHODS ===
|
||||||
query := r.withCtx(ctx).
|
|
||||||
Table("stock_allocations AS sa").
|
|
||||||
Select(`
|
|
||||||
pw.product_id AS product_id,
|
|
||||||
p.name AS product_name,
|
|
||||||
f.name AS flag,
|
|
||||||
COALESCE(mdp.delivery_date, mdp.created_at) AS date,
|
|
||||||
COALESCE(m.so_number, '') AS reference,
|
|
||||||
0 AS qty_in,
|
|
||||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
|
||||||
COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
|
|
||||||
`).
|
|
||||||
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
|
|
||||||
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
|
||||||
Joins("JOIN marketings m ON m.id = mp.marketing_id").
|
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
|
||||||
Joins("JOIN products p ON p.id = pw.product_id").
|
|
||||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
|
||||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
|
||||||
Where("w.kandang_id = ?", kandangID).
|
|
||||||
Where("f.name IN ?", sapronakFlagsAll).
|
|
||||||
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
|
|
||||||
|
|
||||||
return scanAndGroupDetails(query)
|
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var purchaseAgg struct {
|
||||||
|
TotalIn float64 `gorm:"column:total_in"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("purchase_items pi").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
|
||||||
|
Where("f.name = ?", "PAKAN").
|
||||||
|
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
|
||||||
|
Scan(&purchaseAgg).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var usageAgg struct {
|
||||||
|
TotalUsed float64 `gorm:"column:total_used"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.DB().WithContext(ctx).
|
||||||
|
Table("recording_stocks rs").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||||
|
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||||
|
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Where("f.name = ?", "PAKAN").
|
||||||
|
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
||||||
|
Scan(&usageAgg).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var agg struct {
|
||||||
|
Total float64 `gorm:"column:total_culling"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_depletions rd").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
|
||||||
|
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||||
|
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Where("f.name = ?", utils.FlagAyamCulling).
|
||||||
|
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
|
||||||
|
Scan(&agg).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg.Total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return 0, 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var agg struct {
|
||||||
|
TotalWeight float64 `gorm:"column:total_weight"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
TotalPrice float64 `gorm:"column:total_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("marketing_products mp").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||||
|
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||||
|
Scan(&agg).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||||
|
return 0, 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var agg struct {
|
||||||
|
TotalWeight float64 `gorm:"column:total_weight"`
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
TotalPrice float64 `gorm:"column:total_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("marketing_products mp").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||||
|
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||||
|
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Where("f.name IN ?", flagNames).
|
||||||
|
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||||
|
Scan(&agg).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
|
||||||
|
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var agg struct {
|
||||||
|
TotalQty float64 `gorm:"column:total_qty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("recording_eggs re").
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
|
||||||
|
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||||
|
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Where("f.name IN ?", flagNames).
|
||||||
|
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
|
||||||
|
Scan(&agg).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg.TotalQty, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
|
||||||
|
if fcrID == 0 {
|
||||||
|
return []entity.FcrStandard{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var standards []entity.FcrStandard
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Where("fcr_id = ?", fcrID).
|
||||||
|
Order("weight ASC").
|
||||||
|
Find(&standards).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return standards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService, closingKeuanganSvc closing.ClosingKeuanganService) {
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
||||||
ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc)
|
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||||
|
|
||||||
route := v1.Group("/closings")
|
route := v1.Group("/closings")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
@@ -30,11 +30,9 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
||||||
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
|
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
|
||||||
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
||||||
route.Get("/:projectFlockId/sapronak/summary", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronakSummary)
|
|
||||||
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
|
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
||||||
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
||||||
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuanganByKandang)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ type ClosingService interface {
|
|||||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
|
||||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
|
||||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error)
|
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
||||||
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@ type closingService struct {
|
|||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ClosingRepository
|
Repository repository.ClosingRepository
|
||||||
|
ClosingKeuanganRepo repository.ClosingKeuanganRepository
|
||||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||||
MarketingRepo marketingRepository.MarketingRepository
|
MarketingRepo marketingRepository.MarketingRepository
|
||||||
@@ -62,11 +63,12 @@ type closingService struct {
|
|||||||
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
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, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
|
func NewClosingService(repo repository.ClosingRepository, closingKeuanganRepo repository.ClosingKeuanganRepository, 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{
|
return &closingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
|
ClosingKeuanganRepo: closingKeuanganRepo,
|
||||||
ProjectFlockRepo: projectFlockRepo,
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
@@ -337,10 +339,8 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var projectFlockKandangIDs []uint
|
var projectFlockKandangIDs []uint
|
||||||
if params.KandangID != nil && *params.KandangID > 0 {
|
if params.Type == validation.SapronakTypeOutgoing {
|
||||||
projectFlockKandangIDs = []uint{*params.KandangID}
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
|
||||||
} else if params.Type == validation.SapronakTypeOutgoing {
|
|
||||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
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")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
@@ -354,7 +354,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||||
Limit: params.Limit,
|
Limit: params.Limit,
|
||||||
Offset: offset,
|
Offset: offset,
|
||||||
Search: params.Search,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||||
@@ -389,74 +388,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
|||||||
return items, totalResults, nil
|
return items, totalResults, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if params == nil {
|
|
||||||
params = &validation.ClosingSapronakQuery{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
var projectFlockKandangIDs []uint
|
|
||||||
if params.KandangID != nil && *params.KandangID > 0 {
|
|
||||||
projectFlockKandangIDs = []uint{*params.KandangID}
|
|
||||||
} else if params.Type == validation.SapronakTypeOutgoing {
|
|
||||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{
|
|
||||||
Type: params.Type,
|
|
||||||
WarehouseIDs: warehouseIDs,
|
|
||||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
|
||||||
Search: params.Search,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data")
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, dto.ClosingSapronakSummaryItemDTO{
|
|
||||||
Category: row.Category,
|
|
||||||
TotalQty: row.TotalQty,
|
|
||||||
Uom: dto.UomSummaryDTO{
|
|
||||||
ID: row.UomID,
|
|
||||||
Name: row.UomName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
var kandangIDs []uint
|
var kandangIDs []uint
|
||||||
db := s.Repository.DB().WithContext(ctx)
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
@@ -489,11 +420,14 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
|
|||||||
return ids, nil
|
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
|
var ids []uint
|
||||||
query := s.Repository.DB().WithContext(ctx).
|
query := s.Repository.DB().WithContext(ctx).
|
||||||
Model(&entity.ProjectFlockKandang{}).
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
Where("project_flock_id = ?", projectFlockID)
|
Where("project_flock_id = ?", projectFlockID)
|
||||||
|
if kandangID != nil {
|
||||||
|
query = query.Where("kandang_id = ?", *kandangID)
|
||||||
|
}
|
||||||
err := query.Order("id ASC").Pluck("id", &ids).Error
|
err := query.Order("id ASC").Pluck("id", &ids).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -645,12 +579,107 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
|
||||||
|
s.Log.Infof("🔵 [CLOSING KEUANGAN] Starting fetch for ProjectFlockID: %d", projectFlockID)
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
s.Log.Infof("✅ [CLOSING KEUANGAN] ProjectFlock fetched: ID=%d, Category=%s, FlockName=%s",
|
||||||
|
projectFlock.Id, projectFlock.Category, projectFlock.FlockName)
|
||||||
|
|
||||||
|
// Validasi: Closing Keuangan hanya untuk LAYING, bukan GROWING
|
||||||
|
if projectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
s.Log.Warnf("⚠️ [CLOSING KEUANGAN] ProjectFlock ID %d is GROWING category, closing keuangan not available", projectFlockID)
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Closing keuangan only available for LAYING category")
|
||||||
|
}
|
||||||
|
|
||||||
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||||
|
}
|
||||||
|
s.Log.Infof("💰 [CLOSING KEUANGAN] Budgets fetched: %d records", len(budgets))
|
||||||
|
|
||||||
|
actualUsageRows, err := s.ClosingKeuanganRepo.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
||||||
|
}
|
||||||
|
s.Log.Infof("📊 [CLOSING KEUANGAN] Actual Usage Costs fetched: %d records", len(actualUsageRows))
|
||||||
|
|
||||||
|
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
||||||
|
s.Log.Infof("🛒 [CLOSING KEUANGAN] Converted to Purchase Items: %d items", len(purchaseItems))
|
||||||
|
|
||||||
|
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
||||||
|
}
|
||||||
|
s.Log.Infof("💸 [CLOSING KEUANGAN] Expense Realizations fetched: %d records", len(realizations))
|
||||||
|
|
||||||
|
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("MarketingProduct").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product")
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
||||||
|
}
|
||||||
|
s.Log.Infof("🚚 [CLOSING KEUANGAN] Marketing Delivery Products fetched: %d records", len(deliveryProducts))
|
||||||
|
|
||||||
|
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
||||||
|
}
|
||||||
|
s.Log.Infof("🐣 [CLOSING KEUANGAN] Chickins fetched: %d records", len(chickins))
|
||||||
|
|
||||||
|
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
|
||||||
|
}
|
||||||
|
s.Log.Infof("⚖️ [CLOSING KEUANGAN] Total Weight Produced: %.2f kg", totalWeightProduced)
|
||||||
|
|
||||||
|
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
|
||||||
|
}
|
||||||
|
s.Log.Infof("🥚 [CLOSING KEUANGAN] Total Egg Weight: %.2f kg", totalEggWeightKg)
|
||||||
|
|
||||||
|
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||||
|
}
|
||||||
|
s.Log.Infof("📉 [CLOSING KEUANGAN] Total Depletion: %.2f", totalDepletion)
|
||||||
|
|
||||||
|
input := dto.ClosingKeuanganInput{
|
||||||
|
ProjectFlockCategory: projectFlock.Category,
|
||||||
|
PurchaseItems: purchaseItems,
|
||||||
|
Budgets: budgets,
|
||||||
|
Realizations: realizations,
|
||||||
|
DeliveryProducts: deliveryProducts,
|
||||||
|
Chickins: chickins,
|
||||||
|
TotalWeightProduced: totalWeightProduced,
|
||||||
|
TotalEggWeightKg: totalEggWeightKg,
|
||||||
|
TotalDepletion: totalDepletion,
|
||||||
|
}
|
||||||
|
|
||||||
|
report := dto.ToClosingKeuanganReport(input)
|
||||||
|
|
||||||
|
s.Log.Infof("✅ [CLOSING KEUANGAN] Report generated successfully for ProjectFlockID: %d", projectFlockID)
|
||||||
|
return &report, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
|
rows, err := s.ClosingKeuanganRepo.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err)
|
s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP")
|
||||||
@@ -682,16 +711,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectFlockKandangIDs []uint
|
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
|
||||||
if kandangID != nil && *kandangID > 0 {
|
if err != nil {
|
||||||
projectFlockKandangIDs = []uint{*kandangID}
|
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
|
||||||
} else {
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
|
||||||
var err error
|
|
||||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
|
||||||
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 {
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
@@ -930,19 +953,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
if !isGrowing {
|
if !isGrowing {
|
||||||
if targetAverages.HenDayCount > 0 {
|
if targetAverages.HenDayCount > 0 {
|
||||||
henDayAct := targetAverages.HenDayAvg
|
henDayAct := targetAverages.HenDayAvg
|
||||||
performance.HenDayAct = henDayAct
|
performance.HenDayAct = &henDayAct
|
||||||
}
|
}
|
||||||
if targetAverages.HenHouseCount > 0 {
|
if targetAverages.HenHouseCount > 0 {
|
||||||
henHouseAct := targetAverages.HenHouseAvg
|
henHouseAct := targetAverages.HenHouseAvg
|
||||||
performance.HenHouseAct = henHouseAct
|
performance.HenHouseAct = &henHouseAct
|
||||||
}
|
}
|
||||||
if targetAverages.EggWeightCount > 0 {
|
if targetAverages.EggWeightCount > 0 {
|
||||||
eggWeight := targetAverages.EggWeightAvg
|
eggWeight := targetAverages.EggWeightAvg
|
||||||
performance.EggWeight = eggWeight
|
performance.EggWeight = &eggWeight
|
||||||
}
|
}
|
||||||
if targetAverages.EggMassCount > 0 {
|
if targetAverages.EggMassCount > 0 {
|
||||||
eggMass := targetAverages.EggMassAvg
|
eggMass := targetAverages.EggMassAvg
|
||||||
performance.EggMass = eggMass
|
performance.EggMass = &eggMass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
performance.DeffFcr = performance.FcrStd - performance.FcrAct
|
performance.DeffFcr = performance.FcrStd - performance.FcrAct
|
||||||
@@ -952,16 +975,16 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
|||||||
}
|
}
|
||||||
if !isGrowing {
|
if !isGrowing {
|
||||||
if productionStandardDetail.TargetHenDayProduction != nil {
|
if productionStandardDetail.TargetHenDayProduction != nil {
|
||||||
performance.HendayStd = *productionStandardDetail.TargetHenDayProduction
|
performance.HendayStd = productionStandardDetail.TargetHenDayProduction
|
||||||
}
|
}
|
||||||
if productionStandardDetail.TargetHenHouseProduction != nil {
|
if productionStandardDetail.TargetHenHouseProduction != nil {
|
||||||
performance.HenHouseStd = *productionStandardDetail.TargetHenHouseProduction
|
performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction
|
||||||
}
|
}
|
||||||
if productionStandardDetail.TargetEggWeight != nil {
|
if productionStandardDetail.TargetEggWeight != nil {
|
||||||
performance.EggWeightStd = *productionStandardDetail.TargetEggWeight
|
performance.EggWeightStd = productionStandardDetail.TargetEggWeight
|
||||||
}
|
}
|
||||||
if productionStandardDetail.TargetEggMass != nil {
|
if productionStandardDetail.TargetEggMass != nil {
|
||||||
performance.EggMassStd = *productionStandardDetail.TargetEggMass
|
performance.EggMassStd = productionStandardDetail.TargetEggMass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1100,3 +1123,53 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
|
|||||||
|
|
||||||
return closest.Mortality, closest.FcrNumber
|
return closest.Mortality, closest.FcrNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, actualUsageRows []repository.ActualUsageCostRow) []entity.PurchaseItem {
|
||||||
|
if len(actualUsageRows) == 0 {
|
||||||
|
return []entity.PurchaseItem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all product IDs
|
||||||
|
productIDs := make([]uint, len(actualUsageRows))
|
||||||
|
for i, row := range actualUsageRows {
|
||||||
|
productIDs[i] = row.ProductID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch products with flags from repository
|
||||||
|
products, err := s.ClosingKeuanganRepo.GetProductsWithFlagsByIDs(ctx, productIDs)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
|
||||||
|
products = []entity.Product{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create product map
|
||||||
|
productMap := make(map[uint]*entity.Product)
|
||||||
|
for i := range products {
|
||||||
|
productMap[products[i].Id] = &products[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to pseudo purchase items
|
||||||
|
purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows))
|
||||||
|
for _, row := range actualUsageRows {
|
||||||
|
product := productMap[row.ProductID]
|
||||||
|
|
||||||
|
// Skip if product not found
|
||||||
|
if product == nil {
|
||||||
|
s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseItem := entity.PurchaseItem{
|
||||||
|
Id: 0, // Pseudo item, no ID
|
||||||
|
ProductId: row.ProductID,
|
||||||
|
TotalQty: row.TotalQty,
|
||||||
|
TotalPrice: row.TotalPrice,
|
||||||
|
Price: row.AveragePrice,
|
||||||
|
Product: product,
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseItems = append(purchaseItems, purchaseItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return purchaseItems
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,640 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
|
||||||
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
|
||||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/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"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClosingKeuanganService handles closing keuangan business logic
|
|
||||||
type ClosingKeuanganService interface {
|
|
||||||
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error)
|
|
||||||
GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type closingKeuanganService struct {
|
|
||||||
Log *logrus.Logger
|
|
||||||
ClosingKeuanganRepo repository.ClosingKeuanganRepository
|
|
||||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
|
||||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
|
||||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
|
||||||
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
|
||||||
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
|
||||||
ChickinRepo chickinRepository.ProjectChickinRepository
|
|
||||||
RecordingRepo recordingRepository.RecordingRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClosingKeuanganService(
|
|
||||||
closingKeuanganRepo repository.ClosingKeuanganRepository,
|
|
||||||
projectFlockRepo projectflockRepository.ProjectflockRepository,
|
|
||||||
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
|
|
||||||
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
|
|
||||||
expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository,
|
|
||||||
projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
|
|
||||||
chickinRepo chickinRepository.ProjectChickinRepository,
|
|
||||||
recordingRepo recordingRepository.RecordingRepository,
|
|
||||||
) ClosingKeuanganService {
|
|
||||||
return &closingKeuanganService{
|
|
||||||
Log: utils.Log,
|
|
||||||
ClosingKeuanganRepo: closingKeuanganRepo,
|
|
||||||
ProjectFlockRepo: projectFlockRepo,
|
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
|
||||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
|
||||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
|
||||||
ProjectBudgetRepo: projectBudgetRepo,
|
|
||||||
ChickinRepo: chickinRepo,
|
|
||||||
RecordingRepo: recordingRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) {
|
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
|
||||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload Nonstock.Flags manually
|
|
||||||
var budgetIDs []uint
|
|
||||||
for _, b := range budgets {
|
|
||||||
budgetIDs = append(budgetIDs, b.Id)
|
|
||||||
}
|
|
||||||
if len(budgetIDs) > 0 {
|
|
||||||
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
|
|
||||||
Preload("Nonstock.Flags").
|
|
||||||
Where("id IN ?", budgetIDs).
|
|
||||||
Find(&budgets).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all kandang for this project flock
|
|
||||||
kandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
|
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
|
||||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate and fetch project flock kandang
|
|
||||||
kandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
|
|
||||||
}
|
|
||||||
if kandang.ProjectFlockId != projectFlockID {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preload Nonstock.Flags manually
|
|
||||||
var budgetIDs []uint
|
|
||||||
for _, b := range budgets {
|
|
||||||
budgetIDs = append(budgetIDs, b.Id)
|
|
||||||
}
|
|
||||||
if len(budgetIDs) > 0 {
|
|
||||||
err = s.ProjectBudgetRepo.DB().WithContext(c.Context()).
|
|
||||||
Preload("Nonstock.Flags").
|
|
||||||
Where("id IN ?", budgetIDs).
|
|
||||||
Find(&budgets).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
kandangs := []entity.ProjectFlockKandang{*kandang}
|
|
||||||
|
|
||||||
return s.calculateClosingKeuangan(c, projectFlock, budgets, kandangs, projectFlockID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, budgets []entity.ProjectBudget, kandangs []entity.ProjectFlockKandang, scopeID uint) (*dto.ClosingKeuanganData, error) {
|
|
||||||
// Define flag filters using constants
|
|
||||||
pakanFilters := []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher)}
|
|
||||||
ovkFilters := []string{string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia)}
|
|
||||||
ayamFilters := []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}
|
|
||||||
allFilters := append(pakanFilters, ovkFilters...)
|
|
||||||
allFilters = append(allFilters, ayamFilters...)
|
|
||||||
|
|
||||||
var allProductUsageRows []repository.ProductUsageRow
|
|
||||||
|
|
||||||
// Get ALL product usage
|
|
||||||
for _, kandang := range kandangs {
|
|
||||||
rows, err := s.ClosingKeuanganRepo.GetAllProductUsageByProjectFlockKandangID(c.Context(), kandang.Id, allFilters)
|
|
||||||
if err == nil {
|
|
||||||
allProductUsageRows = append(allProductUsageRows, rows...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classify into categories based on flag priority
|
|
||||||
var pakanProductUsageRows []repository.ProductUsageRow
|
|
||||||
var ovkProductUsageRows []repository.ProductUsageRow
|
|
||||||
var ayamProductUsageRows []repository.ProductUsageRow
|
|
||||||
|
|
||||||
for _, row := range allProductUsageRows {
|
|
||||||
// Parse flag names from comma-separated string
|
|
||||||
flagNames := strings.Split(row.FlagNames, ",")
|
|
||||||
|
|
||||||
hasPakanFlag := false
|
|
||||||
hasOvkFlag := false
|
|
||||||
hasAyamFlag := false
|
|
||||||
|
|
||||||
for _, flag := range flagNames {
|
|
||||||
flag = strings.TrimSpace(flag)
|
|
||||||
if containsItem(pakanFilters, flag) {
|
|
||||||
hasPakanFlag = true
|
|
||||||
}
|
|
||||||
if containsItem(ovkFilters, flag) {
|
|
||||||
hasOvkFlag = true
|
|
||||||
}
|
|
||||||
if containsItem(ayamFilters, flag) {
|
|
||||||
hasAyamFlag = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority: PAKAN > OVK > AYAM
|
|
||||||
if hasPakanFlag {
|
|
||||||
pakanProductUsageRows = append(pakanProductUsageRows, row)
|
|
||||||
} else if hasOvkFlag {
|
|
||||||
ovkProductUsageRows = append(ovkProductUsageRows, row)
|
|
||||||
} else if hasAyamFlag {
|
|
||||||
ayamProductUsageRows = append(ayamProductUsageRows, row)
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Calculate total price for each category
|
|
||||||
var totalPakanPrice, totalOvkPrice, totalAyamPrice float64
|
|
||||||
for _, row := range pakanProductUsageRows {
|
|
||||||
totalPakanPrice += row.TotalPengeluaran
|
|
||||||
}
|
|
||||||
for _, row := range ovkProductUsageRows {
|
|
||||||
totalOvkPrice += row.TotalPengeluaran
|
|
||||||
}
|
|
||||||
for _, row := range ayamProductUsageRows {
|
|
||||||
totalAyamPrice += row.TotalPengeluaran
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if this is per-kandang or per-project-flock scope
|
|
||||||
isPerKandang := len(kandangs) == 1
|
|
||||||
var projectFlockKandangID *uint
|
|
||||||
if isPerKandang {
|
|
||||||
kandangID := kandangs[0].Id
|
|
||||||
projectFlockKandangID = &kandangID
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Fetch realizations
|
|
||||||
var realizations []entity.ExpenseRealization
|
|
||||||
if isPerKandang && projectFlockKandangID != nil {
|
|
||||||
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID)
|
|
||||||
} else {
|
|
||||||
realizations, err = s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, nil)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
|
||||||
}
|
|
||||||
|
|
||||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlock.Id, func(db *gorm.DB) *gorm.DB {
|
|
||||||
db = db.Preload("MarketingProduct").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse").
|
|
||||||
Preload("MarketingProduct.ProductWarehouse.Product")
|
|
||||||
return db
|
|
||||||
})
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by kandang if scope is per-kandang (manual filtering after fetch)
|
|
||||||
if isPerKandang && projectFlockKandangID != nil {
|
|
||||||
filteredProducts := make([]entity.MarketingDeliveryProduct, 0)
|
|
||||||
for _, dp := range deliveryProducts {
|
|
||||||
pfKandangID := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandangId
|
|
||||||
if pfKandangID != nil && *pfKandangID == *projectFlockKandangID {
|
|
||||||
filteredProducts = append(filteredProducts, dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deliveryProducts = filteredProducts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch chickins
|
|
||||||
var chickins []entity.ProjectChickin
|
|
||||||
if isPerKandang && projectFlockKandangID != nil {
|
|
||||||
chickins, err = s.ChickinRepo.GetByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
|
||||||
} else {
|
|
||||||
chickins, err = s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get total depletion
|
|
||||||
var totalDepletion float64
|
|
||||||
if isPerKandang && projectFlockKandangID != nil {
|
|
||||||
totalDepletion, err = s.ClosingKeuanganRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
|
||||||
} else {
|
|
||||||
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
totalDepletion = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlock.Id)
|
|
||||||
if err != nil {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get actual weight from uniformity data
|
|
||||||
var totalWeightFromUniformity float64
|
|
||||||
if isPerKandang && projectFlockKandangID != nil {
|
|
||||||
totalWeightFromUniformity, err = s.ClosingKeuanganRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
|
|
||||||
} else {
|
|
||||||
totalWeightFromUniformity, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
} else if totalWeightFromUniformity > 0 {
|
|
||||||
totalWeightProduced = totalWeightFromUniformity
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch egg data only for Laying category
|
|
||||||
var totalEggWeightKg float64
|
|
||||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
// TODO: Replace with actual method to get egg weight from RecordingRepo
|
|
||||||
// totalEggWeightKg, err = s.RecordingRepo.GetEggWeightByProjectFlockID(c.Context(), projectFlock.Id)
|
|
||||||
// For now, set to 0 as placeholder
|
|
||||||
totalEggWeightKg = 0
|
|
||||||
} else {
|
|
||||||
totalEggWeightKg = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build new DTO structure
|
|
||||||
|
|
||||||
// Calculate totals
|
|
||||||
var totalPopulation float64
|
|
||||||
for _, chickin := range chickins {
|
|
||||||
totalPopulation += chickin.UsageQty
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate actual population (total population - depletion)
|
|
||||||
actualPopulation := totalPopulation - totalDepletion
|
|
||||||
|
|
||||||
// Calculate budget totals by category
|
|
||||||
calculateBudgetByFlag := func(flags []string) float64 {
|
|
||||||
var total float64
|
|
||||||
for _, budget := range budgets {
|
|
||||||
if budget.Nonstock != nil {
|
|
||||||
for _, nonstockFlag := range budget.Nonstock.Flags {
|
|
||||||
flagName := strings.ToUpper(nonstockFlag.Name)
|
|
||||||
for _, targetFlag := range flags {
|
|
||||||
if flagName == strings.ToUpper(targetFlag) {
|
|
||||||
total += budget.Price * budget.Qty
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
// Budget per category
|
|
||||||
budgetPakan := calculateBudgetByFlag([]string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"})
|
|
||||||
budgetOvk := calculateBudgetByFlag([]string{"OVK", "OBAT", "VITAMIN", "KIMIA"})
|
|
||||||
budgetAyam := calculateBudgetByFlag([]string{"DOC", "PULLET", "LAYER"})
|
|
||||||
budgetEkspedisi := calculateBudgetByFlag([]string{"EKSPEDISI"})
|
|
||||||
|
|
||||||
// Operational budget = total budget - pakan - ovk - ayam - ekspedisi
|
|
||||||
totalBudgetAmount := 0.0
|
|
||||||
for _, budget := range budgets {
|
|
||||||
totalBudgetAmount += budget.Price * budget.Qty
|
|
||||||
}
|
|
||||||
budgetOperational := totalBudgetAmount - budgetPakan - budgetOvk - budgetAyam - budgetEkspedisi
|
|
||||||
|
|
||||||
|
|
||||||
// Calculate realization totals
|
|
||||||
var totalRealizationAmount float64
|
|
||||||
var totalEkspedisiRealization float64
|
|
||||||
for _, realization := range realizations {
|
|
||||||
amount := realization.Price * realization.Qty
|
|
||||||
totalRealizationAmount += amount
|
|
||||||
|
|
||||||
// Check if this is ekspedisi (need to check nonstock flags)
|
|
||||||
if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Nonstock != nil {
|
|
||||||
for _, flag := range realization.ExpenseNonstock.Nonstock.Flags {
|
|
||||||
if flag.Name == "EKSPEDISI" {
|
|
||||||
totalEkspedisiRealization += amount
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalOperationalRealization := totalRealizationAmount - totalEkspedisiRealization
|
|
||||||
|
|
||||||
// Filter delivery products based on category
|
|
||||||
var filteredDeliveryProducts []entity.MarketingDeliveryProduct
|
|
||||||
for _, delivery := range deliveryProducts {
|
|
||||||
// Get product from delivery
|
|
||||||
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
product := delivery.MarketingProduct.ProductWarehouse.Product
|
|
||||||
isEggProduct := false
|
|
||||||
isChickenProduct := false
|
|
||||||
|
|
||||||
// Check product flags
|
|
||||||
for _, flag := range product.Flags {
|
|
||||||
flagName := strings.ToUpper(flag.Name)
|
|
||||||
|
|
||||||
// Egg product flags
|
|
||||||
if flagName == "TELUR" || flagName == "TELURUTUH" || flagName == "TELURPECAH" ||
|
|
||||||
flagName == "TELURPUTIH" || flagName == "TELURRETAK" {
|
|
||||||
isEggProduct = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chicken product flags
|
|
||||||
if flagName == "AYAMAFKIR" || flagName == "AYAMCULLING" || flagName == "AYAMMATI" {
|
|
||||||
isChickenProduct = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter based on project flock category
|
|
||||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
// Laying: only egg products
|
|
||||||
if isEggProduct {
|
|
||||||
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Growing/Contract Growing: only chicken products
|
|
||||||
if isChickenProduct || (!isEggProduct && !isChickenProduct) {
|
|
||||||
// Include if chicken product or if no specific flags (default to chicken)
|
|
||||||
filteredDeliveryProducts = append(filteredDeliveryProducts, delivery)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Calculate total weight sold and sales amount from filtered products
|
|
||||||
var totalWeightSold float64
|
|
||||||
var totalSalesAmount float64
|
|
||||||
for _, delivery := range filteredDeliveryProducts {
|
|
||||||
totalWeightSold += delivery.TotalWeight
|
|
||||||
totalSalesAmount += delivery.TotalPrice
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Calculate metrics - always use kg ayam for rp_per_kg
|
|
||||||
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
|
||||||
if actualPopulation > 0 {
|
|
||||||
rpPerBird = amount / actualPopulation // Use actual population
|
|
||||||
}
|
|
||||||
if totalWeightProduced > 0 {
|
|
||||||
rpPerKg = amount / totalWeightProduced
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate metrics for profit loss (use total population and total weight produced)
|
|
||||||
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
|
||||||
if totalPopulation > 0 {
|
|
||||||
rpPerBird = amount / totalPopulation
|
|
||||||
}
|
|
||||||
if totalWeightProduced > 0 {
|
|
||||||
rpPerKg = amount / totalWeightProduced
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build HPP Items using constants
|
|
||||||
hppItems := []dto.HPPItem{}
|
|
||||||
|
|
||||||
// PAKAN item
|
|
||||||
pakanBudgetRpPerBird, pakanBudgetRpPerKg := calculateMetrics(budgetPakan)
|
|
||||||
pakanRealizationRpPerBird, pakanRealizationRpPerKg := calculateMetrics(totalPakanPrice)
|
|
||||||
hppItems = append(hppItems, dto.ToHPPItem(
|
|
||||||
1,
|
|
||||||
"purchase",
|
|
||||||
string(dto.HPPCodePakan),
|
|
||||||
"Pembelian Pakan",
|
|
||||||
dto.ToFinancialMetrics(pakanBudgetRpPerBird, pakanBudgetRpPerKg, budgetPakan),
|
|
||||||
dto.ToFinancialMetrics(pakanRealizationRpPerBird, pakanRealizationRpPerKg, totalPakanPrice),
|
|
||||||
))
|
|
||||||
|
|
||||||
// OVK item
|
|
||||||
ovkBudgetRpPerBird, ovkBudgetRpPerKg := calculateMetrics(budgetOvk)
|
|
||||||
ovkRealizationRpPerBird, ovkRealizationRpPerKg := calculateMetrics(totalOvkPrice)
|
|
||||||
hppItems = append(hppItems, dto.ToHPPItem(
|
|
||||||
2,
|
|
||||||
"purchase",
|
|
||||||
string(dto.HPPCodeOVK),
|
|
||||||
"Pembelian OVK",
|
|
||||||
dto.ToFinancialMetrics(ovkBudgetRpPerBird, ovkBudgetRpPerKg, budgetOvk),
|
|
||||||
dto.ToFinancialMetrics(ovkRealizationRpPerBird, ovkRealizationRpPerKg, totalOvkPrice),
|
|
||||||
))
|
|
||||||
|
|
||||||
// DOC/DEPRESIASI item
|
|
||||||
docCode := string(dto.HPPCodeDOC)
|
|
||||||
docLabel := "Pembelian DOC"
|
|
||||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
docCode = string(dto.HPPCodeDepresiasi)
|
|
||||||
docLabel = "Depresiasi"
|
|
||||||
}
|
|
||||||
docBudgetRpPerBird, docBudgetRpPerKg := calculateMetrics(budgetAyam)
|
|
||||||
docRealizationRpPerBird, docRealizationRpPerKg := calculateMetrics(totalAyamPrice)
|
|
||||||
hppItems = append(hppItems, dto.ToHPPItem(
|
|
||||||
3,
|
|
||||||
"purchase",
|
|
||||||
docCode,
|
|
||||||
docLabel,
|
|
||||||
dto.ToFinancialMetrics(docBudgetRpPerBird, docBudgetRpPerKg, budgetAyam),
|
|
||||||
dto.ToFinancialMetrics(docRealizationRpPerBird, docRealizationRpPerKg, totalAyamPrice),
|
|
||||||
))
|
|
||||||
|
|
||||||
// OVERHEAD item
|
|
||||||
overheadBudgetRpPerBird, overheadBudgetRpPerKg := calculateMetrics(budgetOperational)
|
|
||||||
overheadRealizationRpPerBird, overheadRealizationRpPerKg := calculateMetrics(totalOperationalRealization)
|
|
||||||
hppItems = append(hppItems, dto.ToHPPItem(
|
|
||||||
4,
|
|
||||||
"overhead",
|
|
||||||
string(dto.HPPCodeOverhead),
|
|
||||||
"Pengeluaran Overhead",
|
|
||||||
dto.ToFinancialMetrics(overheadBudgetRpPerBird, overheadBudgetRpPerKg, budgetOperational),
|
|
||||||
dto.ToFinancialMetrics(overheadRealizationRpPerBird, overheadRealizationRpPerKg, totalOperationalRealization),
|
|
||||||
))
|
|
||||||
|
|
||||||
// EKSPEDISI item
|
|
||||||
ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg := calculateMetrics(budgetEkspedisi)
|
|
||||||
ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg := calculateMetrics(totalEkspedisiRealization)
|
|
||||||
hppItems = append(hppItems, dto.ToHPPItem(
|
|
||||||
5,
|
|
||||||
"overhead",
|
|
||||||
string(dto.HPPCodeEkspedisi),
|
|
||||||
"Beban Ekspedisi",
|
|
||||||
dto.ToFinancialMetrics(ekspedisiBudgetRpPerBird, ekspedisiBudgetRpPerKg, budgetEkspedisi),
|
|
||||||
dto.ToFinancialMetrics(ekspedisiRealizationRpPerBird, ekspedisiRealizationRpPerKg, totalEkspedisiRealization),
|
|
||||||
))
|
|
||||||
|
|
||||||
// HPP Summary
|
|
||||||
totalBudgetHpp := budgetPakan + budgetOvk + budgetAyam + budgetOperational + budgetEkspedisi
|
|
||||||
totalRealizationHpp := totalPakanPrice + totalOvkPrice + totalAyamPrice + totalOperationalRealization + totalEkspedisiRealization
|
|
||||||
|
|
||||||
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
|
|
||||||
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
|
|
||||||
|
|
||||||
var eggBudgeting, eggRealization *dto.FinancialMetrics
|
|
||||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) && totalEggWeightKg > 0 {
|
|
||||||
eggBudgetRpPerKg := totalBudgetHpp / totalEggWeightKg
|
|
||||||
eggRealizationRpPerKg := totalRealizationHpp / totalEggWeightKg
|
|
||||||
eggBudgeting = &dto.FinancialMetrics{
|
|
||||||
RpPerBird: 0,
|
|
||||||
RpPerKg: eggBudgetRpPerKg,
|
|
||||||
Amount: totalBudgetHpp,
|
|
||||||
}
|
|
||||||
eggRealization = &dto.FinancialMetrics{
|
|
||||||
RpPerBird: 0,
|
|
||||||
RpPerKg: eggRealizationRpPerKg,
|
|
||||||
Amount: totalRealizationHpp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hppSummary := dto.ToHPPSummary(
|
|
||||||
"HPP",
|
|
||||||
dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp),
|
|
||||||
dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp),
|
|
||||||
eggBudgeting,
|
|
||||||
eggRealization,
|
|
||||||
)
|
|
||||||
|
|
||||||
hppSection := dto.ToHPPSection(hppItems, hppSummary)
|
|
||||||
|
|
||||||
// Build Profit Loss Items using constants
|
|
||||||
plItems := []dto.ProfitLossItem{}
|
|
||||||
|
|
||||||
// SALES item
|
|
||||||
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
|
|
||||||
salesLabel := "Penjualan Ayam"
|
|
||||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
salesLabel = "Penjualan Telur"
|
|
||||||
}
|
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
|
||||||
string(dto.PLCodeSales),
|
|
||||||
salesLabel,
|
|
||||||
"income",
|
|
||||||
salesRpPerBird,
|
|
||||||
salesRpPerKg,
|
|
||||||
totalSalesAmount,
|
|
||||||
))
|
|
||||||
|
|
||||||
// SAPRONAK item - combines DOC/Depresiasi + PAKAN + OVK
|
|
||||||
totalSapronakAmount := totalAyamPrice + totalPakanPrice + totalOvkPrice
|
|
||||||
sapronakRpPerBird := docRealizationRpPerBird + pakanRealizationRpPerBird + ovkRealizationRpPerBird
|
|
||||||
sapronakRpPerKg := docRealizationRpPerKg + pakanRealizationRpPerKg + ovkRealizationRpPerKg
|
|
||||||
sapronakLabel := "Pengeluaran Sapronak"
|
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
|
||||||
string(dto.PLCodeSapronak),
|
|
||||||
sapronakLabel,
|
|
||||||
"purchase",
|
|
||||||
sapronakRpPerBird,
|
|
||||||
sapronakRpPerKg,
|
|
||||||
totalSapronakAmount,
|
|
||||||
))
|
|
||||||
|
|
||||||
// OVERHEAD item
|
|
||||||
overheadRpPerBird, overheadRpPerKg := calculateMetrics(totalOperationalRealization)
|
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
|
||||||
string(dto.PLCodeOverhead),
|
|
||||||
"Overhead",
|
|
||||||
"overhead",
|
|
||||||
overheadRpPerBird,
|
|
||||||
overheadRpPerKg,
|
|
||||||
totalOperationalRealization,
|
|
||||||
))
|
|
||||||
|
|
||||||
// EKSPEDISI item
|
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
|
||||||
string(dto.PLCodeEkspedisi),
|
|
||||||
"Ekspedisi",
|
|
||||||
"overhead",
|
|
||||||
ekspedisiRealizationRpPerBird,
|
|
||||||
ekspedisiRealizationRpPerKg,
|
|
||||||
totalEkspedisiRealization,
|
|
||||||
))
|
|
||||||
|
|
||||||
// Profit Loss Summary
|
|
||||||
// Gross Profit = Sales - (DOC + PAKAN + OVK) only
|
|
||||||
// Gross Profit should NOT include overhead and ekspedisi
|
|
||||||
costOfGoodsSold := totalAyamPrice + totalPakanPrice + totalOvkPrice
|
|
||||||
costOfGoodsSoldRpPerBird := sapronakRpPerBird
|
|
||||||
|
|
||||||
grossProfit := totalSalesAmount - costOfGoodsSold
|
|
||||||
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
|
|
||||||
|
|
||||||
// Operating Expenses (Overhead + Ekspedisi)
|
|
||||||
totalOperatingExpenses := totalOperationalRealization + totalEkspedisiRealization
|
|
||||||
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRealizationRpPerBird
|
|
||||||
|
|
||||||
// Net Profit = Gross Profit - Operating Expenses
|
|
||||||
netProfit := grossProfit - totalOperatingExpenses
|
|
||||||
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
|
|
||||||
|
|
||||||
plSummary := dto.ToProfitLossSummary(
|
|
||||||
dto.ToFinancialMetrics(grossProfitRpPerBird, 0, grossProfit),
|
|
||||||
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, 0, totalOperatingExpenses),
|
|
||||||
dto.ToFinancialMetrics(netProfitRpPerBird, 0, netProfit),
|
|
||||||
)
|
|
||||||
|
|
||||||
profitLossSection := dto.ToProfitLossSection(plItems, plSummary)
|
|
||||||
|
|
||||||
// Build complete response
|
|
||||||
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
|
|
||||||
|
|
||||||
return &data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsItem checks if a string exists in a slice
|
|
||||||
func containsItem(slice []string, item string) bool {
|
|
||||||
for _, s := range slice {
|
|
||||||
if strings.EqualFold(s, item) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,8 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -112,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||||
@@ -126,6 +126,8 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
KandangName: pfk.Kandang.Name,
|
KandangName: pfk.Kandang.Name,
|
||||||
Period: pfk.Period,
|
Period: pfk.Period,
|
||||||
Status: status,
|
Status: status,
|
||||||
|
StartDate: nil,
|
||||||
|
EndDate: nil,
|
||||||
TotalIncomingValue: totalIncoming,
|
TotalIncomingValue: totalIncoming,
|
||||||
TotalUsageValue: totalUsage,
|
TotalUsageValue: totalUsage,
|
||||||
Items: items,
|
Items: items,
|
||||||
@@ -263,7 +265,6 @@ type sapronakDetailMaps struct {
|
|||||||
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||||
TransferIn map[uint][]dto.SapronakDetailDTO
|
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||||
TransferOut map[uint][]dto.SapronakDetailDTO
|
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||||
SalesOut map[uint][]dto.SapronakDetailDTO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSapronakDetails(
|
func buildSapronakDetails(
|
||||||
@@ -273,7 +274,6 @@ func buildSapronakDetails(
|
|||||||
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||||
transferInRows map[uint][]repository.SapronakDetailRow,
|
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||||
transferOutRows map[uint][]repository.SapronakDetailRow,
|
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||||
salesOutRows map[uint][]repository.SapronakDetailRow,
|
|
||||||
) sapronakDetailMaps {
|
) sapronakDetailMaps {
|
||||||
result := sapronakDetailMaps{
|
result := sapronakDetailMaps{
|
||||||
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
@@ -282,7 +282,6 @@ func buildSapronakDetails(
|
|||||||
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
SalesOut: make(map[uint][]dto.SapronakDetailDTO),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||||
@@ -315,12 +314,11 @@ func buildSapronakDetails(
|
|||||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||||
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||||
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||||
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||||
// For sapronak closing report we intentionally ignore date range
|
// For sapronak closing report we intentionally ignore date range
|
||||||
// and aggregate all historical transactions for the kandang/project.
|
// and aggregate all historical transactions for the kandang/project.
|
||||||
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||||
@@ -355,10 +353,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, 0, err
|
return nil, nil, 0, 0, err
|
||||||
}
|
}
|
||||||
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.KandangId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||||
matchesFlag := func(f string) bool {
|
matchesFlag := func(f string) bool {
|
||||||
@@ -371,34 +365,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
return candidate == filterFlag
|
return candidate == filterFlag
|
||||||
}
|
}
|
||||||
dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
|
|
||||||
result := make(map[uint][]dto.SapronakDetailDTO, len(src))
|
|
||||||
seen := make(map[string]struct{})
|
|
||||||
for pid, rows := range src {
|
|
||||||
for _, d := range rows {
|
|
||||||
dateKey := ""
|
|
||||||
if d.Tanggal != nil {
|
|
||||||
dateKey = d.Tanggal.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
qtyKey := d.QtyMasuk
|
|
||||||
if qtyKey == 0 {
|
|
||||||
qtyKey = d.QtyKeluar
|
|
||||||
}
|
|
||||||
|
|
||||||
ref := strings.TrimSpace(d.NoReferensi)
|
|
||||||
key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
|
|
||||||
if ref == "" {
|
|
||||||
key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
|
|
||||||
}
|
|
||||||
if _, ok := seen[key]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
result[pid] = append(result[pid], d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// For project flocks with category GROWING, pullet usage from chickin
|
// For project flocks with category GROWING, pullet usage from chickin
|
||||||
// should not be counted yet. Only when category is LAYING we allow
|
// should not be counted yet. Only when category is LAYING we allow
|
||||||
@@ -437,17 +403,13 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||||
}
|
}
|
||||||
|
|
||||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows)
|
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||||
incomingDetails := detailMaps.Incoming
|
incomingDetails := detailMaps.Incoming
|
||||||
usageDetails := detailMaps.Usage
|
usageDetails := detailMaps.Usage
|
||||||
adjIncoming := detailMaps.AdjIncoming
|
adjIncoming := detailMaps.AdjIncoming
|
||||||
adjOutgoing := detailMaps.AdjOutgoing
|
adjOutgoing := detailMaps.AdjOutgoing
|
||||||
transIncoming := detailMaps.TransferIn
|
transIncoming := detailMaps.TransferIn
|
||||||
transOutgoing := detailMaps.TransferOut
|
transOutgoing := detailMaps.TransferOut
|
||||||
salesOutgoing := detailMaps.SalesOut
|
|
||||||
|
|
||||||
transIncoming = dedupTransfers(transIncoming)
|
|
||||||
transOutgoing = dedupTransfers(transOutgoing)
|
|
||||||
|
|
||||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
if g, ok := groupMap[flag]; ok {
|
if g, ok := groupMap[flag]; ok {
|
||||||
@@ -457,22 +419,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
return groupMap[flag]
|
return groupMap[flag]
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) {
|
|
||||||
flag := ""
|
|
||||||
name := ""
|
|
||||||
if item, ok := itemMap[productID]; ok {
|
|
||||||
flag = item.Flag
|
|
||||||
name = item.ProductName
|
|
||||||
}
|
|
||||||
if flag == "" && len(details) > 0 {
|
|
||||||
flag = details[0].Flag
|
|
||||||
}
|
|
||||||
if name == "" && len(details) > 0 {
|
|
||||||
name = details[0].ProductName
|
|
||||||
}
|
|
||||||
return flag, name
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range incoming {
|
for _, row := range incoming {
|
||||||
if !matchesFlag(row.Flag) {
|
if !matchesFlag(row.Flag) {
|
||||||
continue
|
continue
|
||||||
@@ -608,18 +554,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range incomingDetails {
|
for productID, details := range incomingDetails {
|
||||||
flag, name := resolveFlagName(productID, details)
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
if d.Flag == "" {
|
d.Flag = flag
|
||||||
d.Flag = flag
|
d.ProductName = name
|
||||||
}
|
|
||||||
if d.ProductName == "" {
|
|
||||||
d.ProductName = name
|
|
||||||
}
|
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -628,18 +575,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range adjIncoming {
|
for productID, details := range adjIncoming {
|
||||||
flag, name := resolveFlagName(productID, details)
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
if d.Flag == "" {
|
d.Flag = flag
|
||||||
d.Flag = flag
|
d.ProductName = name
|
||||||
}
|
|
||||||
if d.ProductName == "" {
|
|
||||||
d.ProductName = name
|
|
||||||
}
|
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -648,18 +596,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range usageDetails {
|
for productID, details := range usageDetails {
|
||||||
flag, name := resolveFlagName(productID, details)
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
if d.Flag == "" {
|
d.Flag = flag
|
||||||
d.Flag = flag
|
d.ProductName = name
|
||||||
}
|
|
||||||
if d.ProductName == "" {
|
|
||||||
d.ProductName = name
|
|
||||||
}
|
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
@@ -667,18 +616,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range adjOutgoing {
|
for productID, details := range adjOutgoing {
|
||||||
flag, name := resolveFlagName(productID, details)
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
if d.Flag == "" {
|
d.Flag = flag
|
||||||
d.Flag = flag
|
d.ProductName = name
|
||||||
}
|
|
||||||
if d.ProductName == "" {
|
|
||||||
d.ProductName = name
|
|
||||||
}
|
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
@@ -686,18 +636,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range transIncoming {
|
for productID, details := range transIncoming {
|
||||||
flag, name := resolveFlagName(productID, details)
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
if d.Flag == "" {
|
d.Flag = flag
|
||||||
d.Flag = flag
|
d.ProductName = name
|
||||||
}
|
|
||||||
if d.ProductName == "" {
|
|
||||||
d.ProductName = name
|
|
||||||
}
|
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalMasuk += d.QtyMasuk
|
group.TotalMasuk += d.QtyMasuk
|
||||||
group.TotalNilai += d.Nilai
|
group.TotalNilai += d.Nilai
|
||||||
@@ -706,37 +657,19 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for productID, details := range transOutgoing {
|
for productID, details := range transOutgoing {
|
||||||
flag, name := resolveFlagName(productID, details)
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
if !matchesFlag(flag) {
|
if !matchesFlag(flag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group := ensureGroup(flag)
|
group := ensureGroup(flag)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
if d.Flag == "" {
|
d.Flag = flag
|
||||||
d.Flag = flag
|
d.ProductName = name
|
||||||
}
|
|
||||||
if d.ProductName == "" {
|
|
||||||
d.ProductName = name
|
|
||||||
}
|
|
||||||
group.Items = append(group.Items, d)
|
|
||||||
group.TotalKeluar += d.QtyKeluar
|
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for productID, details := range salesOutgoing {
|
|
||||||
flag, name := resolveFlagName(productID, details)
|
|
||||||
if !matchesFlag(flag) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
group := ensureGroup(flag)
|
|
||||||
for _, d := range details {
|
|
||||||
if d.Flag == "" {
|
|
||||||
d.Flag = flag
|
|
||||||
}
|
|
||||||
if d.ProductName == "" {
|
|
||||||
d.ProductName = name
|
|
||||||
}
|
|
||||||
group.Items = append(group.Items, d)
|
group.Items = append(group.Items, d)
|
||||||
group.TotalKeluar += d.QtyKeluar
|
group.TotalKeluar += d.QtyKeluar
|
||||||
group.SaldoAkhir -= d.QtyKeluar
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
|||||||
@@ -24,5 +24,4 @@ type ClosingSapronakQuery struct {
|
|||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=100"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,10 +396,6 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
updateBody["supplier_id"] = *req.SupplierID
|
updateBody["supplier_id"] = *req.SupplierID
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Notes != nil {
|
|
||||||
updateBody["notes"] = *req.Notes
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.LocationID != nil {
|
if req.LocationID != nil {
|
||||||
locationID := uint(*req.LocationID)
|
locationID := uint(*req.LocationID)
|
||||||
updateBody["location_id"] = locationID
|
updateBody["location_id"] = locationID
|
||||||
@@ -572,28 +568,20 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
}
|
}
|
||||||
|
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||||
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
|
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionUpdated
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
|
|
||||||
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
|
|
||||||
|
|
||||||
if previousStep < utils.ExpenseStepPengajuan {
|
|
||||||
previousStep = utils.ExpenseStepPengajuan
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowExpense,
|
utils.ApprovalWorkflowExpense,
|
||||||
id,
|
id,
|
||||||
previousStep,
|
utils.ExpenseStepPengajuan,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
actorID,
|
actorID,
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ type Update struct {
|
|||||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
||||||
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,20 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransferRelationDTO struct {
|
type TransferRelationDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
MovementNumber string `json:"movement_number"`
|
TransferReason string `json:"transfer_reason"`
|
||||||
TransferReason string `json:"transfer_reason"`
|
TransferDate string `json:"transfer_date"`
|
||||||
TransferDate string `json:"transfer_date"`
|
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
|
||||||
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
|
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
||||||
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
|
}
|
||||||
|
|
||||||
|
type WarehouseSimpleDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductSimpleDTO struct {
|
type ProductSimpleDTO struct {
|
||||||
@@ -22,6 +25,16 @@ type ProductSimpleDTO struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AreaDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
type SupplierSimpleDTO struct {
|
type SupplierSimpleDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -35,6 +48,13 @@ type DocumentDTO struct {
|
|||||||
Size float64 `json:"size"`
|
Size float64 `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WarehouseDetailDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Location *LocationDTO `json:"location"`
|
||||||
|
Area *AreaDTO `json:"area"`
|
||||||
|
}
|
||||||
|
|
||||||
type TransferListDTO struct {
|
type TransferListDTO struct {
|
||||||
TransferRelationDTO
|
TransferRelationDTO
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
@@ -77,19 +97,16 @@ type TransferDeliveryItemDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||||
var sourceWarehouse *warehouseDTO.WarehouseRelationDTO
|
var sourceWarehouse *WarehouseDetailDTO
|
||||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse)
|
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
||||||
sourceWarehouse = &mapped
|
|
||||||
}
|
}
|
||||||
var destinationWarehouse *warehouseDTO.WarehouseRelationDTO
|
var destinationWarehouse *WarehouseDetailDTO
|
||||||
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
||||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse)
|
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
|
||||||
destinationWarehouse = &mapped
|
|
||||||
}
|
}
|
||||||
return TransferRelationDTO{
|
return TransferRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
MovementNumber: e.MovementNumber,
|
|
||||||
TransferReason: e.Reason,
|
TransferReason: e.Reason,
|
||||||
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
||||||
SourceWarehouse: sourceWarehouse,
|
SourceWarehouse: sourceWarehouse,
|
||||||
@@ -97,6 +114,38 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toAreaDTO(a *entity.Area) *AreaDTO {
|
||||||
|
if a == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &AreaDTO{
|
||||||
|
Id: a.Id,
|
||||||
|
Name: a.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLocationDTO(l *entity.Location) *LocationDTO {
|
||||||
|
if l == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &LocationDTO{
|
||||||
|
Id: l.Id,
|
||||||
|
Name: l.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
||||||
|
if w == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &WarehouseDetailDTO{
|
||||||
|
Id: w.Id,
|
||||||
|
Name: w.Name,
|
||||||
|
Location: toLocationDTO(w.Location),
|
||||||
|
Area: toAreaDTO(&w.Area),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||||
var createdUser *userDTO.UserRelationDTO
|
var createdUser *userDTO.UserRelationDTO
|
||||||
if e.CreatedUser != nil {
|
if e.CreatedUser != nil {
|
||||||
|
|||||||
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq)
|
movementNumber := fmt.Sprintf("ST-%05d", seq)
|
||||||
return movementNumber, nil
|
return movementNumber, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
|
|||||||
route := v1.Group("/transfers")
|
route := v1.Group("/transfers")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
||||||
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
||||||
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,11 +99,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
|
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
||||||
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
|
|
||||||
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
|
|
||||||
searchTerm, searchTerm, searchTerm)
|
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -122,9 +118,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
|
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
||||||
}
|
}
|
||||||
|
|
||||||
return transferPtr, nil
|
return transferPtr, nil
|
||||||
@@ -140,12 +136,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
|
||||||
}
|
}
|
||||||
if sourcePW.Quantity < product.ProductQty {
|
if sourcePW.Quantity < product.ProductQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
||||||
}
|
}
|
||||||
pwIDs = append(pwIDs, sourcePW.Id)
|
pwIDs = append(pwIDs, sourcePW.Id)
|
||||||
}
|
}
|
||||||
@@ -165,10 +161,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
|
|
||||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||||
}
|
}
|
||||||
if projectFlockKandang.ClosedAt != nil {
|
if projectFlockKandang.ClosedAt != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
@@ -196,16 +192,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
||||||
}
|
}
|
||||||
if supplier.Category != string(utils.SupplierCategoryBOP) {
|
if supplier.Category != string(utils.SupplierCategoryBOP) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||||
}
|
}
|
||||||
|
|
||||||
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
||||||
@@ -243,16 +239,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID))
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
||||||
}
|
}
|
||||||
|
|
||||||
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
||||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||||
)
|
)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination")
|
||||||
}
|
}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
@@ -260,21 +256,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set ProjectFlockKandangId hanya jika ada kandang
|
|
||||||
var pfkID *uint
|
|
||||||
if projectFlockKandangID > 0 {
|
|
||||||
pfkID = &projectFlockKandangID
|
|
||||||
}
|
|
||||||
|
|
||||||
destPW = &entity.ProductWarehouse{
|
destPW = &entity.ProductWarehouse{
|
||||||
ProductId: uint(product.ProductID),
|
ProductId: uint(product.ProductID),
|
||||||
WarehouseId: uint(req.DestinationWarehouseID),
|
WarehouseId: uint(req.DestinationWarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
ProjectFlockKandangId: pfkID,
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
}
|
}
|
||||||
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
|
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +309,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
for _, prod := range item.Products {
|
for _, prod := range item.Products {
|
||||||
detail, ok := detailMap[uint64(prod.ProductID)]
|
detail, ok := detailMap[uint64(prod.ProductID)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
|
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
||||||
}
|
}
|
||||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
||||||
StockTransferDeliveryId: delivery.Id,
|
StockTransferDeliveryId: delivery.Id,
|
||||||
@@ -383,7 +372,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
@@ -392,7 +381,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
"usage_qty": consumeResult.UsageQuantity,
|
"usage_qty": consumeResult.UsageQuantity,
|
||||||
"pending_qty": consumeResult.PendingQuantity,
|
"pending_qty": consumeResult.PendingQuantity,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID))
|
return fmt.Errorf("gagal update usage tracking: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||||
@@ -405,7 +394,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
@@ -413,7 +402,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"total_qty": replenishResult.AddedQuantity,
|
"total_qty": replenishResult.AddedQuantity,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID))
|
return fmt.Errorf("gagal update total tracking: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +436,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||||
@@ -457,7 +446,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
|
|
||||||
if len(expensePayloads) > 0 {
|
if len(expensePayloads) > 0 {
|
||||||
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
|
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber))
|
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,26 +461,32 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
|
|||||||
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
|
||||||
|
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||||
}
|
}
|
||||||
return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID))
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
|
|
||||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
return 0, nil
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||||
}
|
}
|
||||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif")
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||||
}
|
}
|
||||||
|
|
||||||
return uint(projectFlockKandang.Id), nil
|
return uint(projectFlockKandang.Id), nil
|
||||||
|
|||||||
@@ -140,21 +140,12 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
|
|||||||
if actorID == 0 {
|
if actorID == 0 {
|
||||||
actorID = 1
|
actorID = 1
|
||||||
}
|
}
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||||
svc := commonSvc.NewApprovalService(approvalRepo)
|
action := entity.ApprovalActionUpdated
|
||||||
action := entity.ApprovalActionCreated
|
|
||||||
|
|
||||||
for id := range expenseIDs {
|
for id := range expenseIDs {
|
||||||
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if latestApproval == nil {
|
|
||||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
||||||
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
||||||
|
|
||||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||||
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,30 +148,18 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.WarehouseId > 0 || filters.Search != "" {
|
if filters.WarehouseId > 0 {
|
||||||
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.Search != "" {
|
if filters.Search != "" {
|
||||||
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
|
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
|
||||||
db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id")
|
}
|
||||||
|
|
||||||
|
if filters.Search != "" {
|
||||||
searchPattern := "%" + filters.Search + "%"
|
searchPattern := "%" + filters.Search + "%"
|
||||||
db = db.Where(`(
|
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
|
||||||
marketing_delivery_products.vehicle_number ILIKE ? OR
|
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||||
customers.name ILIKE ? OR
|
|
||||||
warehouses.name ILIKE ? OR
|
|
||||||
products.name ILIKE ? OR
|
|
||||||
sales_users.name ILIKE ? OR
|
|
||||||
CONCAT(
|
|
||||||
marketings.so_number,
|
|
||||||
'-',
|
|
||||||
COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''),
|
|
||||||
'-',
|
|
||||||
COALESCE(product_warehouses.warehouse_id::text, '')
|
|
||||||
) ILIKE ?
|
|
||||||
)`,
|
|
||||||
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.CustomerId > 0 {
|
if filters.CustomerId > 0 {
|
||||||
@@ -190,19 +178,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.AreaId > 0 || filters.LocationId > 0 {
|
|
||||||
db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
|
||||||
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
|
|
||||||
|
|
||||||
if filters.AreaId > 0 {
|
|
||||||
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.LocationId > 0 {
|
|
||||||
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if filters.MarketingType != "" {
|
if filters.MarketingType != "" {
|
||||||
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
Group("marketing_delivery_products.id")
|
Group("marketing_delivery_products.id")
|
||||||
@@ -211,6 +186,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
|||||||
case "ayam":
|
case "ayam":
|
||||||
db = db.Where("flags.name IN (?)", []string{
|
db = db.Where("flags.name IN (?)", []string{
|
||||||
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
|
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
|
||||||
|
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
|
||||||
})
|
})
|
||||||
case "telur":
|
case "telur":
|
||||||
db = db.Where("flags.name IN (?)", []string{
|
db = db.Where("flags.name IN (?)", []string{
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
|||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||||
totalPrice := requestedProduct.UnitPrice * totalWeight
|
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
|
||||||
|
|
||||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
@@ -363,7 +363,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
|||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
|
||||||
totalPrice := requestedProduct.UnitPrice * totalWeight
|
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
|
||||||
|
|
||||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := rp.Qty * rp.AvgWeight
|
totalWeight := rp.Qty * rp.AvgWeight
|
||||||
totalPrice := rp.UnitPrice * totalWeight
|
totalPrice := rp.UnitPrice * rp.Qty
|
||||||
|
|
||||||
updateBody := map[string]any{
|
updateBody := map[string]any{
|
||||||
"product_warehouse_id": rp.ProductWarehouseId,
|
"product_warehouse_id": rp.ProductWarehouseId,
|
||||||
@@ -594,7 +594,7 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
|
|||||||
|
|
||||||
// Hitung total_weight dan total_price otomatis
|
// Hitung total_weight dan total_price otomatis
|
||||||
totalWeight := rp.Qty * rp.AvgWeight
|
totalWeight := rp.Qty * rp.AvgWeight
|
||||||
totalPrice := rp.UnitPrice * totalWeight
|
totalPrice := rp.UnitPrice * rp.Qty
|
||||||
|
|
||||||
marketingProduct := &entity.MarketingProduct{
|
marketingProduct := &entity.MarketingProduct{
|
||||||
MarketingId: marketingId,
|
MarketingId: marketingId,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ type CustomerRelationDTO struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
AccountNumber string `json:"account_number"`
|
AccountNumber string `json:"account_number"`
|
||||||
Address string `json:"address,omitempty"`
|
|
||||||
Balance float64 `json:"balance"`
|
Balance float64 `json:"balance"`
|
||||||
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
|
Pic *userDTO.UserRelationDTO `json:"pic,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -53,8 +52,6 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO {
|
|||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
Type: e.Type,
|
Type: e.Type,
|
||||||
AccountNumber: e.AccountNumber,
|
AccountNumber: e.AccountNumber,
|
||||||
Address: e.Address,
|
|
||||||
Balance: e.Balance,
|
|
||||||
Pic: pic,
|
Pic: pic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
|
|||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
Category: c.Query("category", ""),
|
Category: c.Query("category", ""),
|
||||||
Flag: c.Query("flag", ""),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ func (s supplierService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("NonstockSuppliers.Nonstock.Flags")
|
Preload("NonstockSuppliers.Nonstock.Flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s supplierService) withListRelations(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("CreatedUser")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) {
|
func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -67,7 +63,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withListRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
@@ -76,23 +72,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
|
db = db.Where("category ILIKE ?", "%"+params.Category+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.Flag != "" {
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
flag := strings.ToUpper(params.Flag)
|
|
||||||
db = db.Where(`
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM nonstock_suppliers nsup
|
|
||||||
JOIN nonstocks n ON n.id = nsup.nonstock_id
|
|
||||||
JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?
|
|
||||||
WHERE nsup.supplier_id = suppliers.id
|
|
||||||
AND UPPER(f.name) = ?
|
|
||||||
)`,
|
|
||||||
entity.FlagableTypeNonstock,
|
|
||||||
flag,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Order("suppliers.created_at DESC").Order("suppliers.updated_at DESC")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ type Update struct {
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
Flag string `query:"flag" validate:"omitempty"`
|
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
Category string `query:"category" validate:"omitempty,max=50"`
|
Category string `query:"category" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,11 +110,7 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
typ := strings.ToUpper(req.Type)
|
typ := strings.ToUpper(req.Type)
|
||||||
createValidationOpts := WarehouseTypeValidationOptions{
|
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil {
|
||||||
LocationProvided: req.LocationId != nil,
|
|
||||||
KandangProvided: req.KandangId != nil,
|
|
||||||
}
|
|
||||||
if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,22 +208,9 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
finalKandangId = req.KandangId
|
finalKandangId = req.KandangId
|
||||||
}
|
}
|
||||||
|
|
||||||
originalLocationId := finalLocationId
|
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, finalLocationId, finalKandangId); err != nil {
|
||||||
originalKandangId := finalKandangId
|
|
||||||
updateValidationOpts := WarehouseTypeValidationOptions{
|
|
||||||
AutoClear: true,
|
|
||||||
LocationProvided: req.LocationId != nil,
|
|
||||||
KandangProvided: req.KandangId != nil,
|
|
||||||
}
|
|
||||||
if err := validateWarehouseTypeRequirements(finalType, &finalAreaId, &finalLocationId, &finalKandangId, updateValidationOpts); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if originalLocationId != finalLocationId {
|
|
||||||
updateBody["location_id"] = nil
|
|
||||||
}
|
|
||||||
if originalKandangId != finalKandangId {
|
|
||||||
updateBody["kandang_id"] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) == 0 {
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
@@ -255,65 +238,47 @@ func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseTypeValidationOptions struct {
|
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID *uint, kandangID *uint) error {
|
||||||
AutoClear bool
|
|
||||||
LocationProvided bool
|
|
||||||
KandangProvided bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateWarehouseTypeRequirements(typ string, areaID *uint, locationID **uint, kandangID **uint, opts WarehouseTypeValidationOptions) error {
|
|
||||||
switch utils.WarehouseType(typ) {
|
switch utils.WarehouseType(typ) {
|
||||||
case utils.WarehouseTypeArea:
|
case utils.WarehouseTypeArea:
|
||||||
if areaID == nil || *areaID == 0 {
|
if areaID == nil || *areaID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
|
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is AREA")
|
||||||
}
|
}
|
||||||
if locationID != nil && *locationID != nil {
|
if locationID != nil {
|
||||||
if opts.AutoClear && !opts.LocationProvided {
|
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
|
||||||
*locationID = nil
|
|
||||||
} else {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id must not be provided when type is AREA")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if kandangID != nil && *kandangID != nil {
|
if kandangID != nil {
|
||||||
if opts.AutoClear && !opts.KandangProvided {
|
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
|
||||||
*kandangID = nil
|
|
||||||
} else {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is AREA")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case utils.WarehouseTypeLokasi:
|
case utils.WarehouseTypeLokasi:
|
||||||
if areaID == nil || *areaID == 0 {
|
if areaID == nil || *areaID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
|
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is LOCATION")
|
||||||
}
|
}
|
||||||
if locationID == nil || *locationID == nil {
|
if locationID == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION")
|
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is LOCATION")
|
||||||
}
|
}
|
||||||
if **locationID == 0 {
|
if *locationID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION")
|
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is LOCATION")
|
||||||
}
|
}
|
||||||
if kandangID != nil && *kandangID != nil {
|
if kandangID != nil {
|
||||||
if opts.AutoClear && !opts.KandangProvided {
|
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
|
||||||
*kandangID = nil
|
|
||||||
} else {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must not be provided when type is LOCATION")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case utils.WarehouseTypeKandang:
|
case utils.WarehouseTypeKandang:
|
||||||
if areaID == nil || *areaID == 0 {
|
if areaID == nil || *areaID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
|
return fiber.NewError(fiber.StatusBadRequest, "area_id is required when type is KANDANG")
|
||||||
}
|
}
|
||||||
if locationID == nil || *locationID == nil {
|
if locationID == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG")
|
return fiber.NewError(fiber.StatusBadRequest, "location_id is required when type is KANDANG")
|
||||||
}
|
}
|
||||||
if **locationID == 0 {
|
if *locationID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG")
|
return fiber.NewError(fiber.StatusBadRequest, "location_id must be greater than 0 when type is KANDANG")
|
||||||
}
|
}
|
||||||
if kandangID == nil || *kandangID == nil {
|
if kandangID == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG")
|
return fiber.NewError(fiber.StatusBadRequest, "kandang_id is required when type is KANDANG")
|
||||||
}
|
}
|
||||||
if **kandangID == 0 {
|
if *kandangID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG")
|
return fiber.NewError(fiber.StatusBadRequest, "kandang_id must be greater than 0 when type is KANDANG")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
|
|
||||||
newChikins = append(newChikins, newChickin)
|
newChikins = append(newChikins, newChickin)
|
||||||
|
|
||||||
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId)
|
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId))
|
||||||
}
|
}
|
||||||
|
|
||||||
availableQty := productWarehouse.Quantity - totalPopulationQty
|
availableQty := productWarehouse.Quantity - totalPopulationQty
|
||||||
@@ -584,6 +584,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// autoAddFlagToProduct adds target flag to product if not already present (idempotent)
|
||||||
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
|
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
|
||||||
if s.ProductRepo == nil {
|
if s.ProductRepo == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -54,7 +53,6 @@ type ProjectFlockKandangListDTO struct {
|
|||||||
ProjectFlockKandangRelationDTO
|
ProjectFlockKandangRelationDTO
|
||||||
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
NameWithPeriod string `json:"name_with_period"`
|
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||||
@@ -106,7 +104,6 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
|
|||||||
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
||||||
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||||
Kandang: toKandangRelation(e.Kandang),
|
Kandang: toKandangRelation(e.Kandang),
|
||||||
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
|
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||||
@@ -129,16 +126,6 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
|
|||||||
return &mapped
|
return &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
func toNameWithPeriod(kandang entity.Kandang, period int) string {
|
|
||||||
if kandang.Name == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if period == 0 {
|
|
||||||
return kandang.Name
|
|
||||||
}
|
|
||||||
return kandang.Name + " Period " + strconv.Itoa(period)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toApprovalDTOSelector(
|
func toApprovalDTOSelector(
|
||||||
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
|
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
|
||||||
approval := selector(e)
|
approval := selector(e)
|
||||||
@@ -160,7 +147,6 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
|
|||||||
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
||||||
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||||
Kandang: toKandangRelation(e.Kandang),
|
Kandang: toKandangRelation(e.Kandang),
|
||||||
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
|
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||||
|
|||||||
+1
-22
@@ -2,7 +2,6 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math"
|
|
||||||
|
|
||||||
"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"
|
||||||
@@ -17,7 +16,6 @@ type ProjectFlockPopulationRepository interface {
|
|||||||
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error)
|
|
||||||
|
|
||||||
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
@@ -113,7 +111,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(c
|
|||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
Model(&entity.ProjectFlockPopulation{}).
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
Where("product_warehouse_id = ?", productWarehouseID).
|
Where("product_warehouse_id = ?", productWarehouseID).
|
||||||
Select("COALESCE(SUM(total_qty - total_used_qty), 0)").
|
Select("COALESCE(SUM(total_qty), 0)").
|
||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -137,22 +135,3 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand
|
|||||||
}
|
}
|
||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) {
|
|
||||||
var total float64
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("project_flock_populations").
|
|
||||||
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
|
||||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
|
||||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
|
||||||
Scan(&total).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if total < 0 {
|
|
||||||
total = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return int64(math.Round(total)), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
|||||||
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||||
|
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search"),
|
|
||||||
}
|
}
|
||||||
if projectFlockID > 0 {
|
if projectFlockID > 0 {
|
||||||
query.ProjectFlockKandangId = uint(projectFlockID)
|
query.ProjectFlockKandangId = uint(projectFlockID)
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type RecordingProjectFlockDTO struct {
|
type RecordingProjectFlockDTO struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
FlockName string `json:"flock_name"`
|
FlockName string `json:"flock_name"`
|
||||||
ProjectFlockCategory string `json:"project_flock_category"`
|
ProjectFlockCategory string `json:"project_flock_category"`
|
||||||
Period int `json:"period"`
|
Period int `json:"period"`
|
||||||
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
|
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
|
||||||
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
|
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
|
||||||
TotalChickQty float64 `json:"total_chick_qty"`
|
TotalChickQty float64 `json:"total_chick_qty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingProductionStandardDTO struct {
|
type RecordingProductionStandardDTO struct {
|
||||||
@@ -53,13 +53,6 @@ type RecordingLocationDTO struct {
|
|||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingKandangDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Capacity float64 `json:"capacity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RecordingWarehouseDTO struct {
|
type RecordingWarehouseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -89,14 +82,12 @@ type RecordingListDTO struct {
|
|||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Kandang *RecordingKandangDTO `json:"kandang,omitempty"`
|
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
||||||
Location *RecordingLocationDTO `json:"location,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingDetailDTO struct {
|
type RecordingDetailDTO struct {
|
||||||
RecordingListDTO
|
RecordingListDTO
|
||||||
ProductCategory string `json:"product_category"`
|
ProductCategory string `json:"product_category"`
|
||||||
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
|
||||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||||
Stocks []RecordingStockDTO `json:"stocks"`
|
Stocks []RecordingStockDTO `json:"stocks"`
|
||||||
Eggs []RecordingEggDTO `json:"eggs"`
|
Eggs []RecordingEggDTO `json:"eggs"`
|
||||||
@@ -142,11 +133,10 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
|||||||
|
|
||||||
return RecordingDetailDTO{
|
return RecordingDetailDTO{
|
||||||
RecordingListDTO: listDTO,
|
RecordingListDTO: listDTO,
|
||||||
ProductCategory: recordingProductCategory(e),
|
ProductCategory: recordingProductCategory(e),
|
||||||
Warehouse: recordingWarehouseDTO(e),
|
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +202,7 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO {
|
|||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
Kandang: recordingKandangDTO(e),
|
Warehouse: recordingWarehouseDTO(e),
|
||||||
Location: recordingKandangLocationDTO(e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,20 +214,20 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return RecordingRelationDTO{
|
return RecordingRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlock: toRecordingProjectFlockDTO(e),
|
ProjectFlock: toRecordingProjectFlockDTO(e),
|
||||||
RecordDatetime: e.RecordDatetime,
|
RecordDatetime: e.RecordDatetime,
|
||||||
Day: intValue(e.Day),
|
Day: intValue(e.Day),
|
||||||
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
||||||
CumDepletionRate: floatValue(e.CumDepletionRate),
|
CumDepletionRate: floatValue(e.CumDepletionRate),
|
||||||
CumIntake: intValue(e.CumIntake),
|
CumIntake: intValue(e.CumIntake),
|
||||||
FcrValue: floatValue(e.FcrValue),
|
FcrValue: floatValue(e.FcrValue),
|
||||||
HenDay: floatValue(e.HenDay),
|
HenDay: floatValue(e.HenDay),
|
||||||
HenHouse: floatValue(e.HenHouse),
|
HenHouse: floatValue(e.HenHouse),
|
||||||
FeedIntake: floatValue(e.FeedIntake),
|
FeedIntake: floatValue(e.FeedIntake),
|
||||||
EggMass: floatValue(e.EggMass),
|
EggMass: floatValue(e.EggMass),
|
||||||
EggWeight: floatValue(e.EggWeight),
|
EggWeight: floatValue(e.EggWeight),
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,34 +321,6 @@ func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
|
|||||||
return mapWarehouseDTO(&pw.Warehouse)
|
return mapWarehouseDTO(&pw.Warehouse)
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordingKandangDTO(e entity.Recording) *RecordingKandangDTO {
|
|
||||||
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
kandang := e.ProjectFlockKandang.Kandang
|
|
||||||
return &RecordingKandangDTO{
|
|
||||||
Id: kandang.Id,
|
|
||||||
Name: kandang.Name,
|
|
||||||
Status: kandang.Status,
|
|
||||||
Capacity: kandang.Capacity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func recordingKandangLocationDTO(e entity.Recording) *RecordingLocationDTO {
|
|
||||||
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
location := e.ProjectFlockKandang.Kandang.Location
|
|
||||||
if location.Id == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &RecordingLocationDTO{
|
|
||||||
Id: location.Id,
|
|
||||||
Name: location.Name,
|
|
||||||
Address: location.Address,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
|
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
|
||||||
if len(e.Stocks) > 0 {
|
if len(e.Stocks) > 0 {
|
||||||
pw := e.Stocks[0].ProductWarehouse
|
pw := e.Stocks[0].ProductWarehouse
|
||||||
|
|||||||
@@ -74,28 +74,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
|
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
|
||||||
Key: fifo.UsableKeyRecordingDepletion,
|
|
||||||
Table: "recording_depletions",
|
|
||||||
Columns: fifo.UsableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "source_product_warehouse_id",
|
|
||||||
UsageQuantity: "qty",
|
|
||||||
PendingQuantity: "pending_qty",
|
|
||||||
CreatedAt: "id",
|
|
||||||
},
|
|
||||||
ExcludedStockables: []fifo.StockableKey{
|
|
||||||
fifo.StockableKeyTransferToLayingIn,
|
|
||||||
fifo.StockableKeyStockTransferIn,
|
|
||||||
fifo.StockableKeyAdjustmentIn,
|
|
||||||
fifo.StockableKeyPurchaseItems,
|
|
||||||
fifo.StockableKeyRecordingEgg,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
|
||||||
panic(fmt.Sprintf("failed to register recording depletion usable workflow: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ type RecordingRepository interface {
|
|||||||
repository.BaseRepository[entity.Recording]
|
repository.BaseRepository[entity.Recording]
|
||||||
|
|
||||||
WithRelations(db *gorm.DB) *gorm.DB
|
WithRelations(db *gorm.DB) *gorm.DB
|
||||||
ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB
|
|
||||||
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
||||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ type RecordingRepository interface {
|
|||||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||||
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
||||||
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
|
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
|
||||||
UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error
|
|
||||||
|
|
||||||
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
||||||
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||||
@@ -46,7 +44,6 @@ type RecordingRepository interface {
|
|||||||
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
|
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
|
||||||
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
|
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
|
||||||
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
||||||
GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
|
|
||||||
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
|
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
|
||||||
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
||||||
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
||||||
@@ -86,7 +83,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
Preload("ProjectFlockKandang").
|
Preload("ProjectFlockKandang").
|
||||||
Preload("ProjectFlockKandang.Kandang").
|
Preload("ProjectFlockKandang.Kandang").
|
||||||
Preload("ProjectFlockKandang.Kandang.Location").
|
|
||||||
Preload("ProjectFlockKandang.ProjectFlock").
|
Preload("ProjectFlockKandang.ProjectFlock").
|
||||||
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
|
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
|
||||||
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||||
@@ -110,42 +106,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Eggs.ProductWarehouse.Warehouse.Location")
|
Preload("Eggs.ProductWarehouse.Warehouse.Location")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
|
|
||||||
if normalized == "" {
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
likeQuery := "%" + normalized + "%"
|
|
||||||
subQuery := db.Session(&gorm.Session{NewDB: true}).
|
|
||||||
Table("recordings").
|
|
||||||
Select("recordings.id").
|
|
||||||
Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id").
|
|
||||||
Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
|
||||||
Joins("LEFT JOIN kandangs k ON k.id = pfk.kandang_id").
|
|
||||||
Joins("LEFT JOIN locations l ON l.id = k.location_id").
|
|
||||||
Joins("LEFT JOIN recording_stocks rs ON rs.recording_id = recordings.id").
|
|
||||||
Joins("LEFT JOIN recording_depletions rd ON rd.recording_id = recordings.id").
|
|
||||||
Joins("LEFT JOIN recording_eggs re ON re.recording_id = recordings.id").
|
|
||||||
Joins("LEFT JOIN product_warehouses pws ON pws.id = rs.product_warehouse_id").
|
|
||||||
Joins("LEFT JOIN product_warehouses pwd ON pwd.id = rd.product_warehouse_id").
|
|
||||||
Joins("LEFT JOIN product_warehouses pwe ON pwe.id = re.product_warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses ws ON ws.id = pws.warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses wd ON wd.id = pwd.warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses we ON we.id = pwe.warehouse_id").
|
|
||||||
Where(`
|
|
||||||
LOWER(pf.flock_name) LIKE ?
|
|
||||||
OR LOWER(k.name) LIKE ?
|
|
||||||
OR LOWER(l.name) LIKE ?
|
|
||||||
OR LOWER(l.address) LIKE ?
|
|
||||||
OR LOWER(ws.name) LIKE ?
|
|
||||||
OR LOWER(wd.name) LIKE ?
|
|
||||||
OR LOWER(we.name) LIKE ?`,
|
|
||||||
likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery,
|
|
||||||
)
|
|
||||||
return db.Where("recordings.id IN (?)", subQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
||||||
if projectFlockKandangId == 0 {
|
if projectFlockKandangId == 0 {
|
||||||
return nil, errors.New("project_flock_kandang_id is required")
|
return nil, errors.New("project_flock_kandang_id is required")
|
||||||
@@ -206,12 +166,6 @@ func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, us
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error {
|
|
||||||
return tx.Model(&entity.RecordingDepletion{}).
|
|
||||||
Where("id = ?", depletionID).
|
|
||||||
Update("pending_qty", pendingQty).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
||||||
if len(depletions) == 0 {
|
if len(depletions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -367,25 +321,38 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
var result struct {
|
var rows []struct {
|
||||||
TotalQty float64
|
TotalQty float64
|
||||||
|
UomName string
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.
|
if err := tx.
|
||||||
Table("recording_stocks").
|
Table("recording_stocks").
|
||||||
Select("COALESCE(SUM(COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0)), 0) AS total_qty").
|
Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
|
||||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
|
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
||||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||||
Where("recording_stocks.recording_id = ?", recordingID).
|
Where("recording_stocks.recording_id = ?", recordingID).
|
||||||
Scan(&result).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.TotalQty <= 0 {
|
var total float64
|
||||||
return 0, nil
|
for _, row := range rows {
|
||||||
|
if row.TotalQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(row.UomName) {
|
||||||
|
case "kilogram", "kg", "kilograms", "kilo":
|
||||||
|
total += row.TotalQty * 1000
|
||||||
|
case "gram", "g", "grams":
|
||||||
|
total += row.TotalQty
|
||||||
|
default:
|
||||||
|
total += row.TotalQty
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result.TotalQty * 1000, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
|
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
|
||||||
@@ -581,30 +548,3 @@ func nextRecordingDay(days []int) int {
|
|||||||
|
|
||||||
return len(normalized) + 1
|
return len(normalized) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTotalWeightProducedFromUniformityByProjectFlockID calculates total weight produced from uniformity data
|
|
||||||
// It takes the latest uniformity record per kandang and calculates: SUM(mean_weight * chick_qty_of_weight / 1000)
|
|
||||||
func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
TotalWeight float64
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("project_flock_kandang_uniformity").
|
|
||||||
Select("COALESCE(SUM((mean_up / 1.10) * chick_qty_of_weight / 1000), 0) as total_weight").
|
|
||||||
Joins("JOIN ("+
|
|
||||||
" SELECT pfku.project_flock_kandang_id, MAX(pfku.id) as latest_id "+
|
|
||||||
" FROM project_flock_kandang_uniformity pfku "+
|
|
||||||
" JOIN project_flock_kandangs pfk ON pfk.id = pfku.project_flock_kandang_id "+
|
|
||||||
" WHERE pfk.project_flock_id = ? "+
|
|
||||||
" GROUP BY pfku.project_flock_kandang_id "+
|
|
||||||
") latest ON latest.project_flock_kandang_id = project_flock_kandang_uniformity.project_flock_kandang_id "+
|
|
||||||
"AND project_flock_kandang_uniformity.id = latest.latest_id", projectFlockID).
|
|
||||||
Scan(&result).Error
|
|
||||||
|
|
||||||
return result.TotalWeight, err
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ type RecordingFIFOIntegrationService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
|
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
|
||||||
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
|
|
||||||
|
|
||||||
type recordingService struct {
|
type recordingService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
@@ -117,8 +116,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
if params.ProjectFlockKandangId != 0 {
|
if params.ProjectFlockKandangId != 0 {
|
||||||
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
|
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
|
||||||
}
|
}
|
||||||
db = s.Repository.ApplySearchFilters(db, params.Search)
|
return db.Order("record_datetime DESC").Order("created_at DESC")
|
||||||
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -211,6 +209,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
if !isLaying && len(req.Eggs) > 0 {
|
if !isLaying && len(req.Eggs) > 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||||
}
|
}
|
||||||
|
if isLaying && len(req.Eggs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
|
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -279,24 +280,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
|
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
|
||||||
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
|
|
||||||
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i := range mappedDepletions {
|
|
||||||
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
||||||
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if s.FifoSvc != nil {
|
|
||||||
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs)
|
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs)
|
||||||
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
|
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
|
||||||
@@ -310,7 +297,11 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
var warehouseDeltas map[uint]float64
|
var warehouseDeltas map[uint]float64
|
||||||
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)
|
if s.FifoSvc != nil {
|
||||||
|
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, nil)
|
||||||
|
} else {
|
||||||
|
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)
|
||||||
|
}
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -416,6 +407,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
if !isLaying && len(req.Eggs) > 0 {
|
if !isLaying && len(req.Eggs) > 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||||
}
|
}
|
||||||
|
if isLaying && len(req.Eggs) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges {
|
if hasStockChanges {
|
||||||
@@ -437,38 +431,17 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasDepletionChanges {
|
if hasDepletionChanges {
|
||||||
if s.FifoSvc != nil {
|
|
||||||
if err := s.releaseRecordingDepletions(ctx, tx, existingDepletions); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
|
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
|
||||||
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
|
|
||||||
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i := range mappedDepletions {
|
|
||||||
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
|
||||||
s.Log.Errorf("Failed to update depletions: %+v", err)
|
s.Log.Errorf("Failed to update depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.FifoSvc != nil {
|
|
||||||
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -674,11 +647,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
s.Log.Errorf("Failed to list depletions before delete: %+v", err)
|
s.Log.Errorf("Failed to list depletions before delete: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if s.FifoSvc != nil {
|
|
||||||
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oldEggs, err := s.Repository.ListEggs(tx, id)
|
oldEggs, err := s.Repository.ListEggs(tx, id)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -797,46 +765,6 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) consumeRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
|
||||||
if len(depletions) == 0 || s.FifoSvc == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, depletion := range depletions {
|
|
||||||
if depletion.Id == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceWarehouseID := uint(0)
|
|
||||||
if depletion.SourceProductWarehouseId != nil {
|
|
||||||
sourceWarehouseID = *depletion.SourceProductWarehouseId
|
|
||||||
}
|
|
||||||
if sourceWarehouseID == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
|
|
||||||
}
|
|
||||||
|
|
||||||
desired := depletion.Qty + depletion.PendingQty
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
|
||||||
UsableKey: recordingDepletionUsableKey,
|
|
||||||
UsableID: depletion.Id,
|
|
||||||
ProductWarehouseID: sourceWarehouseID,
|
|
||||||
Quantity: desired,
|
|
||||||
AllowPending: false,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
||||||
return s.consumeRecordingStocks(ctx, tx, stocks)
|
return s.consumeRecordingStocks(ctx, tx, stocks)
|
||||||
}
|
}
|
||||||
@@ -868,67 +796,10 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) releaseRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
|
||||||
if len(depletions) == 0 || s.FifoSvc == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, depletion := range depletions {
|
|
||||||
if depletion.Id == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceWarehouseID := uint(0)
|
|
||||||
if depletion.SourceProductWarehouseId != nil {
|
|
||||||
sourceWarehouseID = *depletion.SourceProductWarehouseId
|
|
||||||
}
|
|
||||||
if sourceWarehouseID == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
|
||||||
UsableKey: recordingDepletionUsableKey,
|
|
||||||
UsableID: depletion.Id,
|
|
||||||
Tx: tx,
|
|
||||||
}); err != nil {
|
|
||||||
s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
|
||||||
return s.releaseRecordingStocks(ctx, tx, stocks)
|
return s.releaseRecordingStocks(ctx, tx, stocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) {
|
|
||||||
if projectFlockKandangID == 0 {
|
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
|
||||||
}
|
|
||||||
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
|
|
||||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi")
|
|
||||||
}
|
|
||||||
for _, pop := range populations {
|
|
||||||
if pop.ProductWarehouseId > 0 && pop.TotalQty > 0 {
|
|
||||||
return pop.ProductWarehouseId, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, pop := range populations {
|
|
||||||
if pop.ProductWarehouseId > 0 {
|
|
||||||
return pop.ProductWarehouseId, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan")
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildWarehouseDeltas(
|
func buildWarehouseDeltas(
|
||||||
oldDepletions, newDepletions []entity.RecordingDepletion,
|
oldDepletions, newDepletions []entity.RecordingDepletion,
|
||||||
oldEggs, newEggs []entity.RecordingEgg,
|
oldEggs, newEggs []entity.RecordingEgg,
|
||||||
@@ -1070,8 +941,10 @@ func (s *recordingService) syncRecordingStocks(
|
|||||||
|
|
||||||
desired := item.Qty
|
desired := item.Qty
|
||||||
stock.UsageQty = &desired
|
stock.UsageQty = &desired
|
||||||
zero := 0.0
|
if item.PendingQty != nil {
|
||||||
stock.PendingQty = &zero
|
pending := *item.PendingQty
|
||||||
|
stock.PendingQty = &pending
|
||||||
|
}
|
||||||
stocksToConsume = append(stocksToConsume, stock)
|
stocksToConsume = append(stocksToConsume, stock)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,20 +990,43 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
|
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
|
||||||
|
hasPending := false
|
||||||
|
for _, item := range incoming {
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
hasPending = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
existingUsage := make(map[uint]float64)
|
existingUsage := make(map[uint]float64)
|
||||||
|
existingTotal := make(map[uint]float64)
|
||||||
for _, stock := range existing {
|
for _, stock := range existing {
|
||||||
var usage float64
|
var usage float64
|
||||||
|
var pending float64
|
||||||
if stock.UsageQty != nil {
|
if stock.UsageQty != nil {
|
||||||
usage = *stock.UsageQty
|
usage = *stock.UsageQty
|
||||||
}
|
}
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
existingUsage[stock.ProductWarehouseId] += usage
|
existingUsage[stock.ProductWarehouseId] += usage
|
||||||
|
existingTotal[stock.ProductWarehouseId] += usage + pending
|
||||||
}
|
}
|
||||||
|
|
||||||
incomingUsage := make(map[uint]float64)
|
incomingUsage := make(map[uint]float64)
|
||||||
|
incomingTotal := make(map[uint]float64)
|
||||||
for _, item := range incoming {
|
for _, item := range incoming {
|
||||||
|
var pending float64
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
pending = *item.PendingQty
|
||||||
|
}
|
||||||
incomingUsage[item.ProductWarehouseId] += item.Qty
|
incomingUsage[item.ProductWarehouseId] += item.Qty
|
||||||
|
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasPending {
|
||||||
|
return floatMapsMatch(existingTotal, incomingTotal)
|
||||||
|
}
|
||||||
return floatMapsMatch(existingUsage, incomingUsage)
|
return floatMapsMatch(existingUsage, incomingUsage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,7 +1224,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
|
|
||||||
var eggMass float64
|
var eggMass float64
|
||||||
if remainingChick > 0 && totalEggWeightGrams > 0 {
|
if remainingChick > 0 && totalEggWeightGrams > 0 {
|
||||||
eggMass = totalEggWeightGrams / remainingChick
|
eggMass = (totalEggWeightGrams / remainingChick) * 1000
|
||||||
updates["egg_mass"] = eggMass
|
updates["egg_mass"] = eggMass
|
||||||
recording.EggMass = &eggMass
|
recording.EggMass = &eggMass
|
||||||
} else {
|
} else {
|
||||||
@@ -1338,7 +1234,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
|
|
||||||
var eggWeight float64
|
var eggWeight float64
|
||||||
if totalEggQty > 0 && totalEggWeightGrams > 0 {
|
if totalEggQty > 0 && totalEggWeightGrams > 0 {
|
||||||
eggWeight = totalEggWeightGrams / totalEggQty
|
eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
|
||||||
updates["egg_weight"] = eggWeight
|
updates["egg_weight"] = eggWeight
|
||||||
recording.EggWeight = &eggWeight
|
recording.EggWeight = &eggWeight
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package validation
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
Stock struct {
|
Stock struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||||
|
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Depletion struct {
|
Depletion struct {
|
||||||
@@ -19,24 +20,23 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Stocks []Stock `json:"stocks" validate:"dive"`
|
Stocks []Stock `json:"stocks" validate:"dive"`
|
||||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
Depletions []Depletion `json:"depletions" validate:"dive"`
|
||||||
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Approve struct {
|
type Approve struct {
|
||||||
|
|||||||
+3
-6
@@ -25,12 +25,8 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer
|
|||||||
|
|
||||||
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
|
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
|
||||||
TransferDate: c.Query("transfer_date", ""),
|
|
||||||
FlockSource: uint(c.QueryInt("flock_source", 0)),
|
|
||||||
FlockDestination: uint(c.QueryInt("flock_destination", 0)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
@@ -183,6 +179,7 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
||||||
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -162,19 +162,9 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
|
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
|
||||||
// Tampilkan requested qty sebelum approve, consumed qty setelah approve
|
|
||||||
var displayQty float64
|
|
||||||
if source.UsageQty > 0 {
|
|
||||||
// Sudah di-approve dan di-consume, tampilkan actual consumed quantity
|
|
||||||
displayQty = source.UsageQty
|
|
||||||
} else {
|
|
||||||
// Belum di-approve, tampilkan requested quantity
|
|
||||||
displayQty = source.RequestedQty
|
|
||||||
}
|
|
||||||
|
|
||||||
return LayingTransferSourceDTO{
|
return LayingTransferSourceDTO{
|
||||||
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
||||||
Qty: displayQty,
|
Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
|
||||||
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
||||||
Note: source.Note,
|
Note: source.Note,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,31 +110,8 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
|||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
// Apply search and filters
|
|
||||||
if params.Search != "" {
|
|
||||||
searchPattern := "%" + params.Search + "%"
|
|
||||||
db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
|
|
||||||
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
|
|
||||||
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
|
|
||||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.TransferDate != "" {
|
|
||||||
db = db.Where("transfer_date::date = ?::date", params.TransferDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.FlockSource > 0 {
|
|
||||||
db = db.Where("from_project_flock_id = ?", params.FlockSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.FlockDestination > 0 {
|
|
||||||
db = db.Where("to_project_flock_id = ?", params.FlockDestination)
|
|
||||||
}
|
|
||||||
|
|
||||||
db = db.Order("created_at DESC")
|
|
||||||
|
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
db = db.Order("created_at DESC")
|
||||||
return db
|
return db
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -239,7 +216,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
|
|
||||||
for _, sourceDetail := range req.SourceKandangs {
|
for _, sourceDetail := range req.SourceKandangs {
|
||||||
if sourceDetail.Quantity <= 0 {
|
if sourceDetail.Quantity <= 0 {
|
||||||
continue
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
|
||||||
}
|
}
|
||||||
totalSourceQty += sourceDetail.Quantity
|
totalSourceQty += sourceDetail.Quantity
|
||||||
|
|
||||||
@@ -270,18 +247,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
|
|
||||||
for _, targetDetail := range req.TargetKandangs {
|
for _, targetDetail := range req.TargetKandangs {
|
||||||
if targetDetail.Quantity <= 0 {
|
if targetDetail.Quantity <= 0 {
|
||||||
continue
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
|
||||||
}
|
}
|
||||||
totalTargetQty += targetDetail.Quantity
|
totalTargetQty += targetDetail.Quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalSourceQty == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0")
|
|
||||||
}
|
|
||||||
if totalTargetQty == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalSourceQty != totalTargetQty {
|
if totalSourceQty != totalTargetQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
|
||||||
}
|
}
|
||||||
@@ -309,16 +279,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, sourceDetail := range req.SourceKandangs {
|
for _, sourceDetail := range req.SourceKandangs {
|
||||||
if sourceDetail.Quantity == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
||||||
|
|
||||||
source := entity.LayingTransferSource{
|
source := entity.LayingTransferSource{
|
||||||
LayingTransferId: createBody.Id,
|
LayingTransferId: createBody.Id,
|
||||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||||
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
|
|
||||||
UsageQty: 0,
|
UsageQty: 0,
|
||||||
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
||||||
ProductWarehouseId: &productWarehouseId,
|
ProductWarehouseId: &productWarehouseId,
|
||||||
@@ -330,9 +295,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, targetDetail := range req.TargetKandangs {
|
for _, targetDetail := range req.TargetKandangs {
|
||||||
if targetDetail.Quantity == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -501,9 +463,8 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
|||||||
source := entity.LayingTransferSource{
|
source := entity.LayingTransferSource{
|
||||||
LayingTransferId: id,
|
LayingTransferId: id,
|
||||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||||
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
|
|
||||||
UsageQty: 0,
|
UsageQty: 0,
|
||||||
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
PendingUsageQty: sourceDetail.Quantity,
|
||||||
ProductWarehouseId: &productWarehouseId,
|
ProductWarehouseId: &productWarehouseId,
|
||||||
}
|
}
|
||||||
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
|
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
|
||||||
@@ -739,7 +700,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
|
||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber)
|
||||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
||||||
StockableID: target.Id,
|
StockableID: target.Id,
|
||||||
@@ -853,15 +814,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
|
|||||||
|
|
||||||
kandangAvailableQty := make(map[uint]float64)
|
kandangAvailableQty := make(map[uint]float64)
|
||||||
for _, kandang := range kandangs {
|
for _, kandang := range kandangs {
|
||||||
// Gunakan fungsi repository yang sama dengan recording service
|
|
||||||
totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
|
totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err)
|
s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err)
|
||||||
kandangAvailableQty[kandang.Id] = 0
|
kandangAvailableQty[kandang.Id] = 0
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
kandangAvailableQty[kandang.Id] = totalAvailable
|
kandangAvailableQty[kandang.Id] = totalQty
|
||||||
}
|
}
|
||||||
|
|
||||||
return pf, kandangAvailableQty, nil
|
return pf, kandangAvailableQty, nil
|
||||||
|
|||||||
+4
-8
@@ -2,12 +2,12 @@ package validation
|
|||||||
|
|
||||||
type SourceKandangDetail struct {
|
type SourceKandangDetail struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TargetKandangDetail struct {
|
type TargetKandangDetail struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
@@ -29,12 +29,8 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty"`
|
|
||||||
TransferDate string `query:"transfer_date" validate:"omitempty"`
|
|
||||||
FlockSource uint `query:"flock_source" validate:"omitempty,number"`
|
|
||||||
FlockDestination uint `query:"flock_destination" validate:"omitempty,number"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Approve struct {
|
type Approve struct {
|
||||||
|
|||||||
@@ -345,52 +345,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil {
|
||||||
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
category := strings.TrimSpace(pfk.ProjectFlock.Category)
|
|
||||||
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
|
|
||||||
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
|
|
||||||
if strings.TrimSpace(standard.ProjectCategory) != "" {
|
|
||||||
category = standard.ProjectCategory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
weekBase := 1
|
|
||||||
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
|
|
||||||
weekBase = 18
|
|
||||||
}
|
|
||||||
if req.Week < weekBase {
|
|
||||||
if weekBase == 18 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
|
||||||
}
|
|
||||||
|
|
||||||
var latestWeek int
|
|
||||||
if err := s.Repository.DB().WithContext(c.Context()).
|
|
||||||
Model(&entity.ProjectFlockKandangUniformity{}).
|
|
||||||
Where("project_flock_kandang_id = ? AND deleted_at IS NULL", req.ProjectFlockKandangId).
|
|
||||||
Select("COALESCE(MAX(week), 0)").
|
|
||||||
Scan(&latestWeek).Error; err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence")
|
|
||||||
}
|
|
||||||
if latestWeek == 0 && req.Week != weekBase {
|
|
||||||
if weekBase == 18 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
|
||||||
}
|
|
||||||
if latestWeek > 0 && req.Week > latestWeek+1 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,35 +487,8 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
|||||||
if req.ProjectFlockKandangId != nil {
|
if req.ProjectFlockKandangId != nil {
|
||||||
targetPFKID = *req.ProjectFlockKandangId
|
targetPFKID = *req.ProjectFlockKandangId
|
||||||
}
|
}
|
||||||
if targetPFKID != 0 && targetWeek > 0 {
|
|
||||||
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetPFKID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
category := strings.TrimSpace(pfk.ProjectFlock.Category)
|
|
||||||
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
|
|
||||||
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
|
|
||||||
if strings.TrimSpace(standard.ProjectCategory) != "" {
|
|
||||||
category = standard.ProjectCategory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
weekBase := 1
|
|
||||||
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
|
|
||||||
weekBase = 18
|
|
||||||
}
|
|
||||||
if targetWeek < weekBase {
|
|
||||||
if weekBase == 18 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if targetDate != nil {
|
if targetDate != nil {
|
||||||
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek); err != nil {
|
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,7 +604,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
|||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int) error {
|
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error {
|
||||||
if projectFlockKandangID == 0 || week == 0 {
|
if projectFlockKandangID == 0 || week == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ type ExpenseReceivingPayload struct {
|
|||||||
TransportPerItem *float64
|
TransportPerItem *float64
|
||||||
ReceivedQty float64
|
ReceivedQty float64
|
||||||
ReceivedDate *time.Time
|
ReceivedDate *time.Time
|
||||||
VehicleNumber *string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type groupedItem struct {
|
type groupedItem struct {
|
||||||
@@ -167,21 +166,12 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[
|
|||||||
if actorID == 0 {
|
if actorID == 0 {
|
||||||
actorID = 1
|
actorID = 1
|
||||||
}
|
}
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||||
svc := commonSvc.NewApprovalService(approvalRepo)
|
action := entity.ApprovalActionUpdated
|
||||||
action := entity.ApprovalActionCreated
|
|
||||||
|
|
||||||
for id := range expenseIDs {
|
for id := range expenseIDs {
|
||||||
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if latestApproval == nil {
|
|
||||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -192,22 +182,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
filtered := make([]ExpenseReceivingPayload, 0, len(updates))
|
|
||||||
for _, upd := range updates {
|
|
||||||
if upd.SupplierID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if upd.TransportPerItem == nil || *upd.TransportPerItem <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if upd.VehicleNumber == nil || strings.TrimSpace(*upd.VehicleNumber) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, upd)
|
|
||||||
}
|
|
||||||
if len(filtered) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load current links to decide whether to update in place or recreate.
|
// Load current links to decide whether to update in place or recreate.
|
||||||
type itemLink struct {
|
type itemLink struct {
|
||||||
@@ -231,9 +205,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
|
|
||||||
itemLinks := make(map[uint]itemLink)
|
itemLinks := make(map[uint]itemLink)
|
||||||
updatedExpenses := make(map[uint64]struct{})
|
updatedExpenses := make(map[uint64]struct{})
|
||||||
if len(filtered) > 0 {
|
if len(updates) > 0 {
|
||||||
ids := make([]uint, 0, len(filtered))
|
ids := make([]uint, 0, len(updates))
|
||||||
for _, upd := range filtered {
|
for _, upd := range updates {
|
||||||
if upd.PurchaseItemID != 0 {
|
if upd.PurchaseItemID != 0 {
|
||||||
ids = append(ids, upd.PurchaseItemID)
|
ids = append(ids, upd.PurchaseItemID)
|
||||||
}
|
}
|
||||||
@@ -278,7 +252,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
|
|
||||||
groups := make(map[string][]groupedItem)
|
groups := make(map[string][]groupedItem)
|
||||||
|
|
||||||
for _, payload := range filtered {
|
for _, payload := range updates {
|
||||||
if payload.ReceivedDate == nil {
|
if payload.ReceivedDate == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -702,7 +702,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
warehouseID uint
|
warehouseID uint
|
||||||
supplierID uint
|
supplierID uint
|
||||||
transportPerItem *float64
|
transportPerItem *float64
|
||||||
vehicleNumber *string
|
|
||||||
overrideWarehouse bool
|
overrideWarehouse bool
|
||||||
receivedQty float64
|
receivedQty float64
|
||||||
}
|
}
|
||||||
@@ -757,7 +756,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
}
|
}
|
||||||
visitedItems[payload.PurchaseItemID] = struct{}{}
|
visitedItems[payload.PurchaseItemID] = struct{}{}
|
||||||
|
|
||||||
var supplierID uint
|
supplierID := purchase.SupplierId
|
||||||
if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 {
|
if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 {
|
||||||
supplierID = *payload.ExpeditionVendorID
|
supplierID = *payload.ExpeditionVendorID
|
||||||
}
|
}
|
||||||
@@ -771,15 +770,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
transportPerItem = &val
|
transportPerItem = &val
|
||||||
}
|
}
|
||||||
|
|
||||||
var vehicleNumber *string
|
|
||||||
if payload.VehicleNumber != nil && strings.TrimSpace(*payload.VehicleNumber) != "" {
|
|
||||||
val := strings.TrimSpace(*payload.VehicleNumber)
|
|
||||||
vehicleNumber = &val
|
|
||||||
} else if item.VehicleNumber != nil && strings.TrimSpace(*item.VehicleNumber) != "" {
|
|
||||||
val := strings.TrimSpace(*item.VehicleNumber)
|
|
||||||
vehicleNumber = &val
|
|
||||||
}
|
|
||||||
|
|
||||||
prepared = append(prepared, preparedReceiving{
|
prepared = append(prepared, preparedReceiving{
|
||||||
item: item,
|
item: item,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
@@ -787,7 +777,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
warehouseID: warehouseID,
|
warehouseID: warehouseID,
|
||||||
supplierID: supplierID,
|
supplierID: supplierID,
|
||||||
transportPerItem: transportPerItem,
|
transportPerItem: transportPerItem,
|
||||||
vehicleNumber: vehicleNumber,
|
|
||||||
overrideWarehouse: overrideWarehouse,
|
overrideWarehouse: overrideWarehouse,
|
||||||
receivedQty: receivedQty,
|
receivedQty: receivedQty,
|
||||||
})
|
})
|
||||||
@@ -975,7 +964,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
TransportPerItem: prep.transportPerItem,
|
TransportPerItem: prep.transportPerItem,
|
||||||
ReceivedQty: prep.receivedQty,
|
ReceivedQty: prep.receivedQty,
|
||||||
ReceivedDate: &date,
|
ReceivedDate: &date,
|
||||||
VehicleNumber: prep.vehicleNumber,
|
|
||||||
}
|
}
|
||||||
receivingPayloads = append(receivingPayloads, payload)
|
receivingPayloads = append(receivingPayloads, payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,6 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
|
|||||||
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
||||||
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
||||||
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
||||||
AreaId: int64(ctx.QueryInt("area_id", 0)),
|
|
||||||
LocationId: int64(ctx.QueryInt("location_id", 0)),
|
|
||||||
MarketingType: ctx.Query("marketing_type", ""),
|
MarketingType: ctx.Query("marketing_type", ""),
|
||||||
FilterBy: ctx.Query("filter_by", ""),
|
FilterBy: ctx.Query("filter_by", ""),
|
||||||
StartDate: ctx.Query("start_date", ""),
|
StartDate: ctx.Query("start_date", ""),
|
||||||
@@ -244,65 +242,6 @@ 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) GetCustomerPayment(ctx *fiber.Ctx) error {
|
|
||||||
var customerIDs []uint
|
|
||||||
if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" {
|
|
||||||
ids := strings.Split(customerIDsStr, ",")
|
|
||||||
for _, idStr := range ids {
|
|
||||||
idStr = strings.TrimSpace(idStr)
|
|
||||||
if idStr != "" {
|
|
||||||
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
|
|
||||||
customerIDs = append(customerIDs, uint(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &validation.CustomerPaymentQuery{
|
|
||||||
Page: ctx.QueryInt("page", 1),
|
|
||||||
Limit: ctx.QueryInt("limit", 10),
|
|
||||||
CustomerIDs: customerIDs,
|
|
||||||
StartDate: ctx.Query("start_date", ""),
|
|
||||||
EndDate: ctx.Query("end_date", ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate pagination
|
|
||||||
if len(customerIDs) == 0 && (query.Page < 1 || query.Limit < 1) {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0 when customer_ids is not provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If single customer mode (only 1 customer ID), return without pagination
|
|
||||||
if len(customerIDs) == 1 {
|
|
||||||
return ctx.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get customer payment report successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiple customers mode with pagination
|
|
||||||
return ctx.Status(fiber.StatusOK).
|
|
||||||
JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get customer payment report successfully",
|
|
||||||
Meta: response.Meta{
|
|
||||||
Page: query.Page,
|
|
||||||
Limit: query.Limit,
|
|
||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
|
||||||
TotalResults: totalResults,
|
|
||||||
},
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
|
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
|
||||||
idParam := ctx.Params("idProjectFlockKandang")
|
idParam := ctx.Params("idProjectFlockKandang")
|
||||||
if idParam == "" {
|
if idParam == "" {
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
|
||||||
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CustomerPaymentReportRow struct {
|
|
||||||
TransactionType string `json:"transaction_type"`
|
|
||||||
TransactionID int64 `json:"transaction_id"`
|
|
||||||
TransDate time.Time `json:"trans_date"`
|
|
||||||
DeliveryDate *time.Time `json:"delivery_date"`
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
|
|
||||||
Qty float64 `json:"qty"`
|
|
||||||
Weight float64 `json:"weight"`
|
|
||||||
AverageWeight float64 `json:"average_weight"`
|
|
||||||
UnitPrice float64 `json:"unit_price"`
|
|
||||||
FinalPrice float64 `json:"final_price"`
|
|
||||||
TotalPrice float64 `json:"total_price"`
|
|
||||||
PaymentAmount float64 `json:"payment_amount"`
|
|
||||||
AccountsReceivable float64 `json:"accounts_receivable"`
|
|
||||||
AgingDay *int `json:"aging_day"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
VehicleNumbers []string `json:"vehicle_numbers"`
|
|
||||||
PickupInfo []string `json:"pickup_info"`
|
|
||||||
SalesPerson string `json:"sales_person"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomerPaymentReportSummary struct {
|
|
||||||
TotalQty float64 `json:"total_qty"`
|
|
||||||
TotalWeight float64 `json:"total_weight"`
|
|
||||||
TotalFinalAmount float64 `json:"total_final_amount"`
|
|
||||||
TotalGrandAmount float64 `json:"total_grand_amount"`
|
|
||||||
TotalPayment float64 `json:"total_payment"`
|
|
||||||
TotalAccountsReceivable float64 `json:"total_accounts_receivable"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomerPaymentReportItem struct {
|
|
||||||
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
|
||||||
InitialBalance float64 `json:"initial_balance"`
|
|
||||||
Rows []CustomerPaymentReportRow `json:"rows"`
|
|
||||||
Summary CustomerPaymentReportSummary `json:"summary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomerPaymentReportResponse struct {
|
|
||||||
Data []CustomerPaymentReportItem `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) CustomerPaymentReportRow {
|
|
||||||
return CustomerPaymentReportRow{
|
|
||||||
TransactionType: tx.TransactionType,
|
|
||||||
TransactionID: tx.TransactionID,
|
|
||||||
TransDate: tx.TransDate,
|
|
||||||
DeliveryDate: tx.DeliveryDate,
|
|
||||||
Reference: tx.Reference,
|
|
||||||
Qty: tx.Qty,
|
|
||||||
Weight: tx.Weight,
|
|
||||||
AverageWeight: tx.AverageWeight,
|
|
||||||
UnitPrice: tx.Price,
|
|
||||||
FinalPrice: tx.FinalPrice,
|
|
||||||
TotalPrice: tx.TotalPrice,
|
|
||||||
PaymentAmount: tx.PaymentAmount,
|
|
||||||
VehicleNumbers: parseStringSlice(tx.VehicleNumbers),
|
|
||||||
PickupInfo: parseStringSlice(tx.PickupInfo),
|
|
||||||
SalesPerson: tx.SalesPerson,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToCustomerPaymentReportItem(customer entities.Customer, initialBalance float64, rows []CustomerPaymentReportRow, summary CustomerPaymentReportSummary) CustomerPaymentReportItem {
|
|
||||||
return CustomerPaymentReportItem{
|
|
||||||
Customer: customerDTO.ToCustomerRelationDTO(customer),
|
|
||||||
InitialBalance: initialBalance,
|
|
||||||
Rows: rows,
|
|
||||||
Summary: summary,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToCustomerPaymentReportSummary(rows []CustomerPaymentReportRow, initialBalance float64) CustomerPaymentReportSummary {
|
|
||||||
summary := CustomerPaymentReportSummary{}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
summary.TotalQty += row.Qty
|
|
||||||
summary.TotalWeight += row.Weight
|
|
||||||
|
|
||||||
if row.TransactionType == "SALES" {
|
|
||||||
summary.TotalFinalAmount += row.FinalPrice
|
|
||||||
summary.TotalGrandAmount += row.TotalPrice
|
|
||||||
} else if row.TransactionType == "PAYMENT" {
|
|
||||||
summary.TotalPayment += row.PaymentAmount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Total AR = Initial Balance - Total Sales + Total Payment
|
|
||||||
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
|
|
||||||
|
|
||||||
return summary
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseStringSlice(str string) []string {
|
|
||||||
str = strings.TrimSpace(str)
|
|
||||||
if str == "" || str == "-" {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(str, ",")
|
|
||||||
result := make([]string, 0, len(parts))
|
|
||||||
for _, part := range parts {
|
|
||||||
part = strings.TrimSpace(part)
|
|
||||||
if part != "" {
|
|
||||||
result = append(result, part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -25,15 +25,14 @@ type HppPerKandangResponseData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangRowDTO struct {
|
type HppPerKandangRowDTO struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
|
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
|
||||||
NameWithPeriode string `json:"name_with_periode"`
|
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
||||||
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
|
||||||
AvgWeightKg float64 `json:"avg_weight_kg"`
|
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
|
||||||
EggProductionPieces int64 `json:"egg_production_pieces"`
|
AvgWeightKg float64 `json:"avg_weight_kg"`
|
||||||
EggProductionKg float64 `json:"egg_production_kg"`
|
EggProductionPieces int64 `json:"egg_production_pieces"`
|
||||||
// EggProductionTotalWeightKg float64 `json:"egg_production_total_weight_kg"`
|
EggProductionKg float64 `json:"egg_production_kg"`
|
||||||
// EggProductionTotalPieces int64 `json:"egg_production_total_pieces"`
|
|
||||||
// FeedCostRp float64 `json:"feed_cost_rp"`
|
// FeedCostRp float64 `json:"feed_cost_rp"`
|
||||||
// OvkCostRp float64 `json:"ovk_cost_rp"`
|
// OvkCostRp float64 `json:"ovk_cost_rp"`
|
||||||
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
||||||
@@ -41,8 +40,8 @@ type HppPerKandangRowDTO struct {
|
|||||||
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
||||||
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
||||||
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
|
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
|
||||||
// HppRp float64 `json:"hpp_rp"`
|
HppRp float64 `json:"hpp_rp"`
|
||||||
// RemainingValueRp int64 `json:"remaining_value_rp"`
|
RemainingValueRp int64 `json:"remaining_value_rp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangRowKandangDTO struct {
|
type HppPerKandangRowKandangDTO struct {
|
||||||
@@ -81,28 +80,34 @@ type HppPerKandangSummaryDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangSummaryWeightRangeDTO struct {
|
type HppPerKandangSummaryWeightRangeDTO struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
AvgWeightKg float64 `json:"avg_weight_kg"`
|
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
|
||||||
EggProductionPieces int64 `json:"egg_production_pieces"`
|
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
|
||||||
EggProductionKg float64 `json:"egg_production_kg"`
|
AvgWeightKg float64 `json:"avg_weight_kg"`
|
||||||
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
EggProductionPieces int64 `json:"egg_production_pieces"`
|
||||||
EggValueRp int64 `json:"egg_value_rp"`
|
EggProductionKg float64 `json:"egg_production_kg"`
|
||||||
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
||||||
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
EggValueRp int64 `json:"egg_value_rp"`
|
||||||
AverageDocPriceRp float64 `json:"average_doc_price_rp"`
|
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
||||||
HppRp float64 `json:"hpp_rp"`
|
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
||||||
RemainingValueRp int64 `json:"remaining_value_rp"`
|
AverageDocPriceRp float64 `json:"average_doc_price_rp"`
|
||||||
|
HppRp float64 `json:"hpp_rp"`
|
||||||
|
RemainingValueRp int64 `json:"remaining_value_rp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangSummaryTotalDTO struct {
|
type HppPerKandangSummaryTotalDTO struct {
|
||||||
AverageWeightKg float64 `json:"average_weight_kg"`
|
TotalRemainingChickenBirds int64 `json:"total_remaining_chicken_birds"`
|
||||||
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
|
TotalRemainingChickenWeightKg float64 `json:"total_remaining_chicken_weight_kg"`
|
||||||
TotalEggProductionKg float64 `json:"total_egg_production_kg"`
|
AverageWeightKg float64 `json:"average_weight_kg"`
|
||||||
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"`
|
TotalRemainingValueRp int64 `json:"total_remaining_value_rp"`
|
||||||
TotalEggValueRp int64 `json:"total_egg_value_rp"`
|
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
|
||||||
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"`
|
TotalEggProductionKg float64 `json:"total_egg_production_kg"`
|
||||||
|
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"`
|
||||||
|
TotalEggValueRp int64 `json:"total_egg_value_rp"`
|
||||||
|
TotalHppRp float64 `json:"total_hpp_rp"`
|
||||||
|
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO {
|
func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO {
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ type PurchaseSupplierRowDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseSupplierSummaryDTO struct {
|
type PurchaseSupplierSummaryDTO struct {
|
||||||
TotalQty float64 `json:"total_qty"`
|
TotalQty float64 `json:"total_qty"`
|
||||||
TotalPurchaseValue float64 `json:"total_purchase_value"`
|
TotalPurchaseValue float64 `json:"total_purchase_value"`
|
||||||
TotalTransportValue float64 `json:"total_transport_value"`
|
TotalTransportValue float64 `json:"total_transport_value"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
TotalUnitPrice float64 `json:"total_unit_price"`
|
TotalUnitPrice float64 `json:"total_unit_price"`
|
||||||
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
|
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseSupplierDTO struct {
|
type PurchaseSupplierDTO struct {
|
||||||
@@ -122,6 +122,11 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
|
|||||||
rows := make([]PurchaseSupplierRowDTO, 0, len(items))
|
rows := make([]PurchaseSupplierRowDTO, 0, len(items))
|
||||||
summary := PurchaseSupplierSummaryDTO{}
|
summary := PurchaseSupplierSummaryDTO{}
|
||||||
|
|
||||||
|
var unitPriceSum float64
|
||||||
|
var unitPriceCount int
|
||||||
|
var transportUnitPriceSum float64
|
||||||
|
var transportUnitPriceCount int
|
||||||
|
|
||||||
for i := range items {
|
for i := range items {
|
||||||
row := ToPurchaseSupplierRowDTO(&items[i])
|
row := ToPurchaseSupplierRowDTO(&items[i])
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
@@ -131,16 +136,19 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
|
|||||||
summary.TotalTransportValue += row.TransportValue
|
summary.TotalTransportValue += row.TransportValue
|
||||||
summary.TotalAmount += row.TotalAmount
|
summary.TotalAmount += row.TotalAmount
|
||||||
|
|
||||||
|
unitPriceSum += row.UnitPrice
|
||||||
|
unitPriceCount++
|
||||||
|
|
||||||
|
transportUnitPriceSum += row.TransportUnitPrice
|
||||||
|
transportUnitPriceCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
if summary.TotalQty > 0 {
|
if unitPriceCount > 0 {
|
||||||
avg := summary.TotalPurchaseValue / summary.TotalQty
|
summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount))
|
||||||
summary.TotalUnitPrice = math.Round(avg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if summary.TotalQty > 0 {
|
if transportUnitPriceCount > 0 {
|
||||||
avg := summary.TotalTransportValue / summary.TotalQty
|
summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount))
|
||||||
summary.TotalTransportUnitPrice = math.Round(avg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PurchaseSupplierDTO{
|
return PurchaseSupplierDTO{
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
|
||||||
productionStandardRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
productionStandardRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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"
|
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
@@ -36,14 +35,26 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
|
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
|
||||||
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
|
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
|
||||||
productionResultRepository := repportRepo.NewProductionResultRepository(db)
|
productionResultRepository := repportRepo.NewProductionResultRepository(db)
|
||||||
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
|
|
||||||
customerRepository := customerRepo.NewCustomerRepository(db)
|
|
||||||
standardGrowthDetailRepository := productionStandardRepo.NewStandardGrowthDetailRepository(db)
|
standardGrowthDetailRepository := productionStandardRepo.NewStandardGrowthDetailRepository(db)
|
||||||
productionStandardDetailRepository := productionStandardRepo.NewProductionStandardDetailRepository(db)
|
productionStandardDetailRepository := productionStandardRepo.NewProductionStandardDetailRepository(db)
|
||||||
userRepository := rUser.NewUserRepository(db)
|
userRepository := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
||||||
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository)
|
repportService := sRepport.NewRepportService(
|
||||||
|
validate,
|
||||||
|
expenseRealizationRepository,
|
||||||
|
marketingDeliveryProductRepository,
|
||||||
|
purchaseRepository,
|
||||||
|
chickinRepository,
|
||||||
|
recordingRepository,
|
||||||
|
approvalSvc,
|
||||||
|
purchaseSupplierRepository,
|
||||||
|
debtSupplierRepository,
|
||||||
|
hppPerKandangRepository,
|
||||||
|
productionResultRepository,
|
||||||
|
standardGrowthDetailRepository,
|
||||||
|
productionStandardDetailRepository,
|
||||||
|
)
|
||||||
userService := sUser.NewUserService(userRepository, validate)
|
userService := sUser.NewUserService(userRepository, validate)
|
||||||
|
|
||||||
RepportRoutes(router, userService, repportService)
|
RepportRoutes(router, userService, repportService)
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CustomerPaymentTransaction struct {
|
|
||||||
TransactionType string `gorm:"column:transaction_type"`
|
|
||||||
TransactionID int64 `gorm:"column:transaction_id"`
|
|
||||||
CustomerID int64 `gorm:"column:customer_id"`
|
|
||||||
TransDate time.Time `gorm:"column:trans_date"`
|
|
||||||
DeliveryDate *time.Time `gorm:"column:delivery_date"`
|
|
||||||
Reference string `gorm:"column:reference"`
|
|
||||||
VehicleNumbers string `gorm:"column:vehicle_numbers"`
|
|
||||||
Qty float64 `gorm:"column:qty"`
|
|
||||||
Weight float64 `gorm:"column:weight"`
|
|
||||||
AverageWeight float64 `gorm:"column:average_weight"`
|
|
||||||
Price float64 `gorm:"column:price"`
|
|
||||||
FinalPrice float64 `gorm:"column:final_price"`
|
|
||||||
TotalPrice float64 `gorm:"column:total_price"`
|
|
||||||
PaymentAmount float64 `gorm:"column:payment_amount"`
|
|
||||||
PickupInfo string `gorm:"column:pickup_info"`
|
|
||||||
SalesPerson string `gorm:"column:sales_person"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomerPaymentRepository interface {
|
|
||||||
GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error)
|
|
||||||
GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error)
|
|
||||||
GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type customerPaymentRepositoryImpl struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository {
|
|
||||||
return &customerPaymentRepositoryImpl{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) {
|
|
||||||
salesQuery := r.db.WithContext(ctx).
|
|
||||||
Table("marketing_delivery_products mdp").
|
|
||||||
Select(`
|
|
||||||
'SALES' AS transaction_type,
|
|
||||||
mdp.id::BIGINT AS transaction_id,
|
|
||||||
c.id::BIGINT AS customer_id,
|
|
||||||
m.so_date::DATE AS trans_date,
|
|
||||||
mdp.delivery_date::DATE AS delivery_date,
|
|
||||||
m.so_number || '-' || TO_CHAR(mdp.delivery_date, 'YYYYMMDD') || '-' || CAST(pw.warehouse_id AS VARCHAR) AS reference,
|
|
||||||
COALESCE(mdp.vehicle_number, '') AS vehicle_numbers,
|
|
||||||
|
|
||||||
COALESCE(mdp.usage_qty, 0)::NUMERIC(15,3) AS qty,
|
|
||||||
COALESCE(mdp.total_weight, 0)::NUMERIC(15,3) AS weight,
|
|
||||||
COALESCE(mdp.avg_weight, 0)::NUMERIC(15,3) AS average_weight,
|
|
||||||
COALESCE(mdp.unit_price, 0)::NUMERIC(15,3) AS price,
|
|
||||||
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS final_price,
|
|
||||||
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS total_price,
|
|
||||||
0::NUMERIC(15,3) AS payment_amount,
|
|
||||||
w.name AS pickup_info,
|
|
||||||
u.name AS sales_person
|
|
||||||
`).
|
|
||||||
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
|
||||||
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
|
|
||||||
Joins("INNER JOIN customers c ON c.id = m.customer_id").
|
|
||||||
Joins("INNER JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id").
|
|
||||||
Joins("INNER JOIN warehouses w ON w.id = pw.warehouse_id").
|
|
||||||
Joins("INNER JOIN users u ON u.id = m.sales_person_id").
|
|
||||||
Where("mdp.delivery_date IS NOT NULL").
|
|
||||||
Where("m.deleted_at IS NULL").
|
|
||||||
Where("c.deleted_at IS NULL")
|
|
||||||
|
|
||||||
if customerID != nil {
|
|
||||||
salesQuery = salesQuery.Where("c.id = ?", *customerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
paymentQuery := r.db.WithContext(ctx).
|
|
||||||
Table("payments p").
|
|
||||||
Select(`
|
|
||||||
'PAYMENT' AS transaction_type,
|
|
||||||
p.id::BIGINT AS transaction_id,
|
|
||||||
c.id::BIGINT AS customer_id,
|
|
||||||
p.payment_date::DATE AS trans_date,
|
|
||||||
NULL AS delivery_date,
|
|
||||||
COALESCE(p.reference_number, p.payment_code) AS reference,
|
|
||||||
'-' AS vehicle_numbers,
|
|
||||||
0::NUMERIC(15,3) AS qty,
|
|
||||||
0::NUMERIC(15,3) AS weight,
|
|
||||||
0::NUMERIC(15,3) AS average_weight,
|
|
||||||
0::NUMERIC(15,3) AS price,
|
|
||||||
0::NUMERIC(15,3) AS final_price,
|
|
||||||
0::NUMERIC(15,3) AS total_price,
|
|
||||||
p.nominal::NUMERIC(15,3) AS payment_amount,
|
|
||||||
'-' AS pickup_info,
|
|
||||||
'-' AS sales_person
|
|
||||||
`).
|
|
||||||
Joins("INNER JOIN customers c ON c.id = p.party_id").
|
|
||||||
Where("p.party_type = ?", "CUSTOMER").
|
|
||||||
Where("p.direction = ?", "IN").
|
|
||||||
Where("p.transaction_type = ?", "PENJUALAN").
|
|
||||||
Where("p.deleted_at IS NULL").
|
|
||||||
Where("c.deleted_at IS NULL")
|
|
||||||
|
|
||||||
if customerID != nil {
|
|
||||||
paymentQuery = paymentQuery.Where("c.id = ?", *customerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []CustomerPaymentTransaction
|
|
||||||
err := r.db.WithContext(ctx).
|
|
||||||
Raw("? UNION ALL ? ORDER BY customer_id, trans_date, transaction_type DESC, transaction_id",
|
|
||||||
salesQuery,
|
|
||||||
paymentQuery,
|
|
||||||
).
|
|
||||||
Scan(&results).
|
|
||||||
Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) {
|
|
||||||
var result struct {
|
|
||||||
Nominal float64
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).
|
|
||||||
Table("payments").
|
|
||||||
Select("COALESCE(SUM(nominal), 0) as nominal").
|
|
||||||
Where("party_type = ?", "CUSTOMER").
|
|
||||||
Where("party_id = ?", customerID).
|
|
||||||
Where("transaction_type = ?", "SALDO_AWAL").
|
|
||||||
Where("deleted_at IS NULL").
|
|
||||||
Scan(&result).
|
|
||||||
Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Nominal, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) {
|
|
||||||
subQuery := r.db.WithContext(ctx).
|
|
||||||
Table("(" +
|
|
||||||
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " +
|
|
||||||
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " +
|
|
||||||
"INNER JOIN marketings m ON m.id = mp.marketing_id " +
|
|
||||||
"INNER JOIN customers c ON c.id = m.customer_id " +
|
|
||||||
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL " +
|
|
||||||
"UNION " +
|
|
||||||
"SELECT DISTINCT c.id as customer_id FROM payments p " +
|
|
||||||
"INNER JOIN customers c ON c.id = p.party_id " +
|
|
||||||
"WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' " +
|
|
||||||
"AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" +
|
|
||||||
") as customer_ids")
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
if err := subQuery.Count(&total).Error; err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var customerIDs []uint
|
|
||||||
err := r.db.WithContext(ctx).
|
|
||||||
Table("("+
|
|
||||||
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+
|
|
||||||
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+
|
|
||||||
"INNER JOIN marketings m ON m.id = mp.marketing_id "+
|
|
||||||
"INNER JOIN customers c ON c.id = m.customer_id "+
|
|
||||||
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL "+
|
|
||||||
"UNION "+
|
|
||||||
"SELECT DISTINCT c.id as customer_id FROM payments p "+
|
|
||||||
"INNER JOIN customers c ON c.id = p.party_id "+
|
|
||||||
"WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' "+
|
|
||||||
"AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL"+
|
|
||||||
") as customer_ids").
|
|
||||||
Select("customer_id").
|
|
||||||
Order("customer_id ASC").
|
|
||||||
Limit(limit).
|
|
||||||
Offset(offset).
|
|
||||||
Pluck("customer_id", &customerIDs).
|
|
||||||
Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return customerIDs, total, nil
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||||
@@ -18,8 +17,6 @@ type DebtSupplierRepository interface {
|
|||||||
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
|
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
|
||||||
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
||||||
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
||||||
GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
|
|
||||||
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
|
|
||||||
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||||
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||||
}
|
}
|
||||||
@@ -28,11 +25,6 @@ type debtSupplierRepositoryImpl struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentReferenceSummary struct {
|
|
||||||
Total float64
|
|
||||||
LatestPaymentDate time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
||||||
return &debtSupplierRepositoryImpl{db: db}
|
return &debtSupplierRepositoryImpl{db: db}
|
||||||
}
|
}
|
||||||
@@ -175,8 +167,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context,
|
|||||||
Model(&entity.Payment{}).
|
Model(&entity.Payment{}).
|
||||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
Where("direction = ?", "OUT").
|
Where("direction = ?", "OUT").
|
||||||
Where("party_id IN ?", supplierIDs).
|
Where("party_id IN ?", supplierIDs)
|
||||||
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal))
|
|
||||||
|
|
||||||
if strings.TrimSpace(filters.StartDate) != "" {
|
if strings.TrimSpace(filters.StartDate) != "" {
|
||||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||||
@@ -247,7 +238,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
|||||||
Where("direction = ?", "OUT").
|
Where("direction = ?", "OUT").
|
||||||
Where("party_id IN ?", supplierIDs).
|
Where("party_id IN ?", supplierIDs).
|
||||||
Where("reference_number IN ?", references).
|
Where("reference_number IN ?", references).
|
||||||
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
|
|
||||||
Group("reference_number").
|
Group("reference_number").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -264,75 +254,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *debtSupplierRepositoryImpl) GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error) {
|
|
||||||
if len(supplierIDs) == 0 || len(references) == 0 {
|
|
||||||
return map[string]PaymentReferenceSummary{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type paymentRow struct {
|
|
||||||
ReferenceNumber *string `gorm:"column:reference_number"`
|
|
||||||
Total float64 `gorm:"column:total"`
|
|
||||||
LatestPaymentDate time.Time `gorm:"column:latest_payment_date"`
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]paymentRow, 0)
|
|
||||||
if err := r.db.WithContext(ctx).
|
|
||||||
Model(&entity.Payment{}).
|
|
||||||
Select("reference_number, SUM(nominal) AS total, MAX(payment_date) AS latest_payment_date").
|
|
||||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
|
||||||
Where("direction = ?", "OUT").
|
|
||||||
Where("party_id IN ?", supplierIDs).
|
|
||||||
Where("reference_number IN ?", references).
|
|
||||||
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
|
|
||||||
Group("reference_number").
|
|
||||||
Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[string]PaymentReferenceSummary, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[*row.ReferenceNumber] = PaymentReferenceSummary{
|
|
||||||
Total: row.Total,
|
|
||||||
LatestPaymentDate: row.LatestPaymentDate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *debtSupplierRepositoryImpl) GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error) {
|
|
||||||
if len(supplierIDs) == 0 {
|
|
||||||
return map[uint]float64{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type balanceRow struct {
|
|
||||||
SupplierID uint `gorm:"column:supplier_id"`
|
|
||||||
Total float64 `gorm:"column:total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]balanceRow, 0)
|
|
||||||
if err := r.db.WithContext(ctx).
|
|
||||||
Model(&entity.Payment{}).
|
|
||||||
Select("party_id AS supplier_id, SUM(nominal) AS total").
|
|
||||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
|
||||||
Where("party_id IN ?", supplierIDs).
|
|
||||||
Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)).
|
|
||||||
Group("party_id").
|
|
||||||
Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[uint]float64, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
result[row.SupplierID] = row.Total
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||||
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||||
return map[uint]float64{}, nil
|
return map[uint]float64{}, nil
|
||||||
@@ -392,7 +313,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
|
|||||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
Where("direction = ?", "OUT").
|
Where("direction = ?", "OUT").
|
||||||
Where("party_id IN ?", supplierIDs).
|
Where("party_id IN ?", supplierIDs).
|
||||||
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
|
|
||||||
Where("DATE(payment_date) < ?", dateFrom).
|
Where("DATE(payment_date) < ?", dateFrom).
|
||||||
Group("party_id").
|
Group("party_id").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
|
|||||||
@@ -11,46 +11,40 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HppPerKandangRow struct {
|
type HppPerKandangRow struct {
|
||||||
ProjectFlockKandangID uint
|
KandangID uint
|
||||||
ProjectFlockPeriod int
|
KandangName string
|
||||||
KandangID uint
|
KandangStatus string
|
||||||
KandangName string
|
LocationID uint
|
||||||
KandangStatus string
|
LocationName string
|
||||||
LocationID uint
|
PicID uint
|
||||||
LocationName string
|
PicName string
|
||||||
PicID uint
|
RemainingChickenBirds float64
|
||||||
PicName string
|
RemainingChickenWeight float64
|
||||||
RecordingCount int64
|
EggProductionWeightKg float64
|
||||||
// RemainingChickenBirds float64
|
EggProductionPieces float64
|
||||||
// RemainingChickenWeight float64
|
|
||||||
EggProductionWeightKgRemaining float64
|
|
||||||
EggProductionPiecesRemaining float64
|
|
||||||
EggProductionTotalWeightKg float64
|
|
||||||
EggProductionTotalPieces float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangCostRow struct {
|
type HppPerKandangCostRow struct {
|
||||||
ProjectFlockKandangID uint
|
KandangID uint
|
||||||
FeedCost float64
|
FeedCost float64
|
||||||
OvkCost float64
|
OvkCost float64
|
||||||
DocCost float64
|
DocCost float64
|
||||||
DocQty float64
|
DocQty float64
|
||||||
BudgetCost float64
|
BudgetCost float64
|
||||||
ExpenseCost float64
|
ExpenseCost float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangSupplierRow struct {
|
type HppPerKandangSupplierRow struct {
|
||||||
ProjectFlockKandangID uint
|
KandangID uint
|
||||||
SupplierID uint
|
SupplierID uint
|
||||||
SupplierName string
|
SupplierName string
|
||||||
SupplierAlias string
|
SupplierAlias string
|
||||||
Category string
|
Category string
|
||||||
}
|
}
|
||||||
|
|
||||||
type HppPerKandangRepository interface {
|
type HppPerKandangRepository interface {
|
||||||
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
|
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
|
||||||
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
|
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
|
||||||
GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type hppPerKandangRepository struct {
|
type hppPerKandangRepository struct {
|
||||||
@@ -64,32 +58,9 @@ func NewHppPerKandangRepository(db *gorm.DB) HppPerKandangRepository {
|
|||||||
func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) {
|
func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) {
|
||||||
var rows []HppPerKandangRow
|
var rows []HppPerKandangRow
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
validRecordings := r.db.WithContext(ctx).
|
|
||||||
Table("recordings AS r").
|
|
||||||
Select("r.id, r.project_flock_kandangs_id, r.total_chick_qty").
|
|
||||||
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
|
||||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
|
||||||
Where("r.deleted_at IS NULL").
|
|
||||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
|
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
query := r.db.WithContext(ctx).
|
||||||
Table("project_flocks AS pf").
|
Table("recordings AS r").
|
||||||
Select(`
|
Select(`
|
||||||
pfk.id AS project_flock_kandang_id,
|
|
||||||
pfk.period AS project_flock_period,
|
|
||||||
k.id AS kandang_id,
|
k.id AS kandang_id,
|
||||||
k.name AS kandang_name,
|
k.name AS kandang_name,
|
||||||
k.status AS kandang_status,
|
k.status AS kandang_status,
|
||||||
@@ -97,31 +68,23 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
|||||||
loc.name AS location_name,
|
loc.name AS location_name,
|
||||||
pic.id AS pic_id,
|
pic.id AS pic_id,
|
||||||
pic.name AS pic_name,
|
pic.name AS pic_name,
|
||||||
COALESCE(COUNT(vr.id), 0) AS recording_count,
|
COALESCE(MAX(r.total_chick_qty), 0) AS remaining_chicken_birds,
|
||||||
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds,
|
COALESCE(SUM(rbw.total_weight), 0) AS remaining_chicken_weight,
|
||||||
0 AS remaining_chicken_weight,
|
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
|
||||||
0 AS egg_production_weight_kg,
|
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
|
||||||
0 AS egg_production_pieces,
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
0 AS egg_production_total_weight_kg,
|
|
||||||
0 AS egg_production_total_pieces`).
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
|
||||||
Joins(`
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT project_flock_kandang_id, MIN(chick_in_date) AS chick_in_date
|
|
||||||
FROM project_chickins
|
|
||||||
GROUP BY project_flock_kandang_id
|
|
||||||
) AS pc ON pc.project_flock_kandang_id = pfk.id`).
|
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
Joins("JOIN users AS pic ON pic.id = k.pic_id").
|
Joins("JOIN users AS pic ON pic.id = k.pic_id").
|
||||||
Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings).
|
Joins("LEFT JOIN recording_bws AS rbw ON rbw.recording_id = r.id").
|
||||||
Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
|
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
|
||||||
Where("(pfk.closed_at IS NULL OR ? BETWEEN pc.chick_in_date AND pfk.closed_at)", start)
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL")
|
||||||
|
|
||||||
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
query = query.Group("pfk.id, pfk.period, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
query = query.Group("k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
||||||
Order("pfk.id ASC")
|
Order("k.id ASC")
|
||||||
|
|
||||||
if err := query.Scan(&rows).Error; err != nil {
|
if err := query.Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -130,44 +93,41 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
|||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
|
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
|
||||||
var rows []HppPerKandangCostRow
|
var rows []HppPerKandangCostRow
|
||||||
|
|
||||||
|
recordingPfk := r.db.WithContext(ctx).
|
||||||
|
Table("recordings AS r").
|
||||||
|
Select("DISTINCT pfk.id").
|
||||||
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
|
Where("r.deleted_at IS NULL")
|
||||||
|
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||||
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
query := r.db.WithContext(ctx).
|
||||||
Table("recordings AS r").
|
Table("recordings AS r").
|
||||||
Select(`
|
Select(`
|
||||||
pfk.id AS project_flock_kandang_id,
|
k.id AS kandang_id,
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
|
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS feed_cost,
|
END), 0) AS feed_cost,
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
|
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0) AS ovk_cost`,
|
END), 0) AS ovk_cost`,
|
||||||
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
|
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
|
||||||
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
|
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
||||||
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
|
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
|
||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
||||||
@@ -176,30 +136,31 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
|
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
|
||||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
||||||
Where("r.record_datetime < ?", end).
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
Where("r.deleted_at IS NULL").
|
Where("r.deleted_at IS NULL")
|
||||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
|
|
||||||
|
|
||||||
query = query.Group("pfk.id").Order("pfk.id ASC")
|
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
|
query = query.Group("k.id").Order("k.id ASC")
|
||||||
|
|
||||||
if err := query.Scan(&rows).Error; err != nil {
|
if err := query.Scan(&rows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
docRows := make([]struct {
|
docRows := make([]struct {
|
||||||
ProjectFlockKandangID uint
|
KandangID uint
|
||||||
DocCost float64
|
DocCost float64
|
||||||
DocQty float64
|
DocQty float64
|
||||||
SupplierID *uint
|
SupplierID *uint
|
||||||
SupplierName *string
|
SupplierName *string
|
||||||
SupplierAlias *string
|
SupplierAlias *string
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
docQuery := r.db.WithContext(ctx).
|
docQuery := r.db.WithContext(ctx).
|
||||||
Table("project_chickins AS pc").
|
Table("project_chickins AS pc").
|
||||||
Select(`
|
Select(`
|
||||||
pfk.id AS project_flock_kandang_id,
|
pfk.kandang_id AS kandang_id,
|
||||||
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
|
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
|
||||||
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
|
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
|
||||||
s.id AS supplier_id,
|
s.id AS supplier_id,
|
||||||
@@ -211,8 +172,9 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
|
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
|
||||||
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
||||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
||||||
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
Where("pc.project_flock_kandang_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
||||||
Group("pfk.id, s.id, s.name, s.alias")
|
Group("pfk.kandang_id, s.id, s.name, s.alias")
|
||||||
|
docQuery = applyLocationFilters(docQuery, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
if err := docQuery.Scan(&docRows).Error; err != nil {
|
if err := docQuery.Scan(&docRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -221,28 +183,28 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
|
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
|
||||||
for i := range rows {
|
for i := range rows {
|
||||||
row := rows[i]
|
row := rows[i]
|
||||||
costMap[row.ProjectFlockKandangID] = &rows[i]
|
costMap[row.KandangID] = &rows[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
docSuppliers := make([]HppPerKandangSupplierRow, 0)
|
docSuppliers := make([]HppPerKandangSupplierRow, 0)
|
||||||
docSeen := make(map[uint]map[uint]bool)
|
docSeen := make(map[uint]map[uint]bool)
|
||||||
for _, doc := range docRows {
|
for _, doc := range docRows {
|
||||||
entry, ok := costMap[doc.ProjectFlockKandangID]
|
entry, ok := costMap[doc.KandangID]
|
||||||
if !ok {
|
if !ok {
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
rows = append(rows, HppPerKandangCostRow{
|
||||||
ProjectFlockKandangID: doc.ProjectFlockKandangID,
|
KandangID: doc.KandangID,
|
||||||
})
|
})
|
||||||
entry = &rows[len(rows)-1]
|
entry = &rows[len(rows)-1]
|
||||||
costMap[doc.ProjectFlockKandangID] = entry
|
costMap[doc.KandangID] = entry
|
||||||
}
|
}
|
||||||
entry.DocCost += doc.DocCost
|
entry.DocCost += doc.DocCost
|
||||||
entry.DocQty += doc.DocQty
|
entry.DocQty += doc.DocQty
|
||||||
if doc.SupplierID != nil {
|
if doc.SupplierID != nil {
|
||||||
if docSeen[doc.ProjectFlockKandangID] == nil {
|
if docSeen[doc.KandangID] == nil {
|
||||||
docSeen[doc.ProjectFlockKandangID] = make(map[uint]bool)
|
docSeen[doc.KandangID] = make(map[uint]bool)
|
||||||
}
|
}
|
||||||
if !docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] {
|
if !docSeen[doc.KandangID][*doc.SupplierID] {
|
||||||
docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] = true
|
docSeen[doc.KandangID][*doc.SupplierID] = true
|
||||||
supplierName := ""
|
supplierName := ""
|
||||||
if doc.SupplierName != nil {
|
if doc.SupplierName != nil {
|
||||||
supplierName = *doc.SupplierName
|
supplierName = *doc.SupplierName
|
||||||
@@ -252,19 +214,19 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
supplierAlias = *doc.SupplierAlias
|
supplierAlias = *doc.SupplierAlias
|
||||||
}
|
}
|
||||||
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
|
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
|
||||||
ProjectFlockKandangID: doc.ProjectFlockKandangID,
|
KandangID: doc.KandangID,
|
||||||
SupplierID: *doc.SupplierID,
|
SupplierID: *doc.SupplierID,
|
||||||
SupplierName: supplierName,
|
SupplierName: supplierName,
|
||||||
SupplierAlias: supplierAlias,
|
SupplierAlias: supplierAlias,
|
||||||
Category: "DOC",
|
Category: "DOC",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
budgetRows := make([]struct {
|
budgetRows := make([]struct {
|
||||||
ProjectFlockKandangID uint
|
KandangID uint
|
||||||
BudgetCost float64
|
BudgetCost float64
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
pfkUsageSub := r.db.
|
pfkUsageSub := r.db.
|
||||||
@@ -285,63 +247,63 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
budgetQuery := r.db.WithContext(ctx).
|
budgetQuery := r.db.WithContext(ctx).
|
||||||
Table("project_flock_kandangs AS pfk").
|
Table("project_flock_kandangs AS pfk").
|
||||||
Select(`
|
Select(`
|
||||||
pfk.id AS project_flock_kandang_id,
|
k.id AS kandang_id,
|
||||||
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
|
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_id").
|
Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_id").
|
||||||
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
|
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
|
||||||
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
|
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
|
||||||
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
||||||
Group("pfk.id")
|
Group("k.id")
|
||||||
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
|
budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
|
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, budget := range budgetRows {
|
for _, budget := range budgetRows {
|
||||||
entry, ok := costMap[budget.ProjectFlockKandangID]
|
entry, ok := costMap[budget.KandangID]
|
||||||
if !ok {
|
if !ok {
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
rows = append(rows, HppPerKandangCostRow{
|
||||||
ProjectFlockKandangID: budget.ProjectFlockKandangID,
|
KandangID: budget.KandangID,
|
||||||
})
|
})
|
||||||
entry = &rows[len(rows)-1]
|
entry = &rows[len(rows)-1]
|
||||||
costMap[budget.ProjectFlockKandangID] = entry
|
costMap[budget.KandangID] = entry
|
||||||
}
|
}
|
||||||
entry.BudgetCost += budget.BudgetCost
|
entry.BudgetCost += budget.BudgetCost
|
||||||
}
|
}
|
||||||
|
|
||||||
expenseRows := make([]struct {
|
expenseRows := make([]struct {
|
||||||
ProjectFlockKandangID uint
|
KandangID uint
|
||||||
ExpenseCost float64
|
ExpenseCost float64
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
expenseQuery := r.db.WithContext(ctx).
|
expenseQuery := r.db.WithContext(ctx).
|
||||||
Table("project_flock_kandangs AS pfk").
|
Table("project_flock_kandangs AS pfk").
|
||||||
Select(`
|
Select(`
|
||||||
pfk.id AS project_flock_kandang_id,
|
k.id AS kandang_id,
|
||||||
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
|
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
|
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
|
||||||
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
|
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
|
||||||
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
||||||
Group("pfk.id")
|
Group("k.id")
|
||||||
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
|
expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
|
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, exp := range expenseRows {
|
for _, exp := range expenseRows {
|
||||||
entry, ok := costMap[exp.ProjectFlockKandangID]
|
entry, ok := costMap[exp.KandangID]
|
||||||
if !ok {
|
if !ok {
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
rows = append(rows, HppPerKandangCostRow{
|
||||||
ProjectFlockKandangID: exp.ProjectFlockKandangID,
|
KandangID: exp.KandangID,
|
||||||
})
|
})
|
||||||
entry = &rows[len(rows)-1]
|
entry = &rows[len(rows)-1]
|
||||||
costMap[exp.ProjectFlockKandangID] = entry
|
costMap[exp.KandangID] = entry
|
||||||
}
|
}
|
||||||
entry.ExpenseCost += exp.ExpenseCost
|
entry.ExpenseCost += exp.ExpenseCost
|
||||||
}
|
}
|
||||||
@@ -350,7 +312,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
|
|
||||||
feedQuery := r.db.WithContext(ctx).
|
feedQuery := r.db.WithContext(ctx).
|
||||||
Table("recordings AS r").
|
Table("recordings AS r").
|
||||||
Select("DISTINCT pfk.id AS project_flock_kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
|
Select("DISTINCT k.id AS kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||||
@@ -361,21 +323,21 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
||||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
|
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
|
||||||
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
|
||||||
Where("r.record_datetime < ?", end).
|
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||||
Where("r.deleted_at IS NULL")
|
Where("r.deleted_at IS NULL")
|
||||||
// feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
|
feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
if err := feedQuery.Scan(&feedSuppliers).Error; err != nil {
|
if err := feedQuery.Scan(&feedSuppliers).Error; err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range feedSuppliers {
|
for i := range feedSuppliers {
|
||||||
if _, exists := costMap[feedSuppliers[i].ProjectFlockKandangID]; !exists {
|
if _, exists := costMap[feedSuppliers[i].KandangID]; !exists {
|
||||||
rows = append(rows, HppPerKandangCostRow{
|
rows = append(rows, HppPerKandangCostRow{
|
||||||
ProjectFlockKandangID: feedSuppliers[i].ProjectFlockKandangID,
|
KandangID: feedSuppliers[i].KandangID,
|
||||||
})
|
})
|
||||||
costMap[feedSuppliers[i].ProjectFlockKandangID] = &rows[len(rows)-1]
|
costMap[feedSuppliers[i].KandangID] = &rows[len(rows)-1]
|
||||||
}
|
}
|
||||||
feedSuppliers[i].Category = "FEED"
|
feedSuppliers[i].Category = "FEED"
|
||||||
}
|
}
|
||||||
@@ -385,67 +347,6 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
return rows, supplierRows, nil
|
return rows, supplierRows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) {
|
|
||||||
if len(projectFlockKandangIDs) == 0 {
|
|
||||||
return map[uint]HppPerKandangRow{}, 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 {
|
|
||||||
ProjectFlockKandangID uint
|
|
||||||
EggProductionWeightKgRemaining float64
|
|
||||||
EggProductionPiecesRemaining float64
|
|
||||||
EggProductionTotalWeightKg float64
|
|
||||||
EggProductionTotalPieces float64
|
|
||||||
}
|
|
||||||
|
|
||||||
eggRows := make([]eggRow, 0)
|
|
||||||
query := r.db.WithContext(ctx).
|
|
||||||
Table("recordings AS r").
|
|
||||||
Select(`
|
|
||||||
r.project_flock_kandangs_id AS project_flock_kandang_id,
|
|
||||||
COALESCE(SUM((re.total_qty - re.total_used) * re.weight / 1000), 0) AS egg_production_weight_kg_remaining,
|
|
||||||
COALESCE(SUM(re.total_qty - re.total_used), 0) AS egg_production_pieces_remaining,
|
|
||||||
COALESCE(SUM(re.weight / 1000), 0) AS egg_production_total_weight_kg,
|
|
||||||
COALESCE(SUM(re.total_qty), 0) AS egg_production_total_pieces`).
|
|
||||||
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
|
||||||
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
|
|
||||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
|
||||||
Where("r.record_datetime < ?", end).
|
|
||||||
Where("r.deleted_at IS NULL").
|
|
||||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
|
||||||
Group("r.project_flock_kandangs_id")
|
|
||||||
|
|
||||||
if err := query.Scan(&eggRows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[uint]HppPerKandangRow, len(eggRows))
|
|
||||||
for _, row := range eggRows {
|
|
||||||
result[row.ProjectFlockKandangID] = HppPerKandangRow{
|
|
||||||
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
|
||||||
EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
|
|
||||||
EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
|
|
||||||
EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
|
|
||||||
EggProductionTotalPieces: row.EggProductionTotalPieces,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB {
|
func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB {
|
||||||
if len(areaIDs) > 0 {
|
if len(areaIDs) > 0 {
|
||||||
query = query.Where("loc.area_id IN ?", areaIDs)
|
query = query.Where("loc.area_id IN ?", areaIDs)
|
||||||
@@ -454,7 +355,7 @@ func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int
|
|||||||
query = query.Where("k.location_id IN ?", locationIDs)
|
query = query.Where("k.location_id IN ?", locationIDs)
|
||||||
}
|
}
|
||||||
if len(kandangIDs) > 0 {
|
if len(kandangIDs) > 0 {
|
||||||
query = query.Where("pfk.id IN ?", kandangIDs)
|
query = query.Where("k.id IN ?", kandangIDs)
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,21 +25,6 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository {
|
|||||||
return &purchaseSupplierRepositoryImpl{db: db}
|
return &purchaseSupplierRepositoryImpl{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *purchaseSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB {
|
|
||||||
return r.db.WithContext(ctx).
|
|
||||||
Table("approvals AS a").
|
|
||||||
Select("a.approvable_id, a.step_number, 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.ApprovalWorkflowPurchase),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB {
|
func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB {
|
||||||
dateColumn := "purchase_items.received_date"
|
dateColumn := "purchase_items.received_date"
|
||||||
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
|
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
|
||||||
@@ -49,16 +34,10 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context,
|
|||||||
dateColumn = "purchase_items.received_date"
|
dateColumn = "purchase_items.received_date"
|
||||||
}
|
}
|
||||||
|
|
||||||
latestApproval := r.latestPurchaseApproval(ctx)
|
|
||||||
|
|
||||||
db := r.db.WithContext(ctx).
|
db := r.db.WithContext(ctx).
|
||||||
Model(&entity.Supplier{}).
|
Model(&entity.Supplier{}).
|
||||||
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
|
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
|
||||||
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id")
|
||||||
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", latestApproval).
|
|
||||||
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
|
|
||||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
|
||||||
Where("purchase_items.received_date IS NOT NULL")
|
|
||||||
|
|
||||||
if filters.SupplierId > 0 {
|
if filters.SupplierId > 0 {
|
||||||
db = db.Where("suppliers.id = ?", filters.SupplierId)
|
db = db.Where("suppliers.id = ?", filters.SupplierId)
|
||||||
@@ -173,11 +152,7 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context
|
|||||||
Preload("ExpenseNonstock.Expense").
|
Preload("ExpenseNonstock.Expense").
|
||||||
Preload("ExpenseNonstock.Expense.Supplier").
|
Preload("ExpenseNonstock.Expense.Supplier").
|
||||||
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
|
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
|
||||||
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
|
Where("purchases.supplier_id IN ?", supplierIDs)
|
||||||
Where("purchases.supplier_id IN ?", supplierIDs).
|
|
||||||
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
|
|
||||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
|
||||||
Where("purchase_items.received_date IS NOT NULL")
|
|
||||||
|
|
||||||
if filters.ProductId > 0 {
|
if filters.ProductId > 0 {
|
||||||
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
|
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
|
|||||||
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("/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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@@ -21,10 +20,9 @@ import (
|
|||||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||||
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
|
||||||
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
|
||||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
|
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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"
|
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
@@ -44,25 +42,21 @@ type RepportService interface {
|
|||||||
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)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type repportService struct {
|
type repportService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
DB *gorm.DB
|
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
||||||
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
|
||||||
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
|
PurchaseRepo purchaseRepo.PurchaseRepository
|
||||||
PurchaseRepo purchaseRepo.PurchaseRepository
|
ChickinRepo chickinRepo.ProjectChickinRepository
|
||||||
ChickinRepo chickinRepo.ProjectChickinRepository
|
RecordingRepo recordingRepo.RecordingRepository
|
||||||
RecordingRepo recordingRepo.RecordingRepository
|
ApprovalSvc approvalService.ApprovalService
|
||||||
ApprovalSvc approvalService.ApprovalService
|
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
||||||
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
DebtSupplierRepo repportRepo.DebtSupplierRepository
|
||||||
DebtSupplierRepo repportRepo.DebtSupplierRepository
|
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
||||||
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
ProductionResultRepo repportRepo.ProductionResultRepository
|
||||||
ProductionResultRepo repportRepo.ProductionResultRepository
|
|
||||||
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
|
|
||||||
CustomerRepo customerRepo.CustomerRepository
|
|
||||||
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
|
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
|
||||||
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
||||||
}
|
}
|
||||||
@@ -77,7 +71,6 @@ type HppCostAggregate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewRepportService(
|
func NewRepportService(
|
||||||
db *gorm.DB,
|
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
|
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
|
||||||
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
|
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||||
@@ -89,27 +82,22 @@ func NewRepportService(
|
|||||||
debtSupplierRepo repportRepo.DebtSupplierRepository,
|
debtSupplierRepo repportRepo.DebtSupplierRepository,
|
||||||
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
||||||
productionResultRepo repportRepo.ProductionResultRepository,
|
productionResultRepo repportRepo.ProductionResultRepository,
|
||||||
customerPaymentRepo repportRepo.CustomerPaymentRepository,
|
|
||||||
customerRepo customerRepo.CustomerRepository,
|
|
||||||
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
|
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
|
||||||
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
|
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
|
||||||
) RepportService {
|
) RepportService {
|
||||||
return &repportService{
|
return &repportService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
DB: db,
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
MarketingDeliveryRepo: marketingDeliveryRepo,
|
||||||
MarketingDeliveryRepo: marketingDeliveryRepo,
|
PurchaseRepo: purchaseRepo,
|
||||||
PurchaseRepo: purchaseRepo,
|
ChickinRepo: chickinRepo,
|
||||||
ChickinRepo: chickinRepo,
|
RecordingRepo: recordingRepo,
|
||||||
RecordingRepo: recordingRepo,
|
ApprovalSvc: approvalSvc,
|
||||||
ApprovalSvc: approvalSvc,
|
PurchaseSupplierRepo: purchaseSupplierRepo,
|
||||||
PurchaseSupplierRepo: purchaseSupplierRepo,
|
DebtSupplierRepo: debtSupplierRepo,
|
||||||
DebtSupplierRepo: debtSupplierRepo,
|
HppPerKandangRepo: hppPerKandangRepo,
|
||||||
HppPerKandangRepo: hppPerKandangRepo,
|
ProductionResultRepo: productionResultRepo,
|
||||||
ProductionResultRepo: productionResultRepo,
|
|
||||||
CustomerPaymentRepo: customerPaymentRepo,
|
|
||||||
CustomerRepo: customerRepo,
|
|
||||||
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
||||||
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
||||||
}
|
}
|
||||||
@@ -320,15 +308,6 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
|
|||||||
standardDetailCache := make(map[int]*entity.ProductionStandardDetail)
|
standardDetailCache := make(map[int]*entity.ProductionStandardDetail)
|
||||||
growthDetailCache := make(map[int]*entity.StandardGrowthDetail)
|
growthDetailCache := make(map[int]*entity.StandardGrowthDetail)
|
||||||
|
|
||||||
weeks := make([]int, len(weeklyResults))
|
|
||||||
for i := range weeklyResults {
|
|
||||||
weeks[i] = defaultStartWoa + i
|
|
||||||
}
|
|
||||||
uniformityMap, err := s.getUniformityByWeek(ctx.Context(), params.ProjectFlockKandangID, weeks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var cumulativeButir int64
|
var cumulativeButir int64
|
||||||
var cumulativeKg float64
|
var cumulativeKg float64
|
||||||
for i := range weeklyResults {
|
for i := range weeklyResults {
|
||||||
@@ -338,12 +317,6 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
|
|||||||
if weeklyResults[i].StdUniformity == "" {
|
if weeklyResults[i].StdUniformity == "" {
|
||||||
weeklyResults[i].StdUniformity = defaultUniformText
|
weeklyResults[i].StdUniformity = defaultUniformText
|
||||||
}
|
}
|
||||||
if uniformity, ok := uniformityMap[defaultStartWoa+i]; ok {
|
|
||||||
weeklyResults[i].Uniformity = uniformity.Uniformity
|
|
||||||
if uniformity.AvgWeight != nil {
|
|
||||||
weeklyResults[i].Bw = *uniformity.AvgWeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cumulativeButir += weeklyResults[i].ButiranJumlah
|
cumulativeButir += weeklyResults[i].ButiranJumlah
|
||||||
weeklyResults[i].TotalButir = cumulativeButir
|
weeklyResults[i].TotalButir = cumulativeButir
|
||||||
@@ -417,261 +390,6 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
|
|||||||
return weeklyResults, totalWeeks, nil
|
return weeklyResults, totalWeeks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) {
|
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine customer IDs to process
|
|
||||||
var customerIDs []uint
|
|
||||||
var totalCustomers int64
|
|
||||||
|
|
||||||
if len(params.CustomerIDs) > 0 {
|
|
||||||
// Specific customer IDs mode (no pagination)
|
|
||||||
customerIDs = params.CustomerIDs
|
|
||||||
totalCustomers = int64(len(customerIDs))
|
|
||||||
|
|
||||||
if len(customerIDs) == 0 {
|
|
||||||
return []dto.CustomerPaymentReportItem{}, 0, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Multiple customers mode with pagination
|
|
||||||
page := params.Page
|
|
||||||
limit := params.Limit
|
|
||||||
if page < 1 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
if limit < 1 {
|
|
||||||
limit = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := (page - 1) * limit
|
|
||||||
|
|
||||||
var err error
|
|
||||||
customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(customerIDs) == 0 {
|
|
||||||
return []dto.CustomerPaymentReportItem{}, 0, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []dto.CustomerPaymentReportItem
|
|
||||||
for _, customerID := range customerIDs {
|
|
||||||
item, err := s.processCustomerPayment(ctx.Context(), customerID, params)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(item.Rows) > 0 {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalCustomers = int64(len(result))
|
|
||||||
return result, totalCustomers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) {
|
|
||||||
|
|
||||||
customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID)
|
|
||||||
if err != nil {
|
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cid := customerID
|
|
||||||
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid)
|
|
||||||
if err != nil {
|
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
|
|
||||||
runningBalance := initialBalance
|
|
||||||
|
|
||||||
for i, tx := range transactions {
|
|
||||||
|
|
||||||
previousBalance := runningBalance
|
|
||||||
|
|
||||||
row := dto.ToCustomerPaymentReportRow(tx)
|
|
||||||
|
|
||||||
if tx.TransactionType == "SALES" {
|
|
||||||
runningBalance -= tx.TotalPrice
|
|
||||||
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
|
|
||||||
row.Status = status
|
|
||||||
|
|
||||||
if status == "LUNAS" {
|
|
||||||
if paymentDate != nil {
|
|
||||||
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
|
|
||||||
row.AgingDay = &days
|
|
||||||
} else {
|
|
||||||
days := 0
|
|
||||||
row.AgingDay = &days
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
days := int(time.Since(tx.TransDate).Hours() / 24)
|
|
||||||
row.AgingDay = &days
|
|
||||||
}
|
|
||||||
} else if tx.TransactionType == "PAYMENT" {
|
|
||||||
runningBalance += tx.PaymentAmount
|
|
||||||
row.Status = ""
|
|
||||||
row.AgingDay = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
row.AccountsReceivable = runningBalance
|
|
||||||
rows = append(rows, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
if params.StartDate != "" || params.EndDate != "" {
|
|
||||||
filteredRows := make([]dto.CustomerPaymentReportRow, 0, len(rows))
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
|
||||||
if err != nil {
|
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var startDate, endDate *time.Time
|
|
||||||
if params.StartDate != "" {
|
|
||||||
parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
|
|
||||||
if err != nil {
|
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
|
||||||
}
|
|
||||||
startDate = &parsed
|
|
||||||
}
|
|
||||||
if params.EndDate != "" {
|
|
||||||
parsed, err := time.ParseInLocation("2006-01-02", params.EndDate, location)
|
|
||||||
if err != nil {
|
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
|
||||||
}
|
|
||||||
endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location)
|
|
||||||
endDate = &endOfDay
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
transDate := row.TransDate.In(location)
|
|
||||||
if startDate != nil && transDate.Before(*startDate) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if endDate != nil && transDate.After(*endDate) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filteredRows = append(filteredRows, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = filteredRows
|
|
||||||
}
|
|
||||||
|
|
||||||
summary := dto.ToCustomerPaymentReportSummary(rows, initialBalance)
|
|
||||||
|
|
||||||
return dto.ToCustomerPaymentReportItem(*customer, initialBalance, rows, summary), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
|
||||||
currentSales := transactions[currentIndex]
|
|
||||||
|
|
||||||
// Status Logic:
|
|
||||||
// 1. LUNAS: previousBalance >= salesAmount (paid from deposit)
|
|
||||||
// 2. LUNAS: future payments make AR >= 0 (eventually paid)
|
|
||||||
// 3. DIBAYAR SEBAGIAN: has payment but not enough
|
|
||||||
// 4. BELUM LUNAS: no payment at all
|
|
||||||
|
|
||||||
if previousBalance >= currentSales.TotalPrice {
|
|
||||||
// Cari payment yang digunakan untuk melunasi sales ini dengan FIFO
|
|
||||||
// Track payment allocations that are consumed by previous sales
|
|
||||||
type paymentAllocation struct {
|
|
||||||
date time.Time
|
|
||||||
amount float64
|
|
||||||
consumed float64
|
|
||||||
}
|
|
||||||
allocations := []paymentAllocation{}
|
|
||||||
runningBalance := 0.0
|
|
||||||
|
|
||||||
// Process all transactions before current sales to build allocation map
|
|
||||||
for i := 0; i < currentIndex; i++ {
|
|
||||||
if transactions[i].TransactionType == "PAYMENT" {
|
|
||||||
allocations = append(allocations, paymentAllocation{
|
|
||||||
date: transactions[i].TransDate,
|
|
||||||
amount: transactions[i].PaymentAmount,
|
|
||||||
consumed: 0,
|
|
||||||
})
|
|
||||||
runningBalance += transactions[i].PaymentAmount
|
|
||||||
} else if transactions[i].TransactionType == "SALES" {
|
|
||||||
salesAmount := transactions[i].TotalPrice
|
|
||||||
remainingToConsume := salesAmount
|
|
||||||
|
|
||||||
// Consume from oldest allocations first (FIFO)
|
|
||||||
for j := range allocations {
|
|
||||||
if remainingToConsume <= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
available := allocations[j].amount - allocations[j].consumed
|
|
||||||
if available > 0 {
|
|
||||||
consume := available
|
|
||||||
if consume > remainingToConsume {
|
|
||||||
consume = remainingToConsume
|
|
||||||
}
|
|
||||||
allocations[j].consumed += consume
|
|
||||||
remainingToConsume -= consume
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runningBalance -= salesAmount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now find which allocation covers the current sales
|
|
||||||
amountNeeded := currentSales.TotalPrice
|
|
||||||
for _, alloc := range allocations {
|
|
||||||
available := alloc.amount - alloc.consumed
|
|
||||||
if available > 0 {
|
|
||||||
if amountNeeded <= available {
|
|
||||||
// This allocation fully covers the sales
|
|
||||||
return "LUNAS", &alloc.date
|
|
||||||
} else {
|
|
||||||
// This allocation partially covers, continue to next
|
|
||||||
amountNeeded -= available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, use the oldest allocation
|
|
||||||
if len(allocations) > 0 {
|
|
||||||
return "LUNAS", &allocations[0].date
|
|
||||||
}
|
|
||||||
return "LUNAS", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice
|
|
||||||
|
|
||||||
futureBalance := currentBalance
|
|
||||||
hasPayment := false
|
|
||||||
var paymentDateThatMadeItLunas *time.Time
|
|
||||||
|
|
||||||
for i := currentIndex + 1; i < len(transactions); i++ {
|
|
||||||
if transactions[i].TransactionType == "PAYMENT" {
|
|
||||||
futureBalance += transactions[i].PaymentAmount
|
|
||||||
hasPayment = true
|
|
||||||
|
|
||||||
if futureBalance >= 0 {
|
|
||||||
paymentDateThatMadeItLunas = &transactions[i].TransDate
|
|
||||||
return "LUNAS", paymentDateThatMadeItLunas
|
|
||||||
}
|
|
||||||
} else if transactions[i].TransactionType == "SALES" {
|
|
||||||
futureBalance -= transactions[i].TotalPrice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasPayment || hasPartialPaymentFromBalance {
|
|
||||||
return "DIBAYAR SEBAGIAN", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "BELUM LUNAS", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
|
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
|
||||||
result := dto.ProductionResultDTO{
|
result := dto.ProductionResultDTO{
|
||||||
CreatedAt: record.CreatedAt,
|
CreatedAt: record.CreatedAt,
|
||||||
@@ -816,68 +534,6 @@ func getEggFlagType(egg entity.RecordingEgg) (utils.FlagType, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
type uniformityWeekData struct {
|
|
||||||
Uniformity float64
|
|
||||||
AvgWeight *float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type uniformityChartPayload struct {
|
|
||||||
Statistics *uniformityChartStats `json:"statistics"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type uniformityChartStats struct {
|
|
||||||
AverageWeight *float64 `json:"average_weight"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKandangID uint, weeks []int) (map[int]uniformityWeekData, error) {
|
|
||||||
result := make(map[int]uniformityWeekData, len(weeks))
|
|
||||||
if projectFlockKandangID == 0 || len(weeks) == 0 {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []entity.ProjectFlockKandangUniformity
|
|
||||||
if err := s.DB.WithContext(ctx).
|
|
||||||
Model(&entity.ProjectFlockKandangUniformity{}).
|
|
||||||
Select("week, uniformity, uniform_date, id").
|
|
||||||
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
|
||||||
Where("week IN ?", weeks).
|
|
||||||
Order("uniform_date DESC").
|
|
||||||
Order("id DESC").
|
|
||||||
Find(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
if _, exists := result[row.Week]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[row.Week] = uniformityWeekData{
|
|
||||||
Uniformity: row.Uniformity,
|
|
||||||
AvgWeight: extractAverageWeight(row.ChartData, s.Log),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractAverageWeight(raw json.RawMessage, log *logrus.Logger) *float64 {
|
|
||||||
if len(raw) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload uniformityChartPayload
|
|
||||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
||||||
if log != nil {
|
|
||||||
log.WithError(err).Warn("uniformity chart_data decode failed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if payload.Statistics == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return payload.Statistics.AverageWeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO {
|
func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO {
|
||||||
if groupSize <= 0 || len(daily) == 0 {
|
if groupSize <= 0 || len(daily) == 0 {
|
||||||
return daily
|
return daily
|
||||||
@@ -1129,17 +785,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
references := collectDebtSupplierReferences(purchases)
|
|
||||||
paymentSummaries, err := s.DebtSupplierRepo.GetPaymentSummariesByReferences(c.Context(), supplierIDs, references)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||||
@@ -1161,7 +806,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
|
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||||
items := purchasesBySupplier[supplierID]
|
items := purchasesBySupplier[supplierID]
|
||||||
paymentItems := paymentsBySupplier[supplierID]
|
paymentItems := paymentsBySupplier[supplierID]
|
||||||
total := dto.DebtSupplierTotalDTO{}
|
total := dto.DebtSupplierTotalDTO{}
|
||||||
@@ -1169,16 +814,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||||
for _, purchase := range items {
|
for _, purchase := range items {
|
||||||
row := buildDebtSupplierRow(purchase, now, location)
|
row := buildDebtSupplierRow(purchase, now, location)
|
||||||
if reference := resolveDebtSupplierReference(purchase); reference != "" {
|
|
||||||
if summary, ok := paymentSummaries[reference]; ok {
|
|
||||||
if isDebtSupplierPaid(row.TotalPrice, summary.Total) {
|
|
||||||
row.Status = "Lunas"
|
|
||||||
if !summary.LatestPaymentDate.IsZero() {
|
|
||||||
row.Aging = calculateDebtSupplierAging(purchase, summary.LatestPaymentDate, location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
||||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||||
Row: row,
|
Row: row,
|
||||||
@@ -1395,55 +1030,6 @@ func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc
|
|||||||
return purchase.CreatedAt.In(loc)
|
return purchase.CreatedAt.In(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectDebtSupplierReferences(purchases []entity.Purchase) []string {
|
|
||||||
if len(purchases) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen := make(map[string]struct{}, len(purchases))
|
|
||||||
result := make([]string, 0, len(purchases))
|
|
||||||
for _, purchase := range purchases {
|
|
||||||
ref := resolveDebtSupplierReference(purchase)
|
|
||||||
if ref == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[ref]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[ref] = struct{}{}
|
|
||||||
result = append(result, ref)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveDebtSupplierReference(purchase entity.Purchase) string {
|
|
||||||
if purchase.PoNumber != nil {
|
|
||||||
if ref := strings.TrimSpace(*purchase.PoNumber); ref != "" {
|
|
||||||
return ref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ref := strings.TrimSpace(purchase.PrNumber); ref != "" {
|
|
||||||
return ref
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool {
|
|
||||||
if totalPrice <= 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return paymentTotal >= totalPrice-0.000001
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int {
|
|
||||||
prDate := purchase.CreatedAt.In(loc)
|
|
||||||
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
|
|
||||||
stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
|
|
||||||
if stopDate.Before(startDate) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(stopDate.Sub(startDate).Hours() / 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
||||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1471,42 +1057,13 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
costRows, supplierRows, err := s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs)
|
||||||
validPfkIDs := make([]uint, 0, len(repoRows))
|
if err != nil {
|
||||||
pfkIndex := make(map[uint]int, len(repoRows))
|
return nil, nil, err
|
||||||
for idx := range repoRows {
|
|
||||||
row := repoRows[idx]
|
|
||||||
pfkIndex[row.ProjectFlockKandangID] = idx
|
|
||||||
if row.RecordingCount > 0 {
|
|
||||||
validPfkIDs = append(validPfkIDs, row.ProjectFlockKandangID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
costRows := make([]repportRepo.HppPerKandangCostRow, 0)
|
|
||||||
supplierRows := make([]repportRepo.HppPerKandangSupplierRow, 0)
|
|
||||||
if len(validPfkIDs) > 0 {
|
|
||||||
costRows, supplierRows, err = s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
eggMap, err := s.HppPerKandangRepo.GetEggProductionByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
for pfkID, egg := range eggMap {
|
|
||||||
if rowIdx, ok := pfkIndex[pfkID]; ok {
|
|
||||||
repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
|
|
||||||
repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
|
|
||||||
repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
|
|
||||||
repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
costMap := make(map[uint]HppCostAggregate, len(costRows))
|
costMap := make(map[uint]HppCostAggregate, len(costRows))
|
||||||
for _, row := range costRows {
|
for _, row := range costRows {
|
||||||
costMap[row.ProjectFlockKandangID] = HppCostAggregate{
|
costMap[row.KandangID] = HppCostAggregate{
|
||||||
FeedCost: row.FeedCost,
|
FeedCost: row.FeedCost,
|
||||||
OvkCost: row.OvkCost,
|
OvkCost: row.OvkCost,
|
||||||
DocCost: row.DocCost,
|
DocCost: row.DocCost,
|
||||||
@@ -1535,15 +1092,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
category = "DOC"
|
category = "DOC"
|
||||||
}
|
}
|
||||||
|
|
||||||
if seen[sup.ProjectFlockKandangID] == nil {
|
if seen[sup.KandangID] == nil {
|
||||||
seen[sup.ProjectFlockKandangID] = make(map[uint]bool)
|
seen[sup.KandangID] = make(map[uint]bool)
|
||||||
}
|
}
|
||||||
if seen[sup.ProjectFlockKandangID][sup.SupplierID] {
|
if seen[sup.KandangID][sup.SupplierID] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[sup.ProjectFlockKandangID][sup.SupplierID] = true
|
seen[sup.KandangID][sup.SupplierID] = true
|
||||||
|
|
||||||
targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], dto.HppPerKandangSupplierDTO{
|
targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{
|
||||||
ID: int64(sup.SupplierID),
|
ID: int64(sup.SupplierID),
|
||||||
Name: sup.SupplierName,
|
Name: sup.SupplierName,
|
||||||
Alias: sup.SupplierAlias,
|
Alias: sup.SupplierAlias,
|
||||||
@@ -1556,75 +1113,48 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
Max float64
|
Max float64
|
||||||
}
|
}
|
||||||
type weightRangeAggregate struct {
|
type weightRangeAggregate struct {
|
||||||
Summary *dto.HppPerKandangSummaryWeightRangeDTO
|
Summary *dto.HppPerKandangSummaryWeightRangeDTO
|
||||||
RemainingBirds int64
|
EggHppSum float64
|
||||||
RemainingWeightKg float64
|
EggHppCount int
|
||||||
AvgWeightSum float64
|
|
||||||
AvgWeightCount int64
|
|
||||||
EggHppSum float64
|
|
||||||
EggHppCount int
|
|
||||||
FeedSuppliers map[int64]dto.HppPerKandangSupplierDTO
|
|
||||||
DocSuppliers map[int64]dto.HppPerKandangSupplierDTO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
||||||
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
|
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
|
||||||
var totalBirds int64
|
var totalBirds int64
|
||||||
// var totalWeight float64
|
var totalWeight float64
|
||||||
var totalEggPieces int64
|
var totalEggPieces int64
|
||||||
var totalEggKg float64
|
var totalEggKg float64
|
||||||
// var totalRemainingValueRp int64
|
var totalRemainingValueRp int64
|
||||||
var totalEggValueRp int64
|
var totalEggValueRp int64
|
||||||
// var totalHppSum float64
|
var totalHppSum float64
|
||||||
var totalHppCount int
|
var totalHppCount int
|
||||||
var totalDocPriceSum float64
|
var totalDocPriceSum float64
|
||||||
var totalDocPriceCount int
|
var totalDocPriceCount int
|
||||||
var totalEggHppSum float64
|
var totalEggHppSum float64
|
||||||
var totalEggHppCount int
|
var totalEggHppCount int
|
||||||
var totalAvgWeightSum float64
|
|
||||||
var totalAvgWeightCount int64
|
|
||||||
|
|
||||||
for _, row := range repoRows {
|
for _, row := range repoRows {
|
||||||
if !params.ShowUnrecorded && row.RecordingCount == 0 {
|
birdsFloat := row.RemainingChickenBirds
|
||||||
continue
|
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
||||||
|
birdsFloat = 0
|
||||||
}
|
}
|
||||||
|
weightFloat := row.RemainingChickenWeight
|
||||||
// birdsFloat := row.RemainingChickenBirds
|
if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
|
||||||
// if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
weightFloat = 0
|
||||||
// birdsFloat = 0
|
|
||||||
// }
|
|
||||||
// weightFloat := row.RemainingChickenWeight
|
|
||||||
// if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
|
|
||||||
// weightFloat = 0
|
|
||||||
// }
|
|
||||||
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
|
|
||||||
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
|
||||||
eggPiecesFloatRemaining = 0
|
|
||||||
}
|
}
|
||||||
eggTotalPiecesFloat := row.EggProductionTotalPieces
|
eggPiecesFloat := row.EggProductionPieces
|
||||||
if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
|
if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) {
|
||||||
eggTotalPiecesFloat = 0
|
eggPiecesFloat = 0
|
||||||
}
|
}
|
||||||
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
|
eggWeightFloat := row.EggProductionWeightKg
|
||||||
if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
|
|
||||||
eggRemainingWeightFloatRemaining = 0
|
|
||||||
}
|
|
||||||
eggWeightFloat := row.EggProductionTotalWeightKg
|
|
||||||
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
|
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
|
||||||
eggWeightFloat = 0
|
eggWeightFloat = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
avgWeight := 0.0
|
avgWeight := 0.0
|
||||||
if eggTotalPiecesFloat > 0 {
|
if birdsFloat > 0 {
|
||||||
avgWeight = eggWeightFloat / eggTotalPiecesFloat
|
avgWeight = weightFloat / birdsFloat
|
||||||
}
|
}
|
||||||
if params.WeightMin != nil && avgWeight < *params.WeightMin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if params.WeightMax != nil && avgWeight > *params.WeightMax {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
weightMin := math.Floor(avgWeight*10) / 10
|
weightMin := math.Floor(avgWeight*10) / 10
|
||||||
if weightMin < 0 {
|
if weightMin < 0 {
|
||||||
weightMin = 0
|
weightMin = 0
|
||||||
@@ -1632,30 +1162,28 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
weightMax := weightMin + 0.09
|
weightMax := weightMin + 0.09
|
||||||
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
|
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
|
||||||
|
|
||||||
// rowBirds := int64(math.Round(birdsFloat))
|
rowBirds := int64(math.Round(birdsFloat))
|
||||||
costEntry := costMap[row.ProjectFlockKandangID]
|
costEntry := costMap[row.KandangID]
|
||||||
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
||||||
// hppRp := 0.0
|
hppRp := 0.0
|
||||||
// if weightFloat > 0 {
|
if weightFloat > 0 {
|
||||||
// hppRp = totalCost / weightFloat
|
hppRp = totalCost / weightFloat
|
||||||
// }
|
}
|
||||||
eggHpp := 0.0
|
eggHpp := 0.0
|
||||||
if eggWeightFloat > 0 {
|
if eggWeightFloat > 0 {
|
||||||
eggHpp = (totalCost / eggWeightFloat) / 1000
|
eggHpp = totalCost / eggWeightFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
|
rowEggPieces := int64(math.Round(eggPiecesFloat))
|
||||||
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
|
rowEggValue := int64(eggHpp * eggWeightFloat)
|
||||||
// rowRemainingValue := int64(hppRp * weightFloat)
|
rowRemainingValue := int64(hppRp * weightFloat)
|
||||||
avgDocPrice := int64(0)
|
avgDocPrice := int64(0)
|
||||||
if costEntry.DocQty > 0 {
|
if costEntry.DocQty > 0 {
|
||||||
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
|
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
|
||||||
}
|
}
|
||||||
|
|
||||||
nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)
|
|
||||||
|
|
||||||
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
|
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
|
||||||
ID: int(row.ProjectFlockKandangID),
|
ID: int(row.KandangID),
|
||||||
Kandang: dto.HppPerKandangRowKandangDTO{
|
Kandang: dto.HppPerKandangRowKandangDTO{
|
||||||
ID: int64(row.KandangID),
|
ID: int64(row.KandangID),
|
||||||
Name: row.KandangName,
|
Name: row.KandangName,
|
||||||
@@ -1673,35 +1201,32 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
WeightMin: weightMin,
|
WeightMin: weightMin,
|
||||||
WeightMax: weightMax,
|
WeightMax: weightMax,
|
||||||
},
|
},
|
||||||
AvgWeightKg: avgWeight,
|
RemainingChickenBirds: rowBirds,
|
||||||
NameWithPeriode: nameWithPeriod,
|
RemainingChickenWeightKg: weightFloat,
|
||||||
|
AvgWeightKg: avgWeight,
|
||||||
// FeedCostRp: costEntry.FeedCost,
|
// FeedCostRp: costEntry.FeedCost,
|
||||||
// OvkCostRp: costEntry.OvkCost,
|
// OvkCostRp: costEntry.OvkCost,
|
||||||
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
|
DocSuppliers: docSupplierMap[row.KandangID],
|
||||||
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
|
FeedSuppliers: feedSupplierMap[row.KandangID],
|
||||||
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
|
EggProductionPieces: rowEggPieces,
|
||||||
EggProductionKg: eggRemainingWeightFloatRemaining,
|
EggProductionKg: eggWeightFloat,
|
||||||
// EggProductionTotalWeightKg: eggWeightFloat,
|
AverageDocPriceRp: avgDocPrice,
|
||||||
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)),
|
HppRp: hppRp,
|
||||||
AverageDocPriceRp: avgDocPrice,
|
EggHppRpPerKg: eggHpp,
|
||||||
// HppRp: hppRp,
|
RemainingValueRp: rowRemainingValue,
|
||||||
EggHppRpPerKg: eggHpp,
|
EggValueRp: rowEggValue,
|
||||||
// RemainingValueRp: rowRemainingValue,
|
|
||||||
EggValueRp: rowEggValue,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// totalBirds += rowBirds
|
totalBirds += rowBirds
|
||||||
// totalWeight += weightFloat
|
totalWeight += weightFloat
|
||||||
totalEggPieces += rowEggPieces
|
totalEggPieces += rowEggPieces
|
||||||
totalEggKg += eggRemainingWeightFloatRemaining
|
totalEggKg += eggWeightFloat
|
||||||
// totalRemainingValueRp += rowRemainingValue
|
totalRemainingValueRp += rowRemainingValue
|
||||||
totalEggValueRp += rowEggValue
|
totalEggValueRp += rowEggValue
|
||||||
totalAvgWeightSum += avgWeight
|
if weightFloat > 0 {
|
||||||
totalAvgWeightCount++
|
totalHppSum += hppRp
|
||||||
// if weightFloat > 0 {
|
totalHppCount++
|
||||||
// totalHppSum += hppRp
|
}
|
||||||
// totalHppCount++
|
|
||||||
// }
|
|
||||||
if avgDocPrice > 0 {
|
if avgDocPrice > 0 {
|
||||||
totalDocPriceSum += float64(avgDocPrice)
|
totalDocPriceSum += float64(avgDocPrice)
|
||||||
totalDocPriceCount++
|
totalDocPriceCount++
|
||||||
@@ -1721,30 +1246,16 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
},
|
},
|
||||||
Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax),
|
Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax),
|
||||||
},
|
},
|
||||||
FeedSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO),
|
|
||||||
DocSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO),
|
|
||||||
}
|
}
|
||||||
perRangeMap[rangeKey] = rangeAgg
|
perRangeMap[rangeKey] = rangeAgg
|
||||||
}
|
}
|
||||||
|
|
||||||
rangeSummary := rangeAgg.Summary
|
rangeSummary := rangeAgg.Summary
|
||||||
// rangeAgg.RemainingBirds += rowBirds
|
rangeSummary.RemainingChickenBirds += rowBirds
|
||||||
// rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
|
rangeSummary.RemainingChickenWeightKg += row.RemainingChickenWeight
|
||||||
rangeAgg.AvgWeightSum += avgWeight
|
|
||||||
rangeAgg.AvgWeightCount++
|
|
||||||
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
|
|
||||||
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
|
|
||||||
rangeAgg.FeedSuppliers[supplier.ID] = supplier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
|
|
||||||
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
|
|
||||||
rangeAgg.DocSuppliers[supplier.ID] = supplier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rangeSummary.EggProductionPieces += rowEggPieces
|
rangeSummary.EggProductionPieces += rowEggPieces
|
||||||
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
|
rangeSummary.EggProductionKg += eggWeightFloat
|
||||||
// rangeSummary.RemainingValueRp += rowRemainingValue
|
rangeSummary.RemainingValueRp += rowRemainingValue
|
||||||
rangeSummary.EggValueRp += rowEggValue
|
rangeSummary.EggValueRp += rowEggValue
|
||||||
if eggWeightFloat > 0 {
|
if eggWeightFloat > 0 {
|
||||||
rangeAgg.EggHppSum += eggHpp
|
rangeAgg.EggHppSum += eggHpp
|
||||||
@@ -1768,37 +1279,31 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
|||||||
agg := perRangeMap[key]
|
agg := perRangeMap[key]
|
||||||
entry := agg.Summary
|
entry := agg.Summary
|
||||||
entry.ID = idx + 1
|
entry.ID = idx + 1
|
||||||
if agg.AvgWeightCount > 0 {
|
if entry.RemainingChickenBirds > 0 {
|
||||||
entry.AvgWeightKg = agg.AvgWeightSum / float64(agg.AvgWeightCount)
|
entry.AvgWeightKg = entry.RemainingChickenWeightKg / float64(entry.RemainingChickenBirds)
|
||||||
}
|
}
|
||||||
if agg.EggHppCount > 0 {
|
if agg.EggHppCount > 0 {
|
||||||
entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount)
|
entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount)
|
||||||
}
|
}
|
||||||
entry.FeedSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.FeedSuppliers))
|
|
||||||
for _, supplier := range agg.FeedSuppliers {
|
|
||||||
entry.FeedSuppliers = append(entry.FeedSuppliers, supplier)
|
|
||||||
}
|
|
||||||
entry.DocSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.DocSuppliers))
|
|
||||||
for _, supplier := range agg.DocSuppliers {
|
|
||||||
entry.DocSuppliers = append(entry.DocSuppliers, supplier)
|
|
||||||
}
|
|
||||||
perRangeSummary = append(perRangeSummary, *entry)
|
perRangeSummary = append(perRangeSummary, *entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalSummary := dto.HppPerKandangSummaryTotalDTO{
|
totalSummary := dto.HppPerKandangSummaryTotalDTO{
|
||||||
TotalEggProductionPieces: totalEggPieces,
|
TotalRemainingChickenBirds: totalBirds,
|
||||||
TotalEggProductionKg: totalEggKg,
|
TotalRemainingChickenWeightKg: totalWeight,
|
||||||
TotalEggValueRp: totalEggValueRp,
|
TotalEggProductionPieces: totalEggPieces,
|
||||||
|
TotalEggProductionKg: totalEggKg,
|
||||||
|
TotalRemainingValueRp: totalRemainingValueRp,
|
||||||
|
TotalEggValueRp: totalEggValueRp,
|
||||||
}
|
}
|
||||||
if totalBirds > 0 {
|
if totalBirds > 0 {
|
||||||
}
|
totalSummary.AverageWeightKg = totalWeight / float64(totalBirds)
|
||||||
if totalAvgWeightCount > 0 {
|
|
||||||
totalSummary.AverageWeightKg = totalAvgWeightSum / float64(totalAvgWeightCount)
|
|
||||||
}
|
}
|
||||||
if totalEggHppCount > 0 {
|
if totalEggHppCount > 0 {
|
||||||
totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)
|
totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)
|
||||||
}
|
}
|
||||||
if totalHppCount > 0 {
|
if totalHppCount > 0 {
|
||||||
|
totalSummary.TotalHppRp = totalHppSum / float64(totalHppCount)
|
||||||
}
|
}
|
||||||
if totalDocPriceCount > 0 {
|
if totalDocPriceCount > 0 {
|
||||||
totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount)
|
totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount)
|
||||||
@@ -1891,9 +1396,6 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
if weightMin != nil && weightMax != nil && *weightMin > *weightMax {
|
|
||||||
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max")
|
|
||||||
}
|
|
||||||
|
|
||||||
params := &validation.HppPerKandangQuery{
|
params := &validation.HppPerKandangQuery{
|
||||||
Page: page,
|
Page: page,
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ type ExpenseQuery struct {
|
|||||||
|
|
||||||
type MarketingQuery struct {
|
type MarketingQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=100"`
|
Search string `query:"search" validate:"omitempty,max=100"`
|
||||||
CustomerId int64 `query:"customer_id" validate:"omitempty"`
|
CustomerId int64 `query:"customer_id" validate:"omitempty"`
|
||||||
ProductId int64 `query:"product_id" validate:"omitempty"`
|
ProductId int64 `query:"product_id" validate:"omitempty"`
|
||||||
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
||||||
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
||||||
AreaId int64 `query:"area_id" validate:"omitempty"`
|
|
||||||
LocationId int64 `query:"location_id" validate:"omitempty"`
|
|
||||||
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
|
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
|
||||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
|
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
|
||||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
@@ -58,7 +56,7 @@ type DebtSupplierQuery struct {
|
|||||||
|
|
||||||
type HppPerKandangQuery struct {
|
type HppPerKandangQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||||
Period string `query:"period" validate:"required"`
|
Period string `query:"period" validate:"required"`
|
||||||
ShowUnrecorded bool `query:"show_unrecorded"`
|
ShowUnrecorded bool `query:"show_unrecorded"`
|
||||||
AreaIDs []int64 `query:"-"`
|
AreaIDs []int64 `query:"-"`
|
||||||
@@ -70,14 +68,6 @@ type HppPerKandangQuery struct {
|
|||||||
|
|
||||||
type ProductionResultQuery struct {
|
type ProductionResultQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||||
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
|
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerPaymentQuery struct {
|
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
|
||||||
CustomerIDs []uint `query:"customer_ids" 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"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,19 +2,18 @@ package fifo
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Usable Keys
|
// Usable Keys
|
||||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||||
UsableKeyRecordingDepletion UsableKey = "RECORDING_DEPLETION"
|
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
|
||||||
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
|
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
||||||
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
||||||
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
|
||||||
|
|
||||||
// Stockable Keys
|
// Stockable Keys
|
||||||
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
|
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
|
||||||
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
||||||
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
||||||
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
||||||
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
||||||
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
|
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto
|
|||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
usagePtr := new(float64)
|
usagePtr := new(float64)
|
||||||
*usagePtr = item.Qty
|
*usagePtr = item.Qty
|
||||||
|
pending := item.PendingQty
|
||||||
|
if pending == nil {
|
||||||
|
pending = new(float64)
|
||||||
|
}
|
||||||
result = append(result, entity.RecordingStock{
|
result = append(result, entity.RecordingStock{
|
||||||
RecordingId: recordingID,
|
RecordingId: recordingID,
|
||||||
ProductWarehouseId: item.ProductWarehouseId,
|
ProductWarehouseId: item.ProductWarehouseId,
|
||||||
UsageQty: usagePtr,
|
UsageQty: usagePtr,
|
||||||
|
PendingQty: pending,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user