mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'cmd/auto-transfer-products-to-farm' into 'development'
cmd: two steps auto transfer See merge request mbugroup/lti-api!472
This commit is contained in:
Binary file not shown.
@@ -48,9 +48,13 @@ type commandOptions struct {
|
||||
AllLocations bool
|
||||
FarmWarehouseOverrideID uint
|
||||
SkipAmbiguous bool
|
||||
Output string
|
||||
ActorID uint
|
||||
RunID string
|
||||
// FlagFilter is an optional set of product flag names (upper-cased).
|
||||
// When non-empty only products that carry at least one of these flags are
|
||||
// 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.
|
||||
@@ -234,7 +238,7 @@ func main() {
|
||||
|
||||
// Step 3: load leftover stocks from extra farm warehouses that need
|
||||
// consolidation into the chosen farm warehouse.
|
||||
extraFarmStocks, err := loadExtraFarmLeftoverStocks(ctx, db, farmMap)
|
||||
extraFarmStocks, err := loadExtraFarmLeftoverStocks(ctx, db, farmMap, opts)
|
||||
if err != nil {
|
||||
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. "+
|
||||
"Stocks from the other LOKASI warehouses are also transferred to the chosen one. "+
|
||||
"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.UintVar(&opts.ActorID, "actor-id", 1, "Actor id used for created/deleted transfers")
|
||||
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.RollbackRunID = strings.TrimSpace(opts.RollbackRunID)
|
||||
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")
|
||||
|
||||
query = applyLocationFilter(query, opts, "kw")
|
||||
query = applyFlagFilter(query, opts)
|
||||
|
||||
var rows []row
|
||||
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
|
||||
// warehouse in the map. These are LOKASI-type warehouses that will be
|
||||
// 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.
|
||||
type extraSource struct {
|
||||
LocationID uint
|
||||
@@ -620,7 +636,7 @@ func loadExtraFarmLeftoverStocks(ctx context.Context, db *gorm.DB, farmMap map[u
|
||||
}
|
||||
|
||||
var rows []row
|
||||
err := db.WithContext(ctx).
|
||||
q := db.WithContext(ctx).
|
||||
Table("product_warehouses pw").
|
||||
Select(`
|
||||
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 products p ON p.id = pw.product_id AND p.deleted_at IS NULL").
|
||||
Where("fw.id IN ?", warehouseIDs).
|
||||
Where("COALESCE(pw.qty, 0) > 0").
|
||||
Order("fw.name ASC, p.name ASC").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
Where("COALESCE(pw.qty, 0) > 0")
|
||||
q = applyFlagFilter(q, opts)
|
||||
if err := q.Order("fw.name ASC, p.name ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -949,6 +964,22 @@ func loadRollbackDetails(ctx context.Context, db *gorm.DB, runID string) ([]roll
|
||||
|
||||
// ── 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 {
|
||||
if opts == nil {
|
||||
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 ────────────────────────────
|
||||
|
||||
func TestBuildPlanFarmConsolidationCreatesOwnGroup(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user