fix: chickin include stock allocation, fix calculation hpp

This commit is contained in:
Hafizh A. Y
2026-03-03 10:36:48 +07:00
parent d5a1751868
commit f6e25be76b
19 changed files with 665 additions and 59 deletions
+4 -2
View File
@@ -366,6 +366,7 @@ func countActiveUsableAllocations(ctx context.Context, db *gorm.DB, usableType s
Table("stock_allocations").
Where("usable_type = ? AND usable_id = ?", usableType, usableID).
Where("status = ?", entity.StockAllocationStatusActive).
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
Count(&count).Error
return count, err
}
@@ -376,19 +377,20 @@ func countActiveStockableAllocations(ctx context.Context, db *gorm.DB, stockable
Table("stock_allocations").
Where("stockable_type = ? AND stockable_id = ?", stockableType, stockableID).
Where("status = ?", entity.StockAllocationStatusActive).
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
Count(&count).Error
return count, err
}
func hardDeleteUsableAllocations(ctx context.Context, tx *gorm.DB, usableType string, usableID uint) error {
return tx.WithContext(ctx).
Exec("DELETE FROM stock_allocations WHERE usable_type = ? AND usable_id = ?", usableType, usableID).
Exec("DELETE FROM stock_allocations WHERE usable_type = ? AND usable_id = ? AND allocation_purpose = ?", usableType, usableID, entity.StockAllocationPurposeConsume).
Error
}
func hardDeleteStockableAllocations(ctx context.Context, tx *gorm.DB, stockableType string, stockableID uint) error {
return tx.WithContext(ctx).
Exec("DELETE FROM stock_allocations WHERE stockable_type = ? AND stockable_id = ?", stockableType, stockableID).
Exec("DELETE FROM stock_allocations WHERE stockable_type = ? AND stockable_id = ? AND allocation_purpose = ?", stockableType, stockableID, entity.StockAllocationPurposeConsume).
Error
}
+1
View File
@@ -324,6 +324,7 @@ func countActiveAllocations(ctx context.Context, db *gorm.DB, usableType string,
Table("stock_allocations").
Where("usable_type = ? AND usable_id = ?", usableType, usableID).
Where("status = ?", entity.StockAllocationStatusActive).
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
Count(&count).Error
if err != nil {
return 0, err
+185 -1
View File
@@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"log"
"math"
"os"
"sort"
"strings"
@@ -14,6 +15,7 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
@@ -181,6 +183,8 @@ func main() {
orphanPopulationRows := int64(0)
syncedPopulationQtyRows := int64(0)
syncedPopulationUsedRows := int64(0)
traceReleasedRows := int64(0)
traceInsertedRows := int64(0)
if rowsOrphan, rowsQty, rowsUsed, err := resyncProjectFlockPopulation(ctx, db, projectFlockKandangID); err != nil {
fmt.Printf("FAIL population_resync project_flock_kandang_id=%d error=%v\n", projectFlockKandangID, err)
failedApply++
@@ -196,9 +200,22 @@ func main() {
)
}
if released, inserted, err := resyncChickinTraceByProjectFlockKandang(ctx, db, fifoStockV2Svc, projectFlockKandangID); err != nil {
fmt.Printf("FAIL chickin_trace_resync project_flock_kandang_id=%d error=%v\n", projectFlockKandangID, err)
failedApply++
} else {
traceReleasedRows = released
traceInsertedRows = inserted
fmt.Printf(
"SYNC chickin_trace released=%d inserted=%d\n",
traceReleasedRows,
traceInsertedRows,
)
}
fmt.Println()
fmt.Printf(
"Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=%d failed_apply=%d population_orphan=%d population_qty_synced=%d population_used_synced=%d\n",
"Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=%d failed_apply=%d population_orphan=%d population_qty_synced=%d population_used_synced=%d trace_released=%d trace_inserted=%d\n",
len(targets),
skippedPW,
failedResolve,
@@ -207,6 +224,8 @@ func main() {
orphanPopulationRows,
syncedPopulationQtyRows,
syncedPopulationUsedRows,
traceReleasedRows,
traceInsertedRows,
)
if failedResolve > 0 || failedApply > 0 {
os.Exit(1)
@@ -448,6 +467,7 @@ func resyncProjectFlockPopulation(ctx context.Context, db *gorm.DB, projectFlock
FROM stock_allocations sa
WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
AND sa.status = 'ACTIVE'
AND sa.allocation_purpose = 'CONSUME'
GROUP BY sa.stockable_id
)
UPDATE project_flock_populations p
@@ -463,3 +483,167 @@ func resyncProjectFlockPopulation(ctx context.Context, db *gorm.DB, projectFlock
return orphanResult.RowsAffected, qtyResult.RowsAffected, usedResult.RowsAffected, nil
}
func resyncChickinTraceByProjectFlockKandang(
ctx context.Context,
db *gorm.DB,
fifoStockV2Svc commonSvc.FifoStockV2Service,
projectFlockKandangID uint,
) (int64, int64, error) {
if projectFlockKandangID == 0 {
return 0, 0, nil
}
var productWarehouseIDs []uint
if err := db.WithContext(ctx).
Table("project_chickins").
Distinct("product_warehouse_id").
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where("deleted_at IS NULL").
Order("product_warehouse_id ASC").
Pluck("product_warehouse_id", &productWarehouseIDs).Error; err != nil {
return 0, 0, err
}
if len(productWarehouseIDs) == 0 {
return 0, 0, nil
}
totalReleased := int64(0)
totalInserted := int64(0)
for _, productWarehouseID := range productWarehouseIDs {
var releasedRows int64
var insertedRows int64
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
flagGroups, err := resolveFlagGroupsByProductWarehouse(ctx, tx, productWarehouseID)
if err != nil {
return err
}
if len(flagGroups) == 0 {
return nil
}
flagGroupCode := strings.TrimSpace(flagGroups[0])
if flagGroupCode == "" {
return nil
}
released := tx.WithContext(ctx).
Table("stock_allocations").
Where("product_warehouse_id = ?", productWarehouseID).
Where("usable_type = ?", fifo.UsableKeyProjectChickin.String()).
Where("allocation_purpose = ?", entity.StockAllocationPurposeTraceChickin).
Where("status = ?", entity.StockAllocationStatusActive).
Updates(map[string]any{
"status": entity.StockAllocationStatusReleased,
"released_at": time.Now(),
"updated_at": time.Now(),
"note": "chickin_trace_reflow_reset",
})
if released.Error != nil {
return released.Error
}
releasedRows = released.RowsAffected
type chickinRow struct {
ID uint `gorm:"column:id"`
UsageQty float64 `gorm:"column:usage_qty"`
ChickIn time.Time `gorm:"column:chick_in_date"`
}
chickins := make([]chickinRow, 0)
if err := tx.WithContext(ctx).
Table("project_chickins").
Select("id, usage_qty, chick_in_date").
Where("product_warehouse_id = ?", productWarehouseID).
Where("deleted_at IS NULL").
Where("usage_qty > 0").
Order("chick_in_date ASC, id ASC").
Scan(&chickins).Error; err != nil {
return err
}
if len(chickins) == 0 {
return nil
}
gatherRows, err := fifoStockV2Svc.Gather(ctx, commonSvc.FifoStockV2GatherRequest{
FlagGroupCode: flagGroupCode,
Lane: "STOCKABLE",
AllocationPurpose: entity.StockAllocationPurposeTraceChickin,
IgnoreSourceUsed: true,
ProductWarehouseID: productWarehouseID,
Limit: 50000,
Tx: tx,
})
if err != nil {
return err
}
if len(gatherRows) == 0 {
return nil
}
type lotKey struct {
StockableType string
StockableID uint
}
remainingByLot := make(map[lotKey]float64, len(gatherRows))
for _, row := range gatherRows {
key := lotKey{StockableType: row.Ref.LegacyTypeKey, StockableID: row.Ref.ID}
remainingByLot[key] = row.AvailableQuantity
}
now := time.Now()
lotIndex := 0
for _, chickinRow := range chickins {
remaining := chickinRow.UsageQty
for remaining > 1e-6 && lotIndex < len(gatherRows) {
lot := gatherRows[lotIndex]
key := lotKey{StockableType: lot.Ref.LegacyTypeKey, StockableID: lot.Ref.ID}
available := remainingByLot[key]
if available <= 1e-6 {
lotIndex++
continue
}
portion := math.Min(remaining, available)
if portion <= 1e-6 {
lotIndex++
continue
}
insert := map[string]any{
"product_warehouse_id": productWarehouseID,
"stockable_type": lot.Ref.LegacyTypeKey,
"stockable_id": lot.Ref.ID,
"usable_type": fifo.UsableKeyProjectChickin.String(),
"usable_id": chickinRow.ID,
"qty": portion,
"status": entity.StockAllocationStatusActive,
"allocation_purpose": entity.StockAllocationPurposeTraceChickin,
"engine_version": "v2",
"flag_group_code": flagGroupCode,
"function_code": "CHICKIN_TRACE",
"created_at": now,
"updated_at": now,
}
if err := tx.WithContext(ctx).Table("stock_allocations").Create(insert).Error; err != nil {
return err
}
insertedRows++
remaining -= portion
remainingByLot[key] = available - portion
}
}
return nil
})
if err != nil {
return totalReleased, totalInserted, err
}
totalReleased += releasedRows
totalInserted += insertedRows
}
return totalReleased, totalInserted, nil
}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"math"
"os"
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
type mismatchRow struct {
ChickinID uint `gorm:"column:chickin_id"`
ProjectFlockKandang uint `gorm:"column:project_flock_kandang_id"`
ProductWarehouseID uint `gorm:"column:product_warehouse_id"`
UsageQty float64 `gorm:"column:usage_qty"`
TraceQty float64 `gorm:"column:trace_qty"`
}
func main() {
var projectFlockKandangID uint
flag.UintVar(&projectFlockKandangID, "project-flock-kandang-id", 0, "Optional project flock kandang scope")
flag.Parse()
ctx := context.Background()
db := database.Connect(config.DBHost, config.DBName)
rows, err := loadTraceMismatches(ctx, db, projectFlockKandangID)
if err != nil {
log.Fatalf("failed to load trace mismatches: %v", err)
}
activeConsumeRows, err := countActiveConsumeProjectChickin(ctx, db, projectFlockKandangID)
if err != nil {
log.Fatalf("failed to count active consume rows: %v", err)
}
fmt.Printf("Scope project_flock_kandang_id=%d\n", projectFlockKandangID)
fmt.Printf("Mismatched chickin trace rows: %d\n", len(rows))
fmt.Printf("Active PROJECT_CHICKIN consume rows: %d\n", activeConsumeRows)
if len(rows) > 0 {
for _, row := range rows {
fmt.Printf(
"MISMATCH chickin_id=%d pfk=%d pw=%d usage=%.3f trace=%.3f diff=%.3f\n",
row.ChickinID,
row.ProjectFlockKandang,
row.ProductWarehouseID,
row.UsageQty,
row.TraceQty,
row.TraceQty-row.UsageQty,
)
}
}
if len(rows) > 0 || activeConsumeRows > 0 {
os.Exit(1)
}
}
func loadTraceMismatches(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) ([]mismatchRow, error) {
query := db.WithContext(ctx).
Table("project_chickins pc").
Select(`
pc.id AS chickin_id,
pc.project_flock_kandang_id,
pc.product_warehouse_id,
COALESCE(pc.usage_qty, 0) AS usage_qty,
COALESCE(SUM(sa.qty), 0) AS trace_qty
`).
Joins(`
LEFT JOIN stock_allocations sa
ON sa.usable_type = ?
AND sa.usable_id = pc.id
AND sa.status = 'ACTIVE'
AND sa.allocation_purpose = 'TRACE_CHICKIN'
`, fifo.UsableKeyProjectChickin.String()).
Where("pc.deleted_at IS NULL").
Where("COALESCE(pc.usage_qty,0) > 0").
Group("pc.id, pc.project_flock_kandang_id, pc.product_warehouse_id, pc.usage_qty")
if projectFlockKandangID > 0 {
query = query.Where("pc.project_flock_kandang_id = ?", projectFlockKandangID)
}
rows := make([]mismatchRow, 0)
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
out := make([]mismatchRow, 0, len(rows))
for _, row := range rows {
if math.Abs(row.TraceQty-row.UsageQty) > 1e-3 {
out = append(out, row)
}
}
return out, nil
}
func countActiveConsumeProjectChickin(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (int64, error) {
q := db.WithContext(ctx).
Table("stock_allocations sa").
Joins("JOIN project_chickins pc ON pc.id = sa.usable_id").
Where("sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()).
Where("sa.status = 'ACTIVE'").
Where("sa.allocation_purpose = 'CONSUME'")
if projectFlockKandangID > 0 {
q = q.Where("pc.project_flock_kandang_id = ?", projectFlockKandangID)
}
var count int64
if err := q.Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}