mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Merge branch 'cmd/consolidate-and-repoint-stocks' into 'development'
cmd: check validate and fix bug risks in commands See merge request mbugroup/lti-api!460
This commit is contained in:
@@ -30,12 +30,13 @@ type options struct {
|
||||
}
|
||||
|
||||
type duplicateGroup struct {
|
||||
WarehouseID uint `json:"warehouse_id"`
|
||||
WarehouseName string `json:"warehouse_name"`
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
AreaName string `json:"area_name"`
|
||||
LocationName string `json:"location_name"`
|
||||
WarehouseID uint `json:"warehouse_id"`
|
||||
WarehouseName string `json:"warehouse_name"`
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
AreaName string `json:"area_name"`
|
||||
LocationName string `json:"location_name"`
|
||||
ProjectFlockKandangID *uint `json:"project_flock_kandang_id,omitempty"`
|
||||
|
||||
SurvivorID uint `json:"survivor_id"`
|
||||
SurvivorQty float64 `json:"survivor_qty"`
|
||||
@@ -128,11 +129,12 @@ WITH duplicates AS (
|
||||
p.name AS product_name,
|
||||
COALESCE(a.name, 'N/A') AS area_name,
|
||||
COALESCE(l.name, 'N/A') AS location_name,
|
||||
pw.project_flock_kandang_id,
|
||||
pw.id,
|
||||
pw.qty,
|
||||
MIN(pw.id) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS survivor_id,
|
||||
COUNT(*) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS duplicate_count,
|
||||
SUM(pw.qty) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS total_qty
|
||||
MIN(pw.id) OVER (PARTITION BY pw.warehouse_id, pw.product_id, pw.project_flock_kandang_id) AS survivor_id,
|
||||
COUNT(*) OVER (PARTITION BY pw.warehouse_id, pw.product_id, pw.project_flock_kandang_id) AS duplicate_count,
|
||||
SUM(pw.qty) OVER (PARTITION BY pw.warehouse_id, pw.product_id, pw.project_flock_kandang_id) AS total_qty
|
||||
FROM product_warehouses pw
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
JOIN products p ON p.id = pw.product_id
|
||||
@@ -147,6 +149,7 @@ SELECT
|
||||
product_name,
|
||||
area_name,
|
||||
location_name,
|
||||
project_flock_kandang_id,
|
||||
survivor_id,
|
||||
(SELECT qty FROM duplicates d2 WHERE d2.id = survivor_id LIMIT 1) AS survivor_qty,
|
||||
duplicate_count - 1 AS absorbed_count,
|
||||
@@ -154,7 +157,7 @@ SELECT
|
||||
STRING_AGG(id::text, ', ' ORDER BY id::text) FILTER (WHERE id <> survivor_id) AS absorbed_ids
|
||||
FROM duplicates
|
||||
WHERE duplicate_count > 1
|
||||
GROUP BY warehouse_id, warehouse_name, product_id, product_name, area_name, location_name, survivor_id, total_qty, duplicate_count
|
||||
GROUP BY warehouse_id, warehouse_name, product_id, product_name, area_name, location_name, project_flock_kandang_id, survivor_id, total_qty, duplicate_count
|
||||
ORDER BY area_name, location_name, warehouse_name, product_name
|
||||
`, filters)
|
||||
|
||||
@@ -378,15 +381,20 @@ func renderConsolidation(mode string, groups []duplicateGroup, summary consolida
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "AREA\tLOCATION\tWAREHOUSE\tPRODUCT\tSURVIVOR_ID\tSURVIVOR_QTY\tABSORBED_COUNT\tTOTAL_MERGED_QTY\tABSORBED_IDS")
|
||||
fmt.Fprintln(w, "AREA\tLOCATION\tWAREHOUSE\tPRODUCT\tPFK_ID\tSURVIVOR_ID\tSURVIVOR_QTY\tABSORBED_COUNT\tTOTAL_MERGED_QTY\tABSORBED_IDS")
|
||||
for _, g := range groups {
|
||||
pfkID := "-"
|
||||
if g.ProjectFlockKandangID != nil {
|
||||
pfkID = fmt.Sprintf("%d", *g.ProjectFlockKandangID)
|
||||
}
|
||||
fmt.Fprintf(
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%d\t%.3f\t%d\t%.3f\t%s\n",
|
||||
"%s\t%s\t%s\t%s\t%s\t%d\t%.3f\t%d\t%.3f\t%s\n",
|
||||
g.AreaName,
|
||||
g.LocationName,
|
||||
g.WarehouseName,
|
||||
g.ProductName,
|
||||
pfkID,
|
||||
g.SurvivorID,
|
||||
g.SurvivorQty,
|
||||
g.AbsorbedCount,
|
||||
|
||||
@@ -729,6 +729,9 @@ func reflowAndRecalculateProductWarehouse(
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err)
|
||||
}
|
||||
if flagGroupCode == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||
FlagGroupCode: flagGroupCode,
|
||||
@@ -778,6 +781,9 @@ func resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, produc
|
||||
Order("rr.id ASC").
|
||||
Limit(1).
|
||||
Take(&selected).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -400,8 +400,10 @@ func buildReferencePlan(ctx context.Context, db *gorm.DB) (*referencePlan, error
|
||||
}
|
||||
|
||||
func runPrechecks(ctx context.Context, db *gorm.DB, rows []planRow, refs *referencePlan, opts *options) error {
|
||||
if err := ensureNoBlockedWarehouseRefs(ctx, db, rows, refs.BlockedWarehouseRefs); err != nil {
|
||||
return err
|
||||
if opts.DeleteWrongWarehouses {
|
||||
if err := ensureNoBlockedWarehouseRefs(ctx, db, rows, refs.BlockedWarehouseRefs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := ensureNoPurchaseItemWarehouseConflicts(ctx, db, rows); err != nil {
|
||||
return err
|
||||
@@ -813,6 +815,9 @@ func reflowAndRecalculateProductWarehouse(
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err)
|
||||
}
|
||||
if flagGroupCode == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||
FlagGroupCode: flagGroupCode,
|
||||
@@ -862,6 +867,9 @@ func resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, produc
|
||||
Order("rr.id ASC").
|
||||
Limit(1).
|
||||
Take(&selected).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ type sourceWarehouseCheck struct {
|
||||
KandangName string `json:"kandang_name"`
|
||||
SourceWarehouseID uint `json:"source_warehouse_id"`
|
||||
SourceWarehouseName string `json:"source_warehouse_name"`
|
||||
Case string `json:"case"`
|
||||
Case string `json:"case" gorm:"column:case_type"`
|
||||
DeletedAt *string `json:"deleted_at"`
|
||||
StockInProductWH float64 `json:"stock_in_product_wh"`
|
||||
ActivePurchaseItems int64 `json:"active_purchase_items"`
|
||||
@@ -145,7 +145,7 @@ func parseFlags() (*options, error) {
|
||||
}
|
||||
|
||||
func verifySourceWarehouses(ctx context.Context, db *gorm.DB, opts *options) ([]sourceWarehouseCheck, error) {
|
||||
filters := buildFilters(opts)
|
||||
filters, args := buildFilters(opts)
|
||||
query := fmt.Sprintf(`
|
||||
WITH case_a_warehouses AS (
|
||||
-- Case A: Kandang-level warehouses (type != 'LOKASI' or NULL)
|
||||
@@ -234,7 +234,7 @@ ORDER BY asw.area_name ASC, asw.kandang_location_name ASC, w.name ASC
|
||||
`, andClause(filters))
|
||||
|
||||
rows := make([]sourceWarehouseCheck, 0)
|
||||
if err := db.WithContext(ctx).Raw(query).Scan(&rows).Error; err != nil {
|
||||
if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ ORDER BY asw.area_name ASC, asw.kandang_location_name ASC, w.name ASC
|
||||
}
|
||||
|
||||
func verifyDestinationWarehouses(ctx context.Context, db *gorm.DB, opts *options, sourceChecks []sourceWarehouseCheck) ([]destinationWarehouseCheck, error) {
|
||||
filters := buildFilters(opts)
|
||||
filters, args := buildFilters(opts)
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
a.name AS area_name,
|
||||
@@ -270,13 +270,11 @@ SELECT
|
||||
COALESCE(SUM(sl.stock), 0) AS stock_logs_total,
|
||||
COUNT(DISTINCT sl.id) AS stock_logs_count
|
||||
FROM warehouses fw
|
||||
JOIN locations loc ON loc.id = fw.location_id
|
||||
JOIN areas a ON a.id = loc.area_id
|
||||
JOIN kandangs k ON k.location_id = fw.location_id AND k.deleted_at IS NULL
|
||||
JOIN locations kl ON kl.id = k.location_id
|
||||
JOIN products p ON true
|
||||
JOIN locations kl ON kl.id = fw.location_id
|
||||
JOIN areas a ON a.id = kl.area_id
|
||||
JOIN product_warehouses pw ON pw.warehouse_id = fw.id
|
||||
JOIN products p ON p.id = pw.product_id
|
||||
JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = 'products' AND UPPER(f.name) IN ('PAKAN', 'OVK')
|
||||
LEFT JOIN product_warehouses pw ON pw.warehouse_id = fw.id AND pw.product_id = p.id
|
||||
LEFT JOIN stock_logs sl ON sl.product_warehouse_id = pw.id
|
||||
WHERE fw.deleted_at IS NULL
|
||||
AND UPPER(COALESCE(fw.type, '')) = 'LOKASI'
|
||||
@@ -293,7 +291,7 @@ ORDER BY a.name ASC, kl.name ASC, fw.name ASC, p.name ASC
|
||||
`, andClause(filters))
|
||||
|
||||
rows := make([]destinationWarehouseCheck, 0)
|
||||
if err := db.WithContext(ctx).Raw(query).Scan(&rows).Error; err != nil {
|
||||
if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -371,15 +369,18 @@ func verifyOrphanedReferences(ctx context.Context, db *gorm.DB, sourceChecks []s
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func buildFilters(opts *options) []string {
|
||||
func buildFilters(opts *options) ([]string, []any) {
|
||||
filters := make([]string, 0, 2)
|
||||
args := make([]any, 0, 2)
|
||||
if opts.AreaName != "" {
|
||||
filters = append(filters, fmt.Sprintf("a.name = '%s'", opts.AreaName))
|
||||
filters = append(filters, "a.name = ?")
|
||||
args = append(args, opts.AreaName)
|
||||
}
|
||||
if opts.KandangLocationName != "" {
|
||||
filters = append(filters, fmt.Sprintf("kl.name = '%s'", opts.KandangLocationName))
|
||||
filters = append(filters, "kl.name = ?")
|
||||
args = append(args, opts.KandangLocationName)
|
||||
}
|
||||
return filters
|
||||
return filters, args
|
||||
}
|
||||
|
||||
func andClause(filters []string) string {
|
||||
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user