Compare commits

..

1 Commits

Author SHA1 Message Date
giovanni 09c951c16a adjust max limit 2026-01-20 11:49:40 +07:00
26 changed files with 196 additions and 1072 deletions
@@ -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;
+4 -6
View File
@@ -1,12 +1,10 @@
package entities
type RecordingDepletion struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
Qty float64 `gorm:"column:qty;not null"`
PendingQty float64 `gorm:"column:pending_qty"`
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty float64 `gorm:"column:qty;not null"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
@@ -236,10 +236,9 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
}
query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
@@ -278,45 +277,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 {
param := c.Params("project_flock_id")
flag := c.Query("flag", "")
+20 -20
View File
@@ -98,26 +98,26 @@ type ClosingEggSalesDTO struct {
}
type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct float64 `json:"hen_day_act,omitempty"`
HendayStd float64 `json:"hen_day_std"`
EggMass float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"`
EggWeight float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"`
HenHouseAct float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"`
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct *float64 `json:"hen_day_act,omitempty"`
HendayStd float64 `json:"hen_day_std"`
EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"`
EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"`
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"`
}
type ClosingSalesGroupDTO struct {
@@ -114,17 +114,6 @@ type ClosingSapronakDTO struct {
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 ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
@@ -212,48 +201,18 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk
if row.UnitPrice == 0 {
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"
}
}
row.TotalAmount += item.Nilai
case "pemakaian", "adjustment keluar":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyUsed += item.QtyKeluar
row.TotalAmount += item.QtyKeluar * price
case "mutasi keluar", "penjualan":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
case "mutasi keluar":
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:
row.QtyIn += item.QtyMasuk
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
}
avg := 0.0
if qtyUsed > 0 {
avg = total / qtyUsed
if qtyIn > 0 {
avg = total / qtyIn
}
cat.Total = SapronakCategoryTotalDTO{
Label: label,
@@ -17,7 +17,6 @@ import (
type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
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)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
@@ -34,7 +33,6 @@ type ClosingRepository interface {
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (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)
FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
}
@@ -61,18 +59,10 @@ type SapronakRow struct {
DestinationWarehouse string `gorm:"column:destination_warehouse"`
Destination string `gorm:"column:destination"`
Quantity float64 `gorm:"column:quantity"`
UnitID uint `gorm:"column:unit_id"`
Unit string `gorm:"column:unit"`
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"`
@@ -84,7 +74,6 @@ type SapronakQueryParams struct {
ProjectFlockKandangIDs []uint
Limit int
Offset int
Search string
}
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
@@ -120,36 +109,14 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
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
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause)
countArgs := append(append([]any{}, args...), searchArgs...)
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
return nil, 0, err
}
dataArgs := append(append([]any{}, args...), searchArgs...)
dataArgs = append(dataArgs, params.Limit, params.Offset)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
var rows []SapronakRow
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
@@ -159,79 +126,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
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
@@ -485,7 +379,6 @@ SELECT
w.name AS destination_warehouse,
'' AS destination,
pi.total_qty AS quantity,
u.id AS unit_id,
u.name AS unit,
COALESCE(p.notes, '') AS notes
FROM purchase_items pi
@@ -534,7 +427,6 @@ SELECT
COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination,
std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit,
'Stock Refill' AS notes
FROM stock_transfer_details std
@@ -584,7 +476,6 @@ SELECT
COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination,
std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit,
'Transfer to other unit' AS notes
FROM stock_transfer_details std
@@ -631,15 +522,13 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
w.name AS source_warehouse,
COALESCE(c.name, '') AS destination_warehouse,
'RETAIL CUSTOMER' AS destination_warehouse,
'' AS destination,
mp.qty AS quantity,
u.id AS unit_id,
u.name AS unit,
m.notes AS notes
FROM marketing_products mp
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 products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id
@@ -1020,50 +909,17 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
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 {
return nil, nil, err
}
incomingLayingQuery := r.withCtx(ctx).
Table("laying_transfer_targets AS ltt").
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,
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(`
@@ -1080,13 +936,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
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)
@@ -1094,71 +947,9 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
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) {
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) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
if len(productIDs) == 0 {
return []entity.Product{}, nil
-1
View File
@@ -30,7 +30,6 @@ 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/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
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/: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)
@@ -40,7 +40,6 @@ type ClosingService interface {
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, 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)
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
}
@@ -354,7 +353,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
ProjectFlockKandangIDs: projectFlockKandangIDs,
Limit: params.Limit,
Offset: offset,
Search: params.Search,
})
if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
@@ -389,74 +387,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
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) {
var kandangIDs []uint
db := s.Repository.DB().WithContext(ctx)
@@ -930,19 +860,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
if !isGrowing {
if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = henDayAct
performance.HenDayAct = &henDayAct
}
if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = henHouseAct
performance.HenHouseAct = &henHouseAct
}
if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = eggWeight
performance.EggWeight = &eggWeight
}
if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg
performance.EggMass = eggMass
performance.EggMass = &eggMass
}
}
performance.DeffFcr = performance.FcrStd - performance.FcrAct
@@ -1100,3 +1030,4 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
return closest.Mortality, closest.FcrNumber
}
@@ -2,7 +2,6 @@ package service
import (
"context"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
@@ -112,7 +111,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
}
// 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, params.Flag)
if err != nil {
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")
@@ -263,7 +262,6 @@ type sapronakDetailMaps struct {
AdjOutgoing map[uint][]dto.SapronakDetailDTO
TransferIn map[uint][]dto.SapronakDetailDTO
TransferOut map[uint][]dto.SapronakDetailDTO
SalesOut map[uint][]dto.SapronakDetailDTO
}
func buildSapronakDetails(
@@ -273,7 +271,6 @@ func buildSapronakDetails(
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
transferInRows map[uint][]repository.SapronakDetailRow,
transferOutRows map[uint][]repository.SapronakDetailRow,
salesOutRows map[uint][]repository.SapronakDetailRow,
) sapronakDetailMaps {
result := sapronakDetailMaps{
Incoming: make(map[uint][]dto.SapronakDetailDTO),
@@ -282,7 +279,6 @@ func buildSapronakDetails(
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
TransferIn: 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) {
@@ -315,7 +311,6 @@ func buildSapronakDetails(
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
return result
}
@@ -355,10 +350,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil {
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))
matchesFlag := func(f string) bool {
@@ -371,34 +362,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
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
// should not be counted yet. Only when category is LAYING we allow
@@ -437,17 +400,13 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
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
usageDetails := detailMaps.Usage
adjIncoming := detailMaps.AdjIncoming
adjOutgoing := detailMaps.AdjOutgoing
transIncoming := detailMaps.TransferIn
transOutgoing := detailMaps.TransferOut
salesOutgoing := detailMaps.SalesOut
transIncoming = dedupTransfers(transIncoming)
transOutgoing = dedupTransfers(transOutgoing)
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
if g, ok := groupMap[flag]; ok {
@@ -724,25 +683,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
}
}
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.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
}
}
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
for _, g := range groupMap {
groups = append(groups, *g)
@@ -24,5 +24,4 @@ type ClosingSapronakQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
}
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
if err != nil {
return "", err
}
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq)
movementNumber := fmt.Sprintf("ST-%05d", seq)
return movementNumber, nil
}
@@ -26,9 +26,8 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
}
if projectFlockID > 0 {
query.ProjectFlockKandangId = uint(projectFlockID)
@@ -15,13 +15,13 @@ import (
// === DTO Structs ===
type RecordingProjectFlockDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
FlockName string `json:"flock_name"`
ProjectFlockCategory string `json:"project_flock_category"`
Period int `json:"period"`
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
TotalChickQty float64 `json:"total_chick_qty"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
FlockName string `json:"flock_name"`
ProjectFlockCategory string `json:"project_flock_category"`
Period int `json:"period"`
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
TotalChickQty float64 `json:"total_chick_qty"`
}
type RecordingProductionStandardDTO struct {
@@ -53,13 +53,6 @@ type RecordingLocationDTO struct {
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 {
Id uint `json:"id"`
Name string `json:"name"`
@@ -89,14 +82,12 @@ type RecordingListDTO struct {
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Kandang *RecordingKandangDTO `json:"kandang,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
}
type RecordingDetailDTO struct {
RecordingListDTO
ProductCategory string `json:"product_category"`
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
ProductCategory string `json:"product_category"`
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
@@ -142,11 +133,10 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
return RecordingDetailDTO{
RecordingListDTO: listDTO,
ProductCategory: recordingProductCategory(e),
Warehouse: recordingWarehouseDTO(e),
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs),
ProductCategory: recordingProductCategory(e),
Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs),
}
}
@@ -212,8 +202,7 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO {
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
Kandang: recordingKandangDTO(e),
Location: recordingKandangLocationDTO(e),
Warehouse: recordingWarehouseDTO(e),
}
}
@@ -225,20 +214,20 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
}
return RecordingRelationDTO{
Id: e.Id,
ProjectFlock: toRecordingProjectFlockDTO(e),
RecordDatetime: e.RecordDatetime,
Day: intValue(e.Day),
TotalDepletionQty: floatValue(e.TotalDepletionQty),
CumDepletionRate: floatValue(e.CumDepletionRate),
CumIntake: intValue(e.CumIntake),
FcrValue: floatValue(e.FcrValue),
HenDay: floatValue(e.HenDay),
HenHouse: floatValue(e.HenHouse),
FeedIntake: floatValue(e.FeedIntake),
EggMass: floatValue(e.EggMass),
EggWeight: floatValue(e.EggWeight),
Approval: latestApproval,
Id: e.Id,
ProjectFlock: toRecordingProjectFlockDTO(e),
RecordDatetime: e.RecordDatetime,
Day: intValue(e.Day),
TotalDepletionQty: floatValue(e.TotalDepletionQty),
CumDepletionRate: floatValue(e.CumDepletionRate),
CumIntake: intValue(e.CumIntake),
FcrValue: floatValue(e.FcrValue),
HenDay: floatValue(e.HenDay),
HenHouse: floatValue(e.HenHouse),
FeedIntake: floatValue(e.FeedIntake),
EggMass: floatValue(e.EggMass),
EggWeight: floatValue(e.EggWeight),
Approval: latestApproval,
}
}
@@ -332,34 +321,6 @@ func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
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 {
if len(e.Stocks) > 0 {
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))
}
}
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)
approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -17,7 +17,6 @@ type RecordingRepository interface {
repository.BaseRepository[entity.Recording]
WithRelations(db *gorm.DB) *gorm.DB
ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
@@ -25,7 +24,6 @@ type RecordingRepository interface {
DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, 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
DeleteDepletions(tx *gorm.DB, recordingID uint) error
@@ -86,7 +84,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("CreatedUser").
Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.Kandang").
Preload("ProjectFlockKandang.Kandang.Location").
Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
@@ -110,42 +107,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
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) {
if projectFlockKandangId == 0 {
return nil, errors.New("project_flock_kandang_id is required")
@@ -206,12 +167,6 @@ func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, us
}).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 {
if len(depletions) == 0 {
return nil
@@ -367,25 +322,38 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
}
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
var result struct {
var rows []struct {
TotalQty float64
UomName string
}
if err := tx.
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 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").
Where("recording_stocks.recording_id = ?", recordingID).
Scan(&result).Error; err != nil {
Scan(&rows).Error; err != nil {
return 0, err
}
if result.TotalQty <= 0 {
return 0, nil
var total float64
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) {
@@ -44,7 +44,6 @@ type RecordingFIFOIntegrationService interface {
}
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
type recordingService struct {
Log *logrus.Logger
@@ -117,8 +116,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
if params.ProjectFlockKandangId != 0 {
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
}
db = s.Repository.ApplySearchFilters(db, params.Search)
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
return db.Order("record_datetime DESC").Order("created_at DESC")
})
if err != nil {
@@ -211,6 +209,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if !isLaying && len(req.Eggs) > 0 {
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 {
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)
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 {
s.Log.Errorf("Failed to persist depletions: %+v", 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)
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
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 {
s.Log.Errorf("Failed to adjust product warehouses: %+v", 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 {
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 {
@@ -437,38 +431,17 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
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 {
s.Log.Errorf("Failed to clear depletions: %+v", err)
return err
}
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 {
s.Log.Errorf("Failed to update depletions: %+v", 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 {
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", 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)
return err
}
if s.FifoSvc != nil {
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions); err != nil {
return err
}
}
oldEggs, err := s.Repository.ListEggs(tx, id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -797,46 +765,6 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
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 {
return s.consumeRecordingStocks(ctx, tx, stocks)
}
@@ -868,67 +796,10 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
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 {
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(
oldDepletions, newDepletions []entity.RecordingDepletion,
oldEggs, newEggs []entity.RecordingEgg,
@@ -1070,8 +941,10 @@ func (s *recordingService) syncRecordingStocks(
desired := item.Qty
stock.UsageQty = &desired
zero := 0.0
stock.PendingQty = &zero
if item.PendingQty != nil {
pending := *item.PendingQty
stock.PendingQty = &pending
}
stocksToConsume = append(stocksToConsume, stock)
}
@@ -1117,20 +990,43 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
}
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)
existingTotal := make(map[uint]float64)
for _, stock := range existing {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
existingUsage[stock.ProductWarehouseId] += usage
existingTotal[stock.ProductWarehouseId] += usage + pending
}
incomingUsage := make(map[uint]float64)
incomingTotal := make(map[uint]float64)
for _, item := range incoming {
var pending float64
if item.PendingQty != nil {
pending = *item.PendingQty
}
incomingUsage[item.ProductWarehouseId] += item.Qty
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
}
if hasPending {
return floatMapsMatch(existingTotal, incomingTotal)
}
return floatMapsMatch(existingUsage, incomingUsage)
}
@@ -1328,7 +1224,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMass = totalEggWeightGrams / remainingChick
eggMass = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mass"] = eggMass
recording.EggMass = &eggMass
} else {
@@ -1338,7 +1234,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggWeight float64
if totalEggQty > 0 && totalEggWeightGrams > 0 {
eggWeight = totalEggWeightGrams / totalEggQty
eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
updates["egg_weight"] = eggWeight
recording.EggWeight = &eggWeight
} else {
@@ -2,8 +2,9 @@ package validation
type (
Stock struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Qty float64 `json:"qty" validate:"required,gte=0"`
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Qty float64 `json:"qty" validate:"required,gte=0"`
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
}
Depletion struct {
@@ -19,24 +20,23 @@ type (
)
type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions" validate:"dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
}
type Update struct {
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
Search string `query:"search" validate:"omitempty,max=50"`
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
}
type Approve struct {
@@ -345,52 +345,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
); err != nil {
return nil, err
}
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 {
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil {
return nil, err
}
@@ -532,35 +487,8 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
if req.ProjectFlockKandangId != nil {
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 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
}
}
@@ -676,7 +604,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
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 {
return nil
}
@@ -27,12 +27,12 @@ type PurchaseSupplierRowDTO struct {
}
type PurchaseSupplierSummaryDTO struct {
TotalQty float64 `json:"total_qty"`
TotalPurchaseValue float64 `json:"total_purchase_value"`
TotalTransportValue float64 `json:"total_transport_value"`
TotalAmount float64 `json:"total_amount"`
TotalUnitPrice float64 `json:"total_unit_price"`
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
TotalQty float64 `json:"total_qty"`
TotalPurchaseValue float64 `json:"total_purchase_value"`
TotalTransportValue float64 `json:"total_transport_value"`
TotalAmount float64 `json:"total_amount"`
TotalUnitPrice float64 `json:"total_unit_price"`
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
}
type PurchaseSupplierDTO struct {
@@ -122,6 +122,11 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
rows := make([]PurchaseSupplierRowDTO, 0, len(items))
summary := PurchaseSupplierSummaryDTO{}
var unitPriceSum float64
var unitPriceCount int
var transportUnitPriceSum float64
var transportUnitPriceCount int
for i := range items {
row := ToPurchaseSupplierRowDTO(&items[i])
rows = append(rows, row)
@@ -131,16 +136,19 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
summary.TotalTransportValue += row.TransportValue
summary.TotalAmount += row.TotalAmount
unitPriceSum += row.UnitPrice
unitPriceCount++
transportUnitPriceSum += row.TransportUnitPrice
transportUnitPriceCount++
}
if summary.TotalQty > 0 {
avg := summary.TotalPurchaseValue / summary.TotalQty
summary.TotalUnitPrice = math.Round(avg)
if unitPriceCount > 0 {
summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount))
}
if summary.TotalQty > 0 {
avg := summary.TotalTransportValue / summary.TotalQty
summary.TotalTransportUnitPrice = math.Round(avg)
if transportUnitPriceCount > 0 {
summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount))
}
return PurchaseSupplierDTO{
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
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)
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, 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)
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
}
@@ -28,11 +25,6 @@ type debtSupplierRepositoryImpl struct {
db *gorm.DB
}
type PaymentReferenceSummary struct {
Total float64
LatestPaymentDate time.Time
}
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
return &debtSupplierRepositoryImpl{db: db}
}
@@ -175,8 +167,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context,
Model(&entity.Payment{}).
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal))
Where("party_id IN ?", supplierIDs)
if strings.TrimSpace(filters.StartDate) != "" {
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
@@ -247,7 +238,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
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
@@ -264,75 +254,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
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) {
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
return map[uint]float64{}, nil
@@ -392,7 +313,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
Where("DATE(payment_date) < ?", dateFrom).
Group("party_id").
Scan(&rows).Error; err != nil {
@@ -25,21 +25,6 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository {
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 {
dateColumn := "purchase_items.received_date"
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
@@ -49,16 +34,10 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context,
dateColumn = "purchase_items.received_date"
}
latestApproval := r.latestPurchaseApproval(ctx)
db := r.db.WithContext(ctx).
Model(&entity.Supplier{}).
Joins("JOIN purchases ON purchases.supplier_id = suppliers.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")
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id")
if filters.SupplierId > 0 {
db = db.Where("suppliers.id = ?", filters.SupplierId)
@@ -173,11 +152,7 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context
Preload("ExpenseNonstock.Expense").
Preload("ExpenseNonstock.Expense.Supplier").
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("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")
Where("purchases.supplier_id IN ?", supplierIDs)
if filters.ProductId > 0 {
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
@@ -1129,17 +1129,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
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")
if err != nil {
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
@@ -1161,7 +1150,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
continue
}
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
items := purchasesBySupplier[supplierID]
paymentItems := paymentsBySupplier[supplierID]
total := dto.DebtSupplierTotalDTO{}
@@ -1169,16 +1158,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
for _, purchase := range items {
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)
combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row,
@@ -1395,55 +1374,6 @@ func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, 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) {
params, filters, err := s.parseHppPerKandangQuery(ctx)
if err != nil {
@@ -70,7 +70,7 @@ type HppPerKandangQuery struct {
type ProductionResultQuery struct {
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,gt=0"`
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
}
+11 -12
View File
@@ -2,19 +2,18 @@ package fifo
const (
// Usable Keys
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyRecordingDepletion UsableKey = "RECORDING_DEPLETION"
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
// Stockable Keys
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
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 {
usagePtr := new(float64)
*usagePtr = item.Qty
pending := item.PendingQty
if pending == nil {
pending = new(float64)
}
result = append(result, entity.RecordingStock{
RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId,
UsageQty: usagePtr,
PendingQty: pending,
})
}
return result