Merge branch 'fix/BE/US-281-adjustment-recording-egg-mass' into 'development'

[FEAT/BE] fix bug recording and closing counting sapronak

See merge request mbugroup/lti-api!296
This commit is contained in:
Hafizh A. Y.
2026-02-03 02:33:54 +00:00
7 changed files with 919 additions and 806 deletions
@@ -234,14 +234,14 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
row.Notes = "TRANSFER STOCK"
}
}
case "pemakaian", "adjustment keluar":
case "pemakaian":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyUsed += item.QtyKeluar
row.TotalAmount += item.QtyKeluar * price
case "mutasi keluar", "penjualan":
case "adjustment keluar", "mutasi keluar", "penjualan":
price := row.UnitPrice
if price == 0 {
price = item.Harga
@@ -36,6 +36,7 @@ type ClosingRepository interface {
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, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
}
@@ -939,6 +940,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
Where("f.name IN ?", sapronakFlagsAll).
Where(`
(sa.usable_type = ? AND r.project_flock_kandangs_id = ?)
@@ -1085,12 +1087,75 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow)
}
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeAdjustment), false)
poByWarehouse := r.DB().
Table("purchase_items pi").
Select("DISTINCT ON (pi.product_warehouse_id) pi.product_warehouse_id, po.po_number, pi.received_date").
Joins("JOIN purchases po ON po.id = pi.purchase_id").
Where("pi.received_date IS NOT NULL").
Order("pi.product_warehouse_id, pi.received_date ASC")
incomingQuery := r.withCtx(ctx).
Table("adjustment_stocks AS ast").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
ast.created_at AS date,
CONCAT('ADJ-', ast.id) AS reference,
COALESCE(ast.total_qty, 0) AS qty_in,
0 AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Where("COALESCE(ast.total_qty, 0) > 0")
incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p")
incoming, err := scanAndGroupDetails(incomingQuery)
if err != nil {
return nil, nil, err
}
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
return in, out, nil
outgoingQuery := 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(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date,
COALESCE(po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN adjustment_stocks ast ON ast.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyAdjustmentOut.String()).
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN adjustment_stocks ast_in ON ast_in.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Joins("LEFT JOIN (?) pfp_po ON pfp_po.product_warehouse_id = pfp.product_warehouse_id", poByWarehouse).
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").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Where("f.name NOT IN ?", sapronakFlags(utils.FlagDOC, utils.FlagPullet)).
Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price")
outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
outgoing, err := scanAndGroupDetails(outgoingQuery)
if err != nil {
return nil, nil, err
}
return incoming, outgoing, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
@@ -1286,6 +1351,59 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF
return sales, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) {
if projectFlockKandangID == 0 {
return map[uint][]SapronakDetailRow{}, nil
}
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(
pi.received_date,
st.transfer_date,
lt.transfer_date,
ast.created_at
) AS date,
COALESCE(
po.po_number,
st.movement_number,
lt.transfer_number,
CONCAT('ADJ-', ast.id),
''
) AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(pi.price, p.product_price, 0) AS price
`).
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Where("f.name IN ?", sapronakFlagsAll).
Group(`
pw.product_id, p.name, f.name,
pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at,
po.po_number, st.movement_number, lt.transfer_number, ast.id,
pi.price, p.product_price
`)
query = r.joinSapronakProductFlag(query, "p")
return scanAndGroupDetails(query)
}
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
if len(productIDs) == 0 {
return []entity.Product{}, nil
@@ -363,7 +363,7 @@ 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.Id)
salesOutRows, err := s.Repository.FetchSapronakSalesAllocatedDetails(ctx, pfk.Id)
if err != nil {
return nil, nil, 0, 0, err
}
@@ -570,13 +570,12 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if existing.ProductName == "" {
existing.ProductName = d.ProductName
}
existing.UsageQty += d.QtyKeluar
existing.UsageValue += d.Nilai
if existing.IncomingQty >= existing.UsageQty {
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
} else {
existing.RemainingQty = 0
// Adjustment keluar should reduce stock without inflating usage-based HPP.
remaining := existing.IncomingQty - existing.UsageQty - d.QtyKeluar
if remaining < 0 {
remaining = 0
}
existing.RemainingQty = remaining
itemMap[productID] = existing
}
}