mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
cmd: two steps auto transfer
This commit is contained in:
Binary file not shown.
@@ -48,9 +48,13 @@ type commandOptions struct {
|
|||||||
AllLocations bool
|
AllLocations bool
|
||||||
FarmWarehouseOverrideID uint
|
FarmWarehouseOverrideID uint
|
||||||
SkipAmbiguous bool
|
SkipAmbiguous bool
|
||||||
Output string
|
// FlagFilter is an optional set of product flag names (upper-cased).
|
||||||
ActorID uint
|
// When non-empty only products that carry at least one of these flags are
|
||||||
RunID string
|
// included. Populated from --flags="PAKAN,OVK".
|
||||||
|
FlagFilter []string
|
||||||
|
Output string
|
||||||
|
ActorID uint
|
||||||
|
RunID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// farmWarehouseEntry is a single LOKASI-type warehouse belonging to a location.
|
// farmWarehouseEntry is a single LOKASI-type warehouse belonging to a location.
|
||||||
@@ -234,7 +238,7 @@ func main() {
|
|||||||
|
|
||||||
// Step 3: load leftover stocks from extra farm warehouses that need
|
// Step 3: load leftover stocks from extra farm warehouses that need
|
||||||
// consolidation into the chosen farm warehouse.
|
// consolidation into the chosen farm warehouse.
|
||||||
extraFarmStocks, err := loadExtraFarmLeftoverStocks(ctx, db, farmMap)
|
extraFarmStocks, err := loadExtraFarmLeftoverStocks(ctx, db, farmMap, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to load extra farm leftover stocks: %v", err)
|
log.Fatalf("failed to load extra farm leftover stocks: %v", err)
|
||||||
}
|
}
|
||||||
@@ -276,10 +280,21 @@ func parseFlags() (*commandOptions, error) {
|
|||||||
"When a location has multiple LOKASI warehouses, use this warehouse id as the chosen target. "+
|
"When a location has multiple LOKASI warehouses, use this warehouse id as the chosen target. "+
|
||||||
"Stocks from the other LOKASI warehouses are also transferred to the chosen one. "+
|
"Stocks from the other LOKASI warehouses are also transferred to the chosen one. "+
|
||||||
"Requires --location-id or --location-name.")
|
"Requires --location-id or --location-name.")
|
||||||
|
var flagsRaw string
|
||||||
|
flag.StringVar(&flagsRaw, "flags", "",
|
||||||
|
"Comma-separated list of product flag names to include (e.g. PAKAN,OVK). "+
|
||||||
|
"Only products that carry at least one of these flags are transferred. "+
|
||||||
|
"Leave empty to transfer all products regardless of flags.")
|
||||||
flag.StringVar(&opts.Output, "output", outputModeTable, "Output format: table or json")
|
flag.StringVar(&opts.Output, "output", outputModeTable, "Output format: table or json")
|
||||||
flag.UintVar(&opts.ActorID, "actor-id", 1, "Actor id used for created/deleted transfers")
|
flag.UintVar(&opts.ActorID, "actor-id", 1, "Actor id used for created/deleted transfers")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
for _, f := range strings.Split(flagsRaw, ",") {
|
||||||
|
if name := strings.ToUpper(strings.TrimSpace(f)); name != "" {
|
||||||
|
opts.FlagFilter = append(opts.FlagFilter, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
opts.LocationName = strings.TrimSpace(opts.LocationName)
|
opts.LocationName = strings.TrimSpace(opts.LocationName)
|
||||||
opts.RollbackRunID = strings.TrimSpace(opts.RollbackRunID)
|
opts.RollbackRunID = strings.TrimSpace(opts.RollbackRunID)
|
||||||
opts.Output = strings.ToLower(strings.TrimSpace(opts.Output))
|
opts.Output = strings.ToLower(strings.TrimSpace(opts.Output))
|
||||||
@@ -553,6 +568,7 @@ func loadKandangLeftoverStocks(ctx context.Context, db *gorm.DB, opts *commandOp
|
|||||||
Order("l.name ASC, kw.name ASC, p.name ASC")
|
Order("l.name ASC, kw.name ASC, p.name ASC")
|
||||||
|
|
||||||
query = applyLocationFilter(query, opts, "kw")
|
query = applyLocationFilter(query, opts, "kw")
|
||||||
|
query = applyFlagFilter(query, opts)
|
||||||
|
|
||||||
var rows []row
|
var rows []row
|
||||||
if err := query.Scan(&rows).Error; err != nil {
|
if err := query.Scan(&rows).Error; err != nil {
|
||||||
@@ -581,7 +597,7 @@ func loadKandangLeftoverStocks(ctx context.Context, db *gorm.DB, opts *commandOp
|
|||||||
// loadExtraFarmLeftoverStocks loads leftover stocks from every OtherFarm
|
// loadExtraFarmLeftoverStocks loads leftover stocks from every OtherFarm
|
||||||
// warehouse in the map. These are LOKASI-type warehouses that will be
|
// warehouse in the map. These are LOKASI-type warehouses that will be
|
||||||
// consolidated into the chosen farm warehouse when --farm-warehouse-id is used.
|
// consolidated into the chosen farm warehouse when --farm-warehouse-id is used.
|
||||||
func loadExtraFarmLeftoverStocks(ctx context.Context, db *gorm.DB, farmMap map[uint]farmWarehouseInfo) ([]kandangStockRow, error) {
|
func loadExtraFarmLeftoverStocks(ctx context.Context, db *gorm.DB, farmMap map[uint]farmWarehouseInfo, opts *commandOptions) ([]kandangStockRow, error) {
|
||||||
// Collect extra farm warehouse IDs together with their location context.
|
// Collect extra farm warehouse IDs together with their location context.
|
||||||
type extraSource struct {
|
type extraSource struct {
|
||||||
LocationID uint
|
LocationID uint
|
||||||
@@ -620,7 +636,7 @@ func loadExtraFarmLeftoverStocks(ctx context.Context, db *gorm.DB, farmMap map[u
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rows []row
|
var rows []row
|
||||||
err := db.WithContext(ctx).
|
q := db.WithContext(ctx).
|
||||||
Table("product_warehouses pw").
|
Table("product_warehouses pw").
|
||||||
Select(`
|
Select(`
|
||||||
fw.id AS source_warehouse_id,
|
fw.id AS source_warehouse_id,
|
||||||
@@ -641,10 +657,9 @@ func loadExtraFarmLeftoverStocks(ctx context.Context, db *gorm.DB, farmMap map[u
|
|||||||
Joins("JOIN warehouses fw ON fw.id = pw.warehouse_id AND fw.deleted_at IS NULL").
|
Joins("JOIN warehouses fw ON fw.id = pw.warehouse_id AND fw.deleted_at IS NULL").
|
||||||
Joins("JOIN products p ON p.id = pw.product_id AND p.deleted_at IS NULL").
|
Joins("JOIN products p ON p.id = pw.product_id AND p.deleted_at IS NULL").
|
||||||
Where("fw.id IN ?", warehouseIDs).
|
Where("fw.id IN ?", warehouseIDs).
|
||||||
Where("COALESCE(pw.qty, 0) > 0").
|
Where("COALESCE(pw.qty, 0) > 0")
|
||||||
Order("fw.name ASC, p.name ASC").
|
q = applyFlagFilter(q, opts)
|
||||||
Scan(&rows).Error
|
if err := q.Order("fw.name ASC, p.name ASC").Scan(&rows).Error; err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,6 +964,22 @@ func loadRollbackDetails(ctx context.Context, db *gorm.DB, runID string) ([]roll
|
|||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// applyFlagFilter adds an EXISTS subquery that restricts results to products
|
||||||
|
// carrying at least one flag from opts.FlagFilter. When the filter is empty
|
||||||
|
// the query is returned unchanged so all products are included.
|
||||||
|
func applyFlagFilter(q *gorm.DB, opts *commandOptions) *gorm.DB {
|
||||||
|
if len(opts.FlagFilter) == 0 {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
return q.Where(`EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_id = p.id
|
||||||
|
AND f.flagable_type = 'products'
|
||||||
|
AND UPPER(f.name) IN ?
|
||||||
|
)`, opts.FlagFilter)
|
||||||
|
}
|
||||||
|
|
||||||
func applyLocationFilter(q *gorm.DB, opts *commandOptions, tableAlias string) *gorm.DB {
|
func applyLocationFilter(q *gorm.DB, opts *commandOptions, tableAlias string) *gorm.DB {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
return q
|
return q
|
||||||
|
|||||||
@@ -301,6 +301,33 @@ func TestBuildPlanSkipAmbiguousDowngradesErrorToSkipped(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── applyFlagFilter (unit-level, via buildTransferPlan) ───────────────────────
|
||||||
|
|
||||||
|
// applyFlagFilter is a DB-level filter so we test its effect indirectly: the
|
||||||
|
// flag filter is applied before rows reach buildTransferPlan, so we simulate
|
||||||
|
// by only passing stock rows that the query would have returned.
|
||||||
|
// The real guard is that loadKandangLeftoverStocks receives the filtered set.
|
||||||
|
// Here we verify that buildTransferPlan itself is agnostic to the filter and
|
||||||
|
// simply processes whatever rows it is given.
|
||||||
|
func TestBuildPlanOnlyTransfersRowsPassedToIt(t *testing.T) {
|
||||||
|
opts := &commandOptions{RunID: "test-run", FlagFilter: []string{"PAKAN"}}
|
||||||
|
farmMap := map[uint]farmWarehouseInfo{
|
||||||
|
10: {LocationID: 10, AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, ChosenID: 50, ChosenName: "Farm A"},
|
||||||
|
}
|
||||||
|
// Simulate: only PAKAN products survived the DB filter; OVK was excluded.
|
||||||
|
stocks := []kandangStockRow{
|
||||||
|
{LocationID: 10, SourceWarehouseID: 20, ProductID: 1, ProductName: "Pakan Broiler", OnHandQty: 100, LeftoverQty: 100, SourceType: sourceTypeKandang},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, groups := buildTransferPlan(opts, farmMap, stocks)
|
||||||
|
if len(groups) != 1 || len(groups[0].Rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 group with 1 row, got %d groups", len(groups))
|
||||||
|
}
|
||||||
|
if groups[0].Rows[0].ProductName != "Pakan Broiler" {
|
||||||
|
t.Errorf("unexpected product: %s", groups[0].Rows[0].ProductName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── buildTransferPlan — farm_consolidation source ────────────────────────────
|
// ── buildTransferPlan — farm_consolidation source ────────────────────────────
|
||||||
|
|
||||||
func TestBuildPlanFarmConsolidationCreatesOwnGroup(t *testing.T) {
|
func TestBuildPlanFarmConsolidationCreatesOwnGroup(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user