mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'codex/sales-at-farm-level' into 'development'
codex/fix: show farm stock usage on closing page See merge request mbugroup/lti-api!384
This commit is contained in:
@@ -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] = ©Row
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user