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 {
@@ -0,0 +1,208 @@
package repository
import (
"context"
"fmt"
"testing"
"time"
"github.com/glebarez/sqlite"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
func TestSapronakIncomingPurchaseQueryPartsUsesAttributedPurchasesWhenProjectFlockKandangIDsProvided(t *testing.T) {
sql, args := sapronakIncomingPurchaseQueryParts(SapronakQueryParams{
WarehouseIDs: []uint{46},
ProjectFlockKandangIDs: []uint{101},
})
if sql != sapronakIncomingPurchasesScopedSQL() {
t.Fatalf("expected scoped purchase SQL, got %q", sql)
}
if len(args) != 8 {
t.Fatalf("expected 8 argument groups, got %d", len(args))
}
}
func TestFetchSapronakIncomingIncludesAttributedFarmPurchasesAndHistoricalWarehouseFallback(t *testing.T) {
db := setupClosingRepositoryTestDB(t)
repo := NewClosingRepository(db)
ctx := context.Background()
receivedAt := time.Date(2026, 4, 1, 4, 0, 0, 0, time.UTC)
statements := []string{
`INSERT INTO warehouses (id, kandang_id) VALUES (1, NULL), (2, 59), (3, 88)`,
`INSERT INTO product_categories (id, code) VALUES (1, 'OBT'), (2, 'RAW')`,
`INSERT INTO products (id, name, product_category_id, product_price) VALUES
(10, 'MEFISTO @1 LITER', 1, 261700),
(20, 'PAKAN GROWING CRUMBLE MALINDO', 2, 15000)`,
`INSERT INTO flags (id, flagable_id, flagable_type, name) VALUES
(1, 10, 'products', 'OVK'),
(2, 10, 'products', 'OBAT')`,
`INSERT INTO purchases (id, po_number, deleted_at) VALUES (1, 'PO-LTI-0005', NULL)`,
`INSERT INTO recordings (id, project_flock_kandangs_id, deleted_at) VALUES (11, 101, NULL), (12, 999, NULL)`,
`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, usage_qty) VALUES (21, 11, 501, 150), (22, 12, 502, 10)`,
`INSERT INTO purchase_items (id, purchase_id, product_id, warehouse_id, project_flock_kandang_id, total_qty, price, received_date) VALUES
(1, 1, 10, 1, NULL, 100, 261700, '` + receivedAt.Format(time.RFC3339) + `'),
(2, 1, 20, 1, NULL, 50, 15000, '` + receivedAt.Format(time.RFC3339) + `'),
(3, 1, 20, 2, NULL, 25, 12000, '` + receivedAt.Format(time.RFC3339) + `'),
(4, 1, 10, 3, 999, 10, 261700, '` + receivedAt.Format(time.RFC3339) + `'),
(5, 1, 20, 1, NULL, 40, 15000, '` + receivedAt.Format(time.RFC3339) + `')`,
fmt.Sprintf(`INSERT INTO stock_allocations (id, product_warehouse_id, stockable_type, stockable_id, usable_type, usable_id, qty, allocation_purpose, status) VALUES
(1, 701, '%s', 1, '%s', 21, 100, 'CONSUME', 'ACTIVE'),
(2, 702, '%s', 2, '%s', 21, 50, 'CONSUME', 'ACTIVE'),
(3, 703, '%s', 5, '%s', 22, 40, 'CONSUME', 'ACTIVE')`,
fifo.StockableKeyPurchaseItems.String(),
fifo.UsableKeyRecordingStock.String(),
fifo.StockableKeyPurchaseItems.String(),
fifo.UsableKeyRecordingStock.String(),
fifo.StockableKeyPurchaseItems.String(),
fifo.UsableKeyRecordingStock.String(),
),
}
for _, stmt := range statements {
if err := db.Exec(stmt).Error; err != nil {
t.Fatalf("failed seeding schema: %v", err)
}
}
rows, err := repo.FetchSapronakIncoming(ctx, 101, 59, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(rows) != 2 {
t.Fatalf("expected 2 sapronak rows, got %d", len(rows))
}
byProduct := make(map[uint]SapronakIncomingRow, len(rows))
for _, row := range rows {
byProduct[row.ProductID] = row
}
if got := byProduct[10]; got.ProductID == 0 || got.Flag != "OVK" || got.Qty != 100 {
t.Fatalf("expected OVK farm purchase qty 100 for product 10, got %+v", got)
}
if got := byProduct[20]; got.ProductID == 0 || got.Flag != "PAKAN" || got.Qty != 75 {
t.Fatalf("expected PAKAN total qty 75 including farm allocated qty 50 and kandang receipt qty 25, got %+v", got)
}
}
func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
if err != nil {
t.Fatalf("failed opening sqlite db: %v", err)
}
statements := []string{
`CREATE TABLE warehouses (
id INTEGER PRIMARY KEY,
kandang_id INTEGER NULL
)`,
`CREATE TABLE product_categories (
id INTEGER PRIMARY KEY,
code TEXT NOT NULL
)`,
`CREATE TABLE uoms (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)`,
`CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
product_category_id INTEGER NULL,
uom_id INTEGER NULL,
product_price NUMERIC(15,3) NOT NULL DEFAULT 0
)`,
`CREATE TABLE flags (
id INTEGER PRIMARY KEY,
flagable_id INTEGER NOT NULL,
flagable_type TEXT NOT NULL,
name TEXT NOT NULL
)`,
`CREATE TABLE purchases (
id INTEGER PRIMARY KEY,
po_number TEXT NULL,
notes TEXT NULL,
deleted_at TIMESTAMP NULL
)`,
`CREATE TABLE purchase_items (
id INTEGER PRIMARY KEY,
purchase_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
warehouse_id INTEGER NOT NULL,
project_flock_kandang_id INTEGER NULL,
total_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
price NUMERIC(15,3) NOT NULL DEFAULT 0,
received_date TIMESTAMP NULL
)`,
`CREATE TABLE recordings (
id INTEGER PRIMARY KEY,
project_flock_kandangs_id INTEGER NOT NULL,
deleted_at TIMESTAMP NULL
)`,
`CREATE TABLE recording_stocks (
id INTEGER PRIMARY KEY,
recording_id INTEGER NOT NULL,
product_warehouse_id INTEGER NOT NULL,
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0
)`,
`CREATE TABLE project_chickins (
id INTEGER PRIMARY KEY,
project_flock_kandang_id INTEGER NOT NULL
)`,
`CREATE TABLE stock_allocations (
id INTEGER PRIMARY KEY,
product_warehouse_id INTEGER NOT NULL,
stockable_type TEXT NOT NULL,
stockable_id INTEGER NOT NULL,
usable_type TEXT NOT NULL,
usable_id INTEGER NOT NULL,
qty NUMERIC(15,3) NOT NULL DEFAULT 0,
allocation_purpose TEXT NOT NULL,
status TEXT NOT NULL
)`,
`CREATE TABLE product_warehouses (
id INTEGER PRIMARY KEY,
product_id INTEGER NOT NULL,
warehouse_id INTEGER NOT NULL,
project_flock_kandang_id INTEGER NULL
)`,
`CREATE TABLE stock_transfers (
id INTEGER PRIMARY KEY,
from_warehouse_id INTEGER NULL,
to_warehouse_id INTEGER NULL,
transfer_date TIMESTAMP NULL,
movement_number TEXT NULL,
reason TEXT NULL
)`,
`CREATE TABLE stock_transfer_details (
id INTEGER PRIMARY KEY,
stock_transfer_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
dest_product_warehouse_id INTEGER NULL,
source_product_warehouse_id INTEGER NULL,
total_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0
)`,
`CREATE TABLE adjustment_stocks (
id INTEGER PRIMARY KEY,
product_warehouse_id INTEGER NOT NULL,
total_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
adj_number TEXT NULL,
created_at TIMESTAMP NULL
)`,
}
for _, stmt := range statements {
if err := db.Exec(stmt).Error; err != nil {
t.Fatalf("failed preparing schema: %v", err)
}
}
return db
}
@@ -383,7 +383,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
} else {
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)
@@ -474,7 +474,7 @@ func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID u
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
} else {
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)
@@ -1156,7 +1156,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
chickenDepletion = 0
}
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age)
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age)
if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording
}
@@ -382,11 +382,11 @@ func buildSapronakDetails(
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
// Filter by project flock period (start = first chickin or pfk created_at, end = closed_at if any).
startDate, endDate := sapronakPeriodRange(pfk)
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId, startDate, endDate)
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.Id, pfk.KandangId, startDate, endDate)
if err != nil {
return nil, nil, 0, 0, err
}
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId, startDate, endDate)
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.Id, pfk.KandangId, startDate, endDate)
if err != nil {
return nil, nil, 0, 0, err
}