codex/fix: show farm stock usage on closing page

This commit is contained in:
Adnan Zahir
2026-04-01 12:31:04 +07:00
parent c4add1501d
commit 5ffb72507b
4 changed files with 549 additions and 25 deletions
@@ -25,8 +25,8 @@ type ClosingRepository interface {
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error)
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
FetchSapronakIncoming(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error)
FetchSapronakIncomingDetails(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error)
FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
FetchSapronakChickinUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error)
@@ -90,6 +90,23 @@ type SapronakQueryParams struct {
EndDate *time.Time
}
func sapronakIncomingPurchaseQueryParts(params SapronakQueryParams) (string, []any) {
if len(params.ProjectFlockKandangIDs) > 0 {
return sapronakIncomingPurchasesScopedSQL(), []any{
fifo.UsableKeyRecordingStock.String(),
fifo.UsableKeyProjectChickin.String(),
fifo.StockableKeyPurchaseItems.String(),
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
params.ProjectFlockKandangIDs,
params.ProjectFlockKandangIDs,
params.WarehouseIDs,
}
}
return sapronakIncomingPurchasesSQL, []any{params.WarehouseIDs}
}
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
db := r.DB().WithContext(ctx)
@@ -103,8 +120,10 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
if len(params.WarehouseIDs) == 0 {
return []SapronakRow{}, 0, nil
}
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs)
purchasesSQL, purchaseArgs := sapronakIncomingPurchaseQueryParts(params)
unionParts = append(unionParts, purchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
args = append(args, purchaseArgs...)
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
@@ -193,8 +212,10 @@ func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params S
if len(params.WarehouseIDs) == 0 {
return []SapronakSummaryRow{}, nil
}
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs)
purchasesSQL, purchaseArgs := sapronakIncomingPurchaseQueryParts(params)
unionParts = append(unionParts, purchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
args = append(args, purchaseArgs...)
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
@@ -855,6 +876,140 @@ func sapronakFlags(flags ...utils.FlagType) []string {
return out
}
func sapronakLegacyFlagByProductCategoryCase(categoryCodeExpr string) string {
return fmt.Sprintf(
`CASE
WHEN UPPER(%s) = 'DOC' THEN '%s'
WHEN UPPER(%s) = 'PLT' THEN '%s'
WHEN UPPER(%s) IN ('RAW', 'PST', 'STR', 'FSR') THEN '%s'
WHEN UPPER(%s) IN ('OBT', 'VTM', 'KMA') THEN '%s'
ELSE NULL
END`,
categoryCodeExpr, utils.FlagDOC,
categoryCodeExpr, utils.FlagPullet,
categoryCodeExpr, utils.FlagPakan,
categoryCodeExpr, utils.FlagOVK,
)
}
func sapronakIncomingPurchasesScopedSQL() string {
return `
WITH scoped_farm_allocations AS (
SELECT
sa.stockable_id AS purchase_item_id,
COALESCE(SUM(sa.qty), 0) AS allocated_qty
FROM stock_allocations sa
LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?
LEFT JOIN recordings rec ON rec.id = rs.recording_id AND rec.deleted_at IS NULL
LEFT JOIN project_chickins pc ON pc.id = sa.usable_id AND sa.usable_type = ?
WHERE sa.stockable_type = ?
AND sa.status = ?
AND sa.allocation_purpose = ?
AND COALESCE(rec.project_flock_kandangs_id, pc.project_flock_kandang_id) IN ?
GROUP BY sa.stockable_id
)
SELECT
CAST(pi.id AS BIGINT) AS id,
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
COALESCE(p.po_number, '') AS reference_number,
'Pembelian' AS transaction_type,
prod.name AS product_name,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_category,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
'-' AS source_warehouse,
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
JOIN purchases p ON p.id = pi.purchase_id
JOIN products prod ON prod.id = pi.product_id
JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pi.warehouse_id
WHERE w.kandang_id IS NOT NULL
AND (
pi.project_flock_kandang_id IN ?
OR (pi.project_flock_kandang_id IS NULL AND pi.warehouse_id IN ?)
)
UNION ALL
SELECT
CAST(pi.id AS BIGINT) AS id,
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
COALESCE(p.po_number, '') AS reference_number,
'Pembelian' AS transaction_type,
prod.name AS product_name,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_category,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
'-' AS source_warehouse,
w.name AS destination_warehouse,
'' AS destination,
sfa.allocated_qty AS quantity,
u.id AS unit_id,
u.name AS unit,
COALESCE(p.notes, '') AS notes
FROM purchase_items pi
JOIN purchases p ON p.id = pi.purchase_id
JOIN products prod ON prod.id = pi.product_id
JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pi.warehouse_id
JOIN scoped_farm_allocations sfa ON sfa.purchase_item_id = pi.id
WHERE w.kandang_id IS NULL
AND COALESCE(sfa.allocated_qty, 0) > 0
`
}
var (
sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
@@ -862,18 +1017,44 @@ var (
)
func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB {
subquery := r.DB().
actualFlags := r.DB().
Table("flags").
Select("DISTINCT ON (flagable_id) flagable_id, name").
Select(`
flagable_id,
MIN(CASE
WHEN UPPER(name) = 'DOC' THEN 1
WHEN UPPER(name) = 'PULLET' THEN 2
WHEN UPPER(name) = 'PAKAN' THEN 3
WHEN UPPER(name) = 'OVK' THEN 4
ELSE 5
END) AS priority
`).
Where("flagable_type = ?", entity.FlagableTypeProduct).
Where("name IN ?", sapronakFlagsAll).
Order(fmt.Sprintf(
"flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 ELSE 5 END",
Where("UPPER(name) IN ?", sapronakFlagsAll).
Group("flagable_id")
legacyFlagExpr := sapronakLegacyFlagByProductCategoryCase("pc.code")
subquery := r.DB().
Table("products AS sapronak_products").
Select(fmt.Sprintf(`
sapronak_products.id AS flagable_id,
CASE
WHEN actual_flags.priority = 1 THEN '%s'
WHEN actual_flags.priority = 2 THEN '%s'
WHEN actual_flags.priority = 3 THEN '%s'
WHEN actual_flags.priority = 4 THEN '%s'
ELSE %s
END AS name
`,
utils.FlagDOC,
utils.FlagPullet,
utils.FlagPakan,
utils.FlagOVK,
))
legacyFlagExpr,
)).
Joins("LEFT JOIN (?) AS actual_flags ON actual_flags.flagable_id = sapronak_products.id", actualFlags).
Joins("LEFT JOIN product_categories pc ON pc.id = sapronak_products.product_category_id").
Where("actual_flags.priority IS NOT NULL OR " + legacyFlagExpr + " IS NOT NULL")
return db.Joins("JOIN (?) f ON f.flagable_id = "+productAlias+".id", subquery)
}
@@ -1132,22 +1313,111 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
return scanAndGroupDetails(query)
}
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint, start, end *time.Time) *gorm.DB {
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) *gorm.DB {
db := r.withCtx(ctx).
Table("purchase_items AS pi").
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
Joins("JOIN products p ON p.id = pi.product_id").
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Where("pi.received_date IS NOT NULL")
if projectFlockKandangID > 0 {
db = db.Where(
"w.kandang_id = ? AND (pi.project_flock_kandang_id = ? OR pi.project_flock_kandang_id IS NULL)",
kandangID,
projectFlockKandangID,
)
} else {
db = db.Where("w.kandang_id = ?", kandangID)
}
db = applyDateRange(db, "pi.received_date", start, end)
return r.joinSapronakProductFlag(db, "p")
}
func (r *ClosingRepositoryImpl) incomingFarmPurchaseAllocationBase(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) *gorm.DB {
db := r.withCtx(ctx).
Table("stock_allocations AS sa").
Joins("JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
Joins("JOIN products p ON p.id = pi.product_id").
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()).
Joins("LEFT JOIN recordings rec ON rec.id = rs.recording_id AND rec.deleted_at IS NULL").
Joins("LEFT JOIN project_chickins pc ON pc.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()).
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
Where("w.kandang_id IS NULL").
Where("COALESCE(rec.project_flock_kandangs_id, pc.project_flock_kandang_id) = ?", projectFlockKandangID).
Where("f.name IN ?", sapronakFlagsAll).
Where("pi.received_date IS NOT NULL")
db = applyDateRange(db, "pi.received_date", start, end)
return r.joinSapronakProductFlag(db, "p")
}
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) {
func mergeSapronakIncomingRows(primary []SapronakIncomingRow, extra []SapronakIncomingRow) []SapronakIncomingRow {
if len(extra) == 0 {
return primary
}
type key struct {
productID uint
flag string
}
merged := make(map[key]*SapronakIncomingRow, len(primary)+len(extra))
order := make([]key, 0, len(primary)+len(extra))
add := func(rows []SapronakIncomingRow) {
for _, row := range rows {
k := key{productID: row.ProductID, flag: row.Flag}
if existing, ok := merged[k]; ok {
existing.Qty += row.Qty
existing.Value += row.Value
if existing.ProductName == "" {
existing.ProductName = row.ProductName
}
if existing.DefaultPrice == 0 {
existing.DefaultPrice = row.DefaultPrice
}
continue
}
copyRow := row
merged[k] = &copyRow
order = append(order, k)
}
}
add(primary)
add(extra)
result := make([]SapronakIncomingRow, 0, len(order))
for _, k := range order {
result = append(result, *merged[k])
}
return result
}
func mergeSapronakDetailMaps(primary map[uint][]SapronakDetailRow, extra map[uint][]SapronakDetailRow) map[uint][]SapronakDetailRow {
if len(primary) == 0 && len(extra) == 0 {
return map[uint][]SapronakDetailRow{}
}
if len(extra) == 0 {
return primary
}
if len(primary) == 0 {
return extra
}
for productID, rows := range extra {
primary[productID] = append(primary[productID], rows...)
}
return primary
}
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) {
rows := make([]SapronakIncomingRow, 0)
db := r.incomingPurchaseBase(ctx, kandangID, start, end).Select(`
db := r.incomingPurchaseBase(ctx, projectFlockKandangID, kandangID, start, end).Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
@@ -1158,22 +1428,68 @@ func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kanda
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
if projectFlockKandangID == 0 {
return rows, nil
}
farmRows := make([]SapronakIncomingRow, 0)
farmDB := r.incomingFarmPurchaseAllocationBase(ctx, projectFlockKandangID, start, end).Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(SUM(sa.qty), 0) AS qty,
COALESCE(SUM(sa.qty * pi.price), 0) AS value,
COALESCE(p.product_price, 0) AS default_price
`)
if err := farmDB.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&farmRows).Error; err != nil {
return nil, err
}
return mergeSapronakIncomingRows(rows, farmRows), nil
}
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
return scanAndGroupDetails(
r.incomingPurchaseBase(ctx, kandangID, start, end).Select(`
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, projectFlockKandangID uint, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
rows, err := scanAndGroupDetails(
r.incomingPurchaseBase(ctx, projectFlockKandangID, kandangID, start, end).Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
pi.received_date AS date,
COALESCE(po.po_number, '') AS reference,
COALESCE(pi.total_qty,0) AS qty_in,
0 AS qty_out,
COALESCE(pi.price,0) AS price
`),
)
if err != nil {
return nil, err
}
if projectFlockKandangID == 0 {
return rows, nil
}
farmRows, err := scanAndGroupDetails(
r.incomingFarmPurchaseAllocationBase(ctx, projectFlockKandangID, start, end).Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
pi.received_date AS date,
COALESCE(po.po_number, '') AS reference,
COALESCE(SUM(sa.qty),0) AS qty_in,
0 AS qty_out,
COALESCE(pi.price,0) AS price
`).Group(`
pi.id, pi.product_id, p.name, f.name,
pi.received_date, po.po_number, pi.price
`),
)
if err != nil {
return nil, err
}
return mergeSapronakDetailMaps(rows, farmRows), nil
}
type stockLogSapronakRow struct {