Merge branch 'revert-915302c4' into 'dev/fifo-v2'

Revert "Merge branch 'fix/implement-fifo-v2' into 'dev/fifo-v2'"

See merge request mbugroup/lti-api!342
This commit is contained in:
Hafizh A. Y.
2026-02-27 09:37:50 +00:00
28 changed files with 163 additions and 1468 deletions
@@ -10,11 +10,6 @@ type FifoStockV2Service = fifoStockV2.Service
type FifoStockV2Lane = fifoStockV2.Lane type FifoStockV2Lane = fifoStockV2.Lane
const (
FifoStockV2LaneStockable FifoStockV2Lane = fifoStockV2.LaneStockable
FifoStockV2LaneUsable FifoStockV2Lane = fifoStockV2.LaneUsable
)
type FifoStockV2Ref = fifoStockV2.Ref type FifoStockV2Ref = fifoStockV2.Ref
type FifoStockV2GatherRequest = fifoStockV2.GatherRequest type FifoStockV2GatherRequest = fifoStockV2.GatherRequest
@@ -1,144 +0,0 @@
package service
import (
"context"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type FifoStockV2RouteRule struct {
FlagGroupCode string `gorm:"column:flag_group_code"`
Lane string `gorm:"column:lane"`
FunctionCode string `gorm:"column:function_code"`
SourceTable string `gorm:"column:source_table"`
LegacyTypeKey string `gorm:"column:legacy_type_key"`
AllowPendingDefault bool `gorm:"column:allow_pending_default"`
}
func ResolveFifoStockV2RouteByProductIDAndLane(
ctx context.Context,
db *gorm.DB,
productID uint,
functionCode string,
lane FifoStockV2Lane,
) (*FifoStockV2RouteRule, error) {
rows, err := resolveFifoStockV2RoutesByProductID(ctx, db, productID, functionCode, lane)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
selected := rows[0]
return &selected, nil
}
func ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx context.Context,
db *gorm.DB,
productWarehouseID uint,
functionCode string,
lane FifoStockV2Lane,
) (*FifoStockV2RouteRule, error) {
rows, err := resolveFifoStockV2RoutesByProductWarehouseID(ctx, db, productWarehouseID, functionCode, lane)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
selected := rows[0]
return &selected, nil
}
func resolveFifoStockV2RoutesByProductID(
ctx context.Context,
db *gorm.DB,
productID uint,
functionCode string,
lane FifoStockV2Lane,
) ([]FifoStockV2RouteRule, error) {
normalizedCode := strings.ToUpper(strings.TrimSpace(functionCode))
if db == nil || productID == 0 || normalizedCode == "" {
return []FifoStockV2RouteRule{}, nil
}
query := db.WithContext(ctx).
Table("fifo_stock_v2_route_rules rr").
Select("rr.flag_group_code, rr.lane, rr.function_code, rr.source_table, rr.legacy_type_key, rr.allow_pending_default").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE").
Where("rr.function_code = ?", normalizedCode).
Where(`
EXISTS (
SELECT 1
FROM flags f
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE f.flagable_type = ?
AND f.flagable_id = ?
AND fm.flag_group_code = rr.flag_group_code
)
`, entity.FlagableTypeProduct, productID)
if lane == FifoStockV2LaneStockable || lane == FifoStockV2LaneUsable {
query = query.Where("rr.lane = ?", string(lane))
}
var rows []FifoStockV2RouteRule
err := query.
Order("CASE WHEN rr.source_table = 'adjustment_stocks' THEN 0 ELSE 1 END ASC").
Order("rr.id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
func resolveFifoStockV2RoutesByProductWarehouseID(
ctx context.Context,
db *gorm.DB,
productWarehouseID uint,
functionCode string,
lane FifoStockV2Lane,
) ([]FifoStockV2RouteRule, error) {
normalizedCode := strings.ToUpper(strings.TrimSpace(functionCode))
if db == nil || productWarehouseID == 0 || normalizedCode == "" {
return []FifoStockV2RouteRule{}, nil
}
query := db.WithContext(ctx).
Table("fifo_stock_v2_route_rules rr").
Select("rr.flag_group_code, rr.lane, rr.function_code, rr.source_table, rr.legacy_type_key, rr.allow_pending_default").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE").
Where("rr.function_code = ?", normalizedCode).
Where(`
EXISTS (
SELECT 1
FROM product_warehouses pw
JOIN flags f ON f.flagable_type = ? AND f.flagable_id = pw.product_id
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE pw.id = ?
AND fm.flag_group_code = rr.flag_group_code
)
`, entity.FlagableTypeProduct, productWarehouseID)
if lane == FifoStockV2LaneStockable || lane == FifoStockV2LaneUsable {
query = query.Where("rr.lane = ?", string(lane))
}
var rows []FifoStockV2RouteRule
err := query.
Order("CASE WHEN rr.source_table = 'adjustment_stocks' THEN 0 ELSE 1 END ASC").
Order("rr.id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
@@ -16,7 +16,6 @@ SET
INSERT INTO fifo_stock_v2_flag_members(flag_name, flag_group_code, priority) INSERT INTO fifo_stock_v2_flag_members(flag_name, flag_group_code, priority)
VALUES VALUES
('AYAM', 'AYAM', 5),
('DOC', 'AYAM', 10), ('DOC', 'AYAM', 10),
('PULLET', 'AYAM', 20), ('PULLET', 'AYAM', 20),
('LAYER', 'AYAM', 30), ('LAYER', 'AYAM', 30),
@@ -1,57 +0,0 @@
BEGIN;
UPDATE fifo_stock_v2_flag_members
SET
is_active = FALSE,
updated_at = NOW()
WHERE flag_name = 'AYAM'
AND flag_group_code = 'AYAM';
INSERT INTO fifo_stock_v2_route_rules(
flag_group_code,
lane,
function_code,
source_table,
source_id_column,
product_warehouse_col,
quantity_col,
used_quantity_col,
pending_quantity_col,
scope_sql,
legacy_type_key,
allow_pending_default,
is_active
)
VALUES
('AYAM', 'USABLE', 'TRANSFER_TO_LAYING_OUT', 'laying_transfer_sources', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_usage_qty', 'deleted_at IS NULL', 'TRANSFERTOLAYING_OUT', TRUE, TRUE)
ON CONFLICT (flag_group_code, lane, function_code, source_table) DO UPDATE
SET
source_id_column = EXCLUDED.source_id_column,
product_warehouse_col = EXCLUDED.product_warehouse_col,
quantity_col = EXCLUDED.quantity_col,
used_quantity_col = EXCLUDED.used_quantity_col,
pending_quantity_col = EXCLUDED.pending_quantity_col,
scope_sql = EXCLUDED.scope_sql,
legacy_type_key = EXCLUDED.legacy_type_key,
allow_pending_default = EXCLUDED.allow_pending_default,
is_active = TRUE,
updated_at = NOW();
UPDATE fifo_stock_v2_overconsume_rules
SET
is_active = TRUE
WHERE lane = 'USABLE'
AND function_code = 'TRANSFER_TO_LAYING_OUT';
INSERT INTO fifo_stock_v2_overconsume_rules(flag_group_code, function_code, lane, allow_overconsume, priority, reason, is_active)
SELECT NULL, 'TRANSFER_TO_LAYING_OUT', 'USABLE', FALSE, 50, 'fifo_v2_exception_transfer_laying_block', TRUE
WHERE NOT EXISTS (
SELECT 1
FROM fifo_stock_v2_overconsume_rules
WHERE flag_group_code IS NULL
AND function_code = 'TRANSFER_TO_LAYING_OUT'
AND lane = 'USABLE'
AND reason = 'fifo_v2_exception_transfer_laying_block'
);
COMMIT;
@@ -1,205 +0,0 @@
BEGIN;
INSERT INTO fifo_stock_v2_flag_members(flag_name, flag_group_code, priority, is_active)
VALUES
('AYAM', 'AYAM', 5, TRUE)
ON CONFLICT (flag_name) DO UPDATE
SET
flag_group_code = EXCLUDED.flag_group_code,
priority = EXCLUDED.priority,
is_active = TRUE,
updated_at = NOW();
WITH desired_rules AS (
SELECT * FROM (
VALUES
-- AYAM STOCKABLE
('AYAM', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'ADJUSTMENT_IN', TRUE, TRUE),
('AYAM', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details', 'id', 'dest_product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'STOCK_TRANSFER_IN', TRUE, TRUE),
('AYAM', 'STOCKABLE', 'PURCHASE_IN', 'purchase_items', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'PURCHASE_ITEMS', TRUE, TRUE),
('AYAM', 'STOCKABLE', 'TRANSFER_TO_LAYING_IN', 'laying_transfer_targets', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'TRANSFERTOLAYING_IN', TRUE, TRUE),
-- AYAM USABLE
('AYAM', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'ADJUSTMENT_OUT', TRUE, TRUE),
('AYAM', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details', 'id', 'source_product_warehouse_id', 'usage_qty', NULL, 'pending_qty', 'deleted_at IS NULL', 'STOCK_TRANSFER_OUT', TRUE, TRUE),
('AYAM', 'USABLE', 'CHICKIN_OUT', 'project_chickins', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_usage_qty', 'deleted_at IS NULL', 'PROJECT_CHICKIN', TRUE, TRUE),
('AYAM', 'USABLE', 'RECORDING_DEPLETION_OUT', 'recording_depletions', 'id', 'source_product_warehouse_id', 'qty', NULL, 'pending_qty', NULL, 'RECORDING_DEPLETION', TRUE, TRUE),
('AYAM', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'MARKETING_DELIVERY', TRUE, TRUE),
-- AFKIR/CULLING/MATI STOCKABLE
('AFKIR_CULLING_MATI', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'ADJUSTMENT_IN', TRUE, TRUE),
('AFKIR_CULLING_MATI', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details', 'id', 'dest_product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'STOCK_TRANSFER_IN', TRUE, TRUE),
('AFKIR_CULLING_MATI', 'STOCKABLE', 'RECORDING_DEPLETION_IN', 'recording_depletions', 'id', 'product_warehouse_id', 'qty', NULL, NULL, NULL, 'RECORDING_DEPLETION', TRUE, TRUE),
-- AFKIR/CULLING/MATI USABLE
('AFKIR_CULLING_MATI', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'ADJUSTMENT_OUT', TRUE, TRUE),
('AFKIR_CULLING_MATI', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details', 'id', 'source_product_warehouse_id', 'usage_qty', NULL, 'pending_qty', 'deleted_at IS NULL', 'STOCK_TRANSFER_OUT', TRUE, TRUE),
('AFKIR_CULLING_MATI', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'MARKETING_DELIVERY', TRUE, TRUE),
-- PAKAN STOCKABLE
('PAKAN', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'ADJUSTMENT_IN', TRUE, TRUE),
('PAKAN', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details', 'id', 'dest_product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'STOCK_TRANSFER_IN', TRUE, TRUE),
('PAKAN', 'STOCKABLE', 'PURCHASE_IN', 'purchase_items', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'PURCHASE_ITEMS', TRUE, TRUE),
-- PAKAN USABLE
('PAKAN', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'ADJUSTMENT_OUT', TRUE, TRUE),
('PAKAN', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details', 'id', 'source_product_warehouse_id', 'usage_qty', NULL, 'pending_qty', 'deleted_at IS NULL', 'STOCK_TRANSFER_OUT', TRUE, TRUE),
('PAKAN', 'USABLE', 'RECORDING_STOCK_OUT', 'recording_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'RECORDING_STOCK', TRUE, TRUE),
('PAKAN', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'MARKETING_DELIVERY', TRUE, TRUE),
-- OVK STOCKABLE
('OVK', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'ADJUSTMENT_IN', TRUE, TRUE),
('OVK', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details', 'id', 'dest_product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'STOCK_TRANSFER_IN', TRUE, TRUE),
('OVK', 'STOCKABLE', 'PURCHASE_IN', 'purchase_items', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'PURCHASE_ITEMS', TRUE, TRUE),
-- OVK USABLE
('OVK', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'ADJUSTMENT_OUT', TRUE, TRUE),
('OVK', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details', 'id', 'source_product_warehouse_id', 'usage_qty', NULL, 'pending_qty', 'deleted_at IS NULL', 'STOCK_TRANSFER_OUT', TRUE, TRUE),
('OVK', 'USABLE', 'RECORDING_STOCK_OUT', 'recording_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'RECORDING_STOCK', TRUE, TRUE),
('OVK', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'MARKETING_DELIVERY', TRUE, TRUE),
-- TELUR STOCKABLE
('TELUR', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'ADJUSTMENT_IN', TRUE, TRUE),
('TELUR', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details', 'id', 'dest_product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'STOCK_TRANSFER_IN', TRUE, TRUE),
('TELUR', 'STOCKABLE', 'RECORDING_EGG_IN', 'recording_eggs', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'RECORDING_EGG', TRUE, TRUE),
-- TELUR USABLE
('TELUR', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'ADJUSTMENT_OUT', TRUE, TRUE),
('TELUR', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details', 'id', 'source_product_warehouse_id', 'usage_qty', NULL, 'pending_qty', 'deleted_at IS NULL', 'STOCK_TRANSFER_OUT', TRUE, TRUE),
('TELUR', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'MARKETING_DELIVERY', TRUE, TRUE),
-- TELUR_GRADE STOCKABLE
('TELUR_GRADE', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'ADJUSTMENT_IN', TRUE, TRUE),
('TELUR_GRADE', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details', 'id', 'dest_product_warehouse_id', 'total_qty', 'total_used', NULL, 'deleted_at IS NULL', 'STOCK_TRANSFER_IN', TRUE, TRUE),
('TELUR_GRADE', 'STOCKABLE', 'RECORDING_EGG_IN', 'recording_eggs', 'id', 'product_warehouse_id', 'total_qty', 'total_used', NULL, NULL, 'RECORDING_EGG', TRUE, TRUE),
-- TELUR_GRADE USABLE
('TELUR_GRADE', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'ADJUSTMENT_OUT', TRUE, TRUE),
('TELUR_GRADE', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details', 'id', 'source_product_warehouse_id', 'usage_qty', NULL, 'pending_qty', 'deleted_at IS NULL', 'STOCK_TRANSFER_OUT', TRUE, TRUE),
('TELUR_GRADE', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_qty', NULL, 'MARKETING_DELIVERY', TRUE, TRUE)
) AS v(
flag_group_code,
lane,
function_code,
source_table,
source_id_column,
product_warehouse_col,
quantity_col,
used_quantity_col,
pending_quantity_col,
scope_sql,
legacy_type_key,
allow_pending_default,
is_active
)
)
INSERT INTO fifo_stock_v2_route_rules(
flag_group_code,
lane,
function_code,
source_table,
source_id_column,
product_warehouse_col,
quantity_col,
used_quantity_col,
pending_quantity_col,
scope_sql,
legacy_type_key,
allow_pending_default,
is_active
)
SELECT
flag_group_code,
lane,
function_code,
source_table,
source_id_column,
product_warehouse_col,
quantity_col,
used_quantity_col,
pending_quantity_col,
scope_sql,
legacy_type_key,
allow_pending_default,
is_active
FROM desired_rules
ON CONFLICT (flag_group_code, lane, function_code, source_table) DO UPDATE
SET
source_id_column = EXCLUDED.source_id_column,
product_warehouse_col = EXCLUDED.product_warehouse_col,
quantity_col = EXCLUDED.quantity_col,
used_quantity_col = EXCLUDED.used_quantity_col,
pending_quantity_col = EXCLUDED.pending_quantity_col,
scope_sql = EXCLUDED.scope_sql,
legacy_type_key = EXCLUDED.legacy_type_key,
allow_pending_default = EXCLUDED.allow_pending_default,
is_active = EXCLUDED.is_active,
updated_at = NOW();
WITH desired_rules AS (
SELECT * FROM (
VALUES
('AYAM', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks'),
('AYAM', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details'),
('AYAM', 'STOCKABLE', 'PURCHASE_IN', 'purchase_items'),
('AYAM', 'STOCKABLE', 'TRANSFER_TO_LAYING_IN', 'laying_transfer_targets'),
('AYAM', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks'),
('AYAM', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details'),
('AYAM', 'USABLE', 'CHICKIN_OUT', 'project_chickins'),
('AYAM', 'USABLE', 'RECORDING_DEPLETION_OUT', 'recording_depletions'),
('AYAM', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products'),
('AFKIR_CULLING_MATI', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks'),
('AFKIR_CULLING_MATI', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details'),
('AFKIR_CULLING_MATI', 'STOCKABLE', 'RECORDING_DEPLETION_IN', 'recording_depletions'),
('AFKIR_CULLING_MATI', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks'),
('AFKIR_CULLING_MATI', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details'),
('AFKIR_CULLING_MATI', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products'),
('PAKAN', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks'),
('PAKAN', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details'),
('PAKAN', 'STOCKABLE', 'PURCHASE_IN', 'purchase_items'),
('PAKAN', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks'),
('PAKAN', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details'),
('PAKAN', 'USABLE', 'RECORDING_STOCK_OUT', 'recording_stocks'),
('PAKAN', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products'),
('OVK', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks'),
('OVK', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details'),
('OVK', 'STOCKABLE', 'PURCHASE_IN', 'purchase_items'),
('OVK', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks'),
('OVK', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details'),
('OVK', 'USABLE', 'RECORDING_STOCK_OUT', 'recording_stocks'),
('OVK', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products'),
('TELUR', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks'),
('TELUR', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details'),
('TELUR', 'STOCKABLE', 'RECORDING_EGG_IN', 'recording_eggs'),
('TELUR', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks'),
('TELUR', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details'),
('TELUR', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products'),
('TELUR_GRADE', 'STOCKABLE', 'ADJUSTMENT_IN', 'adjustment_stocks'),
('TELUR_GRADE', 'STOCKABLE', 'STOCK_TRANSFER_IN', 'stock_transfer_details'),
('TELUR_GRADE', 'STOCKABLE', 'RECORDING_EGG_IN', 'recording_eggs'),
('TELUR_GRADE', 'USABLE', 'ADJUSTMENT_OUT', 'adjustment_stocks'),
('TELUR_GRADE', 'USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details'),
('TELUR_GRADE', 'USABLE', 'MARKETING_OUT', 'marketing_delivery_products')
) AS v(flag_group_code, lane, function_code, source_table)
)
UPDATE fifo_stock_v2_route_rules rr
SET
is_active = FALSE,
updated_at = NOW()
WHERE rr.flag_group_code IN ('AYAM', 'AFKIR_CULLING_MATI', 'PAKAN', 'OVK', 'TELUR', 'TELUR_GRADE')
AND NOT EXISTS (
SELECT 1
FROM desired_rules d
WHERE d.flag_group_code = rr.flag_group_code
AND d.lane = rr.lane
AND d.function_code = rr.function_code
AND d.source_table = rr.source_table
);
UPDATE fifo_stock_v2_overconsume_rules
SET
is_active = FALSE
WHERE lane = 'USABLE'
AND function_code = 'TRANSFER_TO_LAYING_OUT';
COMMIT;
+1 -1
View File
@@ -198,7 +198,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 7500, Price: 7500,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagAyam}, Flags: []utils.FlagType{utils.FlagDOC, utils.FlagPullet, utils.FlagLayer},
IsVisible: true, IsVisible: true,
}, },
{ {
@@ -58,17 +58,17 @@ type SapronakReportDTO struct {
// Simplified view for project-level sapronak response // Simplified view for project-level sapronak response
type SapronakCategoryRowDTO struct { type SapronakCategoryRowDTO struct {
ID int `json:"id"` ID int `json:"id"`
Date string `json:"date"` Date string `json:"date"`
ReferenceNumber string `json:"reference_number"` ReferenceNumber string `json:"reference_number"`
QtyIn float64 `json:"qty_in"` QtyIn float64 `json:"qty_in"`
QtyOut float64 `json:"qty_out"` QtyOut float64 `json:"qty_out"`
QtyUsed float64 `json:"qty_used"` QtyUsed float64 `json:"qty_used"`
Description string `json:"description"` Description string `json:"description"`
ProductCategory []string `json:"product_category"` ProductCategory []string `json:"product_category"`
UnitPrice float64 `json:"unit_price"` UnitPrice float64 `json:"unit_price"`
TotalAmount float64 `json:"total_amount"` TotalAmount float64 `json:"total_amount"`
Notes string `json:"notes"` Notes string `json:"notes"`
} }
type SapronakCategoryTotalDTO struct { type SapronakCategoryTotalDTO struct {
@@ -148,7 +148,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
normalizeFlag := func(raw string) string { normalizeFlag := func(raw string) string {
normalized := strings.ToUpper(strings.TrimSpace(raw)) normalized := strings.ToUpper(strings.TrimSpace(raw))
if normalized == "AYAM" || normalized == "PULLET" { if normalized == "PULLET" {
return "DOC" return "DOC"
} }
return normalized return normalized
@@ -177,7 +177,6 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
flagOrder := map[string]int{ flagOrder := map[string]int{
"AYAM": 0,
"DOC": 0, "DOC": 0,
"PAKAN": 0, "PAKAN": 0,
"OVK": 0, "OVK": 0,
@@ -446,7 +446,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -459,7 +459,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -495,7 +495,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -508,7 +508,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -545,7 +545,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -558,7 +558,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -595,7 +595,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -608,7 +608,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -645,7 +645,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -658,7 +658,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -685,7 +685,7 @@ WHERE pw.warehouse_id IN ?
FROM flags f FROM flags f
WHERE f.flagable_id = pw.product_id WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products' AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('AYAM', 'DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
) )
` `
@@ -702,7 +702,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -715,7 +715,7 @@ SELECT
f.name, f.name,
' ' ORDER BY ' ' ORDER BY
CASE CASE
WHEN UPPER(f.name) IN ('AYAM', 'DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1 ELSE 1
END, END,
f.name f.name
@@ -743,7 +743,7 @@ WHERE pw.project_flock_kandang_id IN ?
FROM flags f FROM flags f
WHERE f.flagable_id = pw.product_id WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products' AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('AYAM', 'DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
) )
` `
) )
@@ -796,9 +796,9 @@ func sapronakFlags(flags ...utils.FlagType) []string {
} }
var ( var (
sapronakFlagsAll = sapronakFlags(utils.FlagAyam, utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet) sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK) sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
sapronakFlagsChickin = sapronakFlags(utils.FlagAyam, utils.FlagDOC, utils.FlagPullet) sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
) )
func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB { func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB {
@@ -808,8 +808,7 @@ func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlia
Where("flagable_type = ?", entity.FlagableTypeProduct). Where("flagable_type = ?", entity.FlagableTypeProduct).
Where("name IN ?", sapronakFlagsAll). Where("name IN ?", sapronakFlagsAll).
Order(fmt.Sprintf( Order(fmt.Sprintf(
"flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 WHEN name = '%s' THEN 5 ELSE 6 END", "flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 ELSE 5 END",
utils.FlagAyam,
utils.FlagDOC, utils.FlagDOC,
utils.FlagPullet, utils.FlagPullet,
utils.FlagPakan, utils.FlagPakan,
@@ -1239,7 +1238,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Where("f.name NOT IN ?", sapronakFlags(utils.FlagAyam, utils.FlagDOC, utils.FlagPullet)). 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") 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") outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
outgoing, err := scanAndGroupDetails(outgoingQuery) outgoing, err := scanAndGroupDetails(outgoingQuery)
@@ -856,7 +856,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
// FeedUsedPerHead: feedUsedPerHead, // FeedUsedPerHead: feedUsedPerHead,
} }
chickenFlagNames := []string{string(utils.FlagAyam), string(utils.FlagPullet), string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagLayer)} chickenFlagNames := []string{string(utils.FlagPullet), string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagLayer)}
chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames) chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err)
@@ -885,7 +885,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
chickenDepletion = 0 chickenDepletion = 0
} }
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age) chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age)
if fcrActFromRecording != nil { if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording chickenPerformance.FcrAct = *fcrActFromRecording
} }
@@ -432,8 +432,8 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
return true return true
} }
candidate := strings.ToUpper(f) candidate := strings.ToUpper(f)
if filterFlag == "AYAM" || filterFlag == "DOC" || filterFlag == "PULLET" { if filterFlag == "DOC" || filterFlag == "PULLET" {
return candidate == "AYAM" || candidate == "DOC" || candidate == "PULLET" return candidate == "DOC" || candidate == "PULLET"
} }
return candidate == filterFlag return candidate == filterFlag
} }
@@ -474,8 +474,7 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if !isLaying { if !isLaying {
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows)) filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
for _, row := range chickinUsageRows { for _, row := range chickinUsageRows {
flag := strings.ToUpper(row.Flag) if strings.ToUpper(row.Flag) == "DOC" {
if flag == "AYAM" || flag == "DOC" {
filteredUsage = append(filteredUsage, row) filteredUsage = append(filteredUsage, row)
} }
} }
@@ -484,8 +483,7 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows)) filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
for pid, rows := range chickinUsageDetailsRows { for pid, rows := range chickinUsageDetailsRows {
for _, d := range rows { for _, d := range rows {
flag := strings.ToUpper(d.Flag) if strings.ToUpper(d.Flag) == "DOC" {
if flag == "AYAM" || flag == "DOC" {
filteredDetail[pid] = append(filteredDetail[pid], d) filteredDetail[pid] = append(filteredDetail[pid], d)
} }
} }
@@ -5,5 +5,5 @@ type CountSapronakQuery struct {
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"` KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
Status string `query:"status" validate:"omitempty,oneof=active closing all"` Status string `query:"status" validate:"omitempty,oneof=active closing all"`
Flag string `query:"flag" validate:"omitempty,oneof=AYAM DOC OVK PAKAN PULLET ayam doc ovk pakan pullet"` Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN PULLET doc ovk pakan pullet"`
} }
@@ -26,49 +26,12 @@ func NewConstantRepository(db *gorm.DB) ConstantRepository {
} }
func (r *ConstantRepositoryImpl) GetConstants() (map[string]interface{}, error) { func (r *ConstantRepositoryImpl) GetConstants() (map[string]interface{}, error) {
flagSet := make(map[string]struct{}) flagList := make([]string, 0)
for _, f := range utils.AllowedFlagTypes(utils.FlagGroupProduct) { for f := range utils.AllFlagTypes() {
flagSet[string(f)] = struct{}{} flagList = append(flagList, string(f))
}
for _, f := range utils.AllowedFlagTypes(utils.FlagGroupNonstock) {
flagSet[string(f)] = struct{}{}
}
flagSet[string(utils.FlagIsActive)] = struct{}{}
flagList := make([]string, 0, len(flagSet))
for f := range flagSet {
flagList = append(flagList, f)
} }
sort.Strings(flagList) sort.Strings(flagList)
legacyFlagAliasesRaw := utils.LegacyFlagTypeAliases()
legacyFlagAliases := make(map[string]string, len(legacyFlagAliasesRaw))
for legacy, canonical := range legacyFlagAliasesRaw {
legacyFlagAliases[string(legacy)] = string(canonical)
}
productFlagOptionsRaw := utils.ProductFlagOptions()
productFlagOptions := make([]map[string]interface{}, 0, len(productFlagOptionsRaw))
productMainFlags := make([]string, 0, len(productFlagOptionsRaw))
productSubFlagToFlag := make(map[string]string)
for _, option := range productFlagOptionsRaw {
flag := string(option.Flag)
productMainFlags = append(productMainFlags, flag)
subFlags := make([]string, len(option.SubFlags))
for i, subFlag := range option.SubFlags {
subFlagStr := string(subFlag)
subFlags[i] = subFlagStr
productSubFlagToFlag[subFlagStr] = flag
}
productFlagOptions = append(productFlagOptions, map[string]interface{}{
"flag": flag,
"sub_flags": subFlags,
"allow_without_sub_flag": option.AllowWithoutSubFlag,
})
}
type approvalStepConstant struct { type approvalStepConstant struct {
StepNumber uint16 `json:"step_number"` StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"` StepName string `json:"step_name"`
@@ -115,13 +78,7 @@ func (r *ConstantRepositoryImpl) GetConstants() (map[string]interface{}, error)
adjustmentSubtypesByType := utils.AdjustmentTransactionSubtypesByTypeForFrontend() adjustmentSubtypesByType := utils.AdjustmentTransactionSubtypesByTypeForFrontend()
return map[string]interface{}{ return map[string]interface{}{
"flags": flagList, "flags": flagList,
"legacy_flag_aliases": legacyFlagAliases,
"product_flag_mapping": map[string]interface{}{
"flags": productMainFlags,
"options": productFlagOptions,
"sub_flag_to_flag": productSubFlagToFlag,
},
"warehouse_types": []string{ "warehouse_types": []string{
"AREA", "AREA",
"LOKASI", "LOKASI",
@@ -364,7 +364,7 @@ func (r *DashboardRepositoryImpl) SumSapronakCost(ctx context.Context, start, en
Joins("LEFT JOIN product_warehouses AS pw ON pw.id = pi.product_warehouse_id"). Joins("LEFT JOIN product_warehouses AS pw ON pw.id = pi.product_warehouse_id").
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = COALESCE(pi.project_flock_kandang_id, pw.project_flock_kandang_id)"). Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.id = COALESCE(pi.project_flock_kandang_id, pw.project_flock_kandang_id)").
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("f.name IN ?", []utils.FlagType{utils.FlagAyam, utils.FlagDOC, utils.FlagPullet, utils.FlagPakan, utils.FlagOVK}). Where("f.name IN ?", []utils.FlagType{utils.FlagDOC, utils.FlagPakan, utils.FlagOVK}).
Where("pi.received_date IS NOT NULL"). Where("pi.received_date IS NOT NULL").
Where("pi.received_date >= ? AND pi.received_date < ?", start, end) Where("pi.received_date >= ? AND pi.received_date < ?", start, end)
@@ -179,7 +179,6 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, cu
flag == string(utils.FlagTelurPecah) || flag == string(utils.FlagTelurPecah) ||
flag == string(utils.FlagTelurPutih) || flag == string(utils.FlagTelurPutih) ||
flag == string(utils.FlagTelurRetak) || flag == string(utils.FlagTelurRetak) ||
flag == string(utils.FlagAyam) ||
flag == string(utils.FlagAyamAfkir) || flag == string(utils.FlagAyamAfkir) ||
flag == string(utils.FlagAyamCulling) || flag == string(utils.FlagAyamCulling) ||
flag == string(utils.FlagAyamMati) { flag == string(utils.FlagAyamMati) {
@@ -144,7 +144,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
for _, t := range marketingTypes { for _, t := range marketingTypes {
switch t { switch t {
case string(utils.MarketingTypeAyamPullet): case string(utils.MarketingTypeAyamPullet):
flagSet[string(utils.FlagAyam)] = struct{}{}
flagSet[string(utils.FlagDOC)] = struct{}{} flagSet[string(utils.FlagDOC)] = struct{}{}
flagSet[string(utils.FlagPullet)] = struct{}{} flagSet[string(utils.FlagPullet)] = struct{}{}
flagSet[string(utils.FlagLayer)] = struct{}{} flagSet[string(utils.FlagLayer)] = struct{}{}
@@ -50,16 +50,6 @@ type transferService struct {
ExpenseBridge TransferExpenseBridge ExpenseBridge TransferExpenseBridge
} }
const (
transferFunctionCodeIn = "STOCK_TRANSFER_IN"
transferFunctionCodeOut = "STOCK_TRANSFER_OUT"
)
type transferRoutePair struct {
Stockable commonSvc.FifoStockV2RouteRule
Usable commonSvc.FifoStockV2RouteRule
}
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService { func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService {
return &transferService{ return &transferService{
Log: utils.Log, Log: utils.Log,
@@ -454,9 +444,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
} }
} }
routePairsByProductID := map[uint]transferRoutePair{} pakanProducts := map[uint]bool{}
if len(req.Products) > 0 { if s.FifoStockV2Svc != nil && len(req.Products) > 0 {
routePairsByProductID, err = s.resolveTransferRoutes(c.Context(), tx, req.Products) pakanProducts, err = s.resolvePakanProducts(c.Context(), tx, req.Products)
if err != nil { if err != nil {
return err return err
} }
@@ -464,17 +454,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
for _, product := range req.Products { for _, product := range req.Products {
detail := detailMap[uint64(product.ProductID)] detail := detailMap[uint64(product.ProductID)]
routePair, ok := routePairsByProductID[uint(product.ProductID)]
if !ok {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Konfigurasi FIFO v2 transfer tidak ditemukan untuk produk %d", product.ProductID),
)
}
outUsageQty := 0.0 outUsageQty := 0.0
outPendingQty := 0.0 outPendingQty := 0.0
useFifoV2 := s.FifoStockV2Svc != nil useFifoV2 := s.FifoStockV2Svc != nil && pakanProducts[uint(product.ProductID)]
if useFifoV2 { if useFifoV2 {
s.Log.Infof( s.Log.Infof(
"[fifo-v2][transfer] use reflow movement=%s detail_id=%d product_id=%d source_pw=%d qty=%.3f", "[fifo-v2][transfer] use reflow movement=%s detail_id=%d product_id=%d source_pw=%d qty=%.3f",
@@ -485,12 +468,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
product.ProductQty, product.ProductQty,
) )
reflowResult, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{ reflowResult, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: routePair.Usable.FlagGroupCode, FlagGroupCode: "PAKAN",
ProductWarehouseID: uint(*detail.SourceProductWarehouseID), ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Usable: commonSvc.FifoStockV2Ref{ Usable: commonSvc.FifoStockV2Ref{
ID: uint(detail.Id), ID: uint(detail.Id),
LegacyTypeKey: routePair.Usable.LegacyTypeKey, LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
FunctionCode: routePair.Usable.FunctionCode, FunctionCode: "STOCK_TRANSFER_OUT",
}, },
DesiredQty: product.ProductQty, DesiredQty: product.ProductQty,
Tx: tx, Tx: tx,
@@ -508,12 +491,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
outPendingQty, outPendingQty,
) )
} else { } else {
usableKey := fifo.UsableKey(strings.TrimSpace(routePair.Usable.LegacyTypeKey))
if usableKey == "" {
usableKey = fifo.UsableKeyStockTransferOut
}
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: usableKey, UsableKey: fifo.UsableKeyStockTransferOut,
UsableID: uint(detail.Id), UsableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.SourceProductWarehouseID), ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Quantity: product.ProductQty, Quantity: product.ProductQty,
@@ -574,12 +553,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
product.ProductQty, product.ProductQty,
) )
} }
stockableKey := fifo.StockableKey(strings.TrimSpace(routePair.Stockable.LegacyTypeKey))
if stockableKey == "" {
stockableKey = fifo.StockableKeyStockTransferIn
}
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: stockableKey, StockableKey: fifo.StockableKeyStockTransferIn,
StockableID: uint(detail.Id), StockableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.DestProductWarehouseID), ProductWarehouseID: uint(*detail.DestProductWarehouseID),
Quantity: product.ProductQty, Quantity: product.ProductQty,
@@ -682,72 +657,50 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return result, nil return result, nil
} }
func (s *transferService) resolveTransferRoutes( func (s *transferService) resolvePakanProducts(
ctx context.Context, ctx context.Context,
tx *gorm.DB, tx *gorm.DB,
products []validation.TransferProduct, products []validation.TransferProduct,
) (map[uint]transferRoutePair, error) { ) (map[uint]bool, error) {
out := make(map[uint]transferRoutePair, len(products)) out := make(map[uint]bool, len(products))
if len(products) == 0 { if len(products) == 0 {
return out, nil return out, nil
} }
productIDs := make(map[uint]struct{}, len(products)) productIDs := make([]uint, 0, len(products))
seen := make(map[uint]struct{}, len(products))
for _, product := range products { for _, product := range products {
if product.ProductID == 0 { if product.ProductID == 0 {
continue continue
} }
productIDs[product.ProductID] = struct{}{} if _, ok := seen[product.ProductID]; ok {
continue
}
seen[product.ProductID] = struct{}{}
productIDs = append(productIDs, product.ProductID)
}
if len(productIDs) == 0 {
return out, nil
} }
for productID := range productIDs { type row struct {
usableRoute, err := commonSvc.ResolveFifoStockV2RouteByProductIDAndLane( ProductID uint `gorm:"column:product_id"`
ctx, }
tx, var rows []row
productID, err := tx.WithContext(ctx).
transferFunctionCodeOut, Table("flags f").
commonSvc.FifoStockV2LaneUsable, Select("DISTINCT f.flagable_id AS product_id").
) Where("f.flagable_type = ?", entity.FlagableTypeProduct).
if err != nil { Where("f.name IN ?", []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}).
return nil, err Where("f.flagable_id IN ?", productIDs).
} Scan(&rows).Error
if usableRoute == nil { if err != nil {
return nil, fiber.NewError( return nil, err
fiber.StatusBadRequest,
fmt.Sprintf("Produk %d tidak mendukung transaksi Transfer Stock (OUT) pada matrix FIFO v2", productID),
)
}
stockableRoute, err := commonSvc.ResolveFifoStockV2RouteByProductIDAndLane(
ctx,
tx,
productID,
transferFunctionCodeIn,
commonSvc.FifoStockV2LaneStockable,
)
if err != nil {
return nil, err
}
if stockableRoute == nil {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Produk %d tidak mendukung transaksi Transfer Stock (IN) pada matrix FIFO v2", productID),
)
}
if strings.TrimSpace(usableRoute.FlagGroupCode) != strings.TrimSpace(stockableRoute.FlagGroupCode) {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Konfigurasi matrix FIFO v2 transfer tidak konsisten untuk produk %d", productID),
)
}
out[productID] = transferRoutePair{
Stockable: *stockableRoute,
Usable: *usableRoute,
}
} }
for _, row := range rows {
out[row.ProductID] = true
}
return out, nil return out, nil
} }
@@ -105,7 +105,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanForAgeChickD
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("flags.name IN (?)", []string{ Where("flags.name IN (?)", []string{
string(utils.FlagAyam),
string(utils.FlagAyamAfkir), string(utils.FlagAyamAfkir),
string(utils.FlagAyamCulling), string(utils.FlagAyamCulling),
string(utils.FlagPullet), string(utils.FlagPullet),
@@ -159,12 +158,9 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanByCategory(c
string(utils.FlagTelurPecah), string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih), string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak), string(utils.FlagTelurRetak),
string(utils.FlagTelurPapacal),
string(utils.FlagTelurJumbo),
}) })
} else { } else {
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
string(utils.FlagAyam),
string(utils.FlagDOC), string(utils.FlagDOC),
string(utils.FlagPullet), string(utils.FlagPullet),
string(utils.FlagLayer), string(utils.FlagLayer),
@@ -331,12 +327,12 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
switch filters.MarketingType { switch filters.MarketingType {
case "ayam": case "ayam":
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
string(utils.FlagAyam), string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer), string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
}) })
case "telur": case "telur":
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih), string(utils.FlagTelurRetak), string(utils.FlagTelurPapacal), string(utils.FlagTelurJumbo), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak),
}) })
case "trading": case "trading":
db = db.Where("flags.name IN (?)", []string{ db = db.Where("flags.name IN (?)", []string{
@@ -549,29 +549,8 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found") return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
} }
route, err := commonSvc.ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx,
tx,
marketingProduct.ProductWarehouseId,
"MARKETING_OUT",
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO v2 marketing route")
}
if route == nil {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak mendukung transaksi Marketing pada matrix FIFO v2", marketingProduct.ProductWarehouseId),
)
}
usableKey := fifo.UsableKey(route.LegacyTypeKey)
if route.LegacyTypeKey == "" {
usableKey = fifo.UsableKeyMarketingDelivery
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: usableKey, UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id, UsableID: deliveryProduct.Id,
ProductWarehouseID: marketingProduct.ProductWarehouseId, ProductWarehouseID: marketingProduct.ProductWarehouseId,
Quantity: requestedQty, Quantity: requestedQty,
@@ -624,27 +603,6 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
} }
route, err := commonSvc.ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx,
tx,
marketingProduct.ProductWarehouseId,
"MARKETING_OUT",
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO v2 marketing route")
}
if route == nil {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak mendukung transaksi Marketing pada matrix FIFO v2", marketingProduct.ProductWarehouseId),
)
}
usableKey := fifo.UsableKey(route.LegacyTypeKey)
if route.LegacyTypeKey == "" {
usableKey = fifo.UsableKeyMarketingDelivery
}
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id) currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
if err != nil { if err != nil {
@@ -656,7 +614,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
} }
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: usableKey, UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id, UsableID: deliveryProduct.Id,
Tx: tx, Tx: tx,
}); err != nil { }); err != nil {
@@ -48,8 +48,6 @@ type salesOrdersService struct {
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
} }
const marketingFunctionCodeOut = "MARKETING_OUT"
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository, func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService { projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{ return &salesOrdersService{
@@ -76,36 +74,6 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Products.ProductWarehouse.Warehouse") Preload("Products.ProductWarehouse.Warehouse")
} }
func (s salesOrdersService) resolveMarketingUsableKey(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (fifo.UsableKey, error) {
if productWarehouseID == 0 {
return "", fiber.NewError(fiber.StatusBadRequest, "Product warehouse tidak valid untuk transaksi Marketing")
}
route, err := commonSvc.ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx,
tx,
productWarehouseID,
marketingFunctionCodeOut,
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return "", fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO v2 marketing route")
}
if route == nil {
return "", fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak mendukung transaksi Marketing pada matrix FIFO v2", productWarehouseID),
)
}
usableKey := fifo.UsableKey(strings.TrimSpace(route.LegacyTypeKey))
if usableKey == "" {
usableKey = fifo.UsableKeyMarketingDelivery
}
return usableKey, nil
}
func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) {
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err return nil, err
@@ -408,12 +376,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if qtyDiff < 0 { if qtyDiff < 0 {
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.") return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
} else if qtyDiff > 0 { } else if qtyDiff > 0 {
usableKey, err := s.resolveMarketingUsableKey(c.Context(), dbTransaction, rp.ProductWarehouseId) _, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
if err != nil { UsableKey: fifo.UsableKeyMarketingDelivery,
return err
}
_, err = s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: usableKey,
UsableID: deliveryProduct.Id, UsableID: deliveryProduct.Id,
ProductWarehouseID: rp.ProductWarehouseId, ProductWarehouseID: rp.ProductWarehouseId,
Quantity: qtyDiff, Quantity: qtyDiff,
@@ -474,13 +438,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if deliveryProduct.DeliveryDate != nil { if deliveryProduct.DeliveryDate != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id))
} }
usableKey, err := s.resolveMarketingUsableKey(c.Context(), dbTransaction, old.ProductWarehouseId)
if err != nil {
return err
}
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{ if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: usableKey, UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id, UsableID: deliveryProduct.Id,
Tx: dbTransaction, Tx: dbTransaction,
}); err != nil { }); err != nil {
@@ -562,22 +522,9 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
if len(marketing.Products) > 0 { if len(marketing.Products) > 0 {
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id) deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
if err == nil && len(deliveryProducts) > 0 { if err == nil && len(deliveryProducts) > 0 {
deliveryUsableKeyByProductID := make(map[uint]fifo.UsableKey, len(marketing.Products))
for _, product := range marketing.Products {
usableKey, err := s.resolveMarketingUsableKey(c.Context(), dbTransaction, product.ProductWarehouseId)
if err != nil {
return err
}
deliveryUsableKeyByProductID[product.Id] = usableKey
}
for _, dp := range deliveryProducts { for _, dp := range deliveryProducts {
usableKey, ok := deliveryUsableKeyByProductID[dp.MarketingProductId]
if !ok {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk delivery %d tidak ditemukan", dp.Id))
}
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{ if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: usableKey, UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: dp.Id, UsableID: dp.Id,
Tx: dbTransaction, Tx: dbTransaction,
}); err != nil { }); err != nil {
@@ -7,7 +7,6 @@ import (
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === DTO Structs === // === DTO Structs ===
@@ -18,9 +17,6 @@ type ProductRelationDTO struct {
ProductPrice float64 `gorm:"type:numeric(15,3);not null"` ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"` SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flag *string `json:"flag,omitempty"`
SubFlag *string `json:"sub_flag,omitempty"`
SubFlags *[]string `json:"sub_flags,omitempty"`
Flags *[]string `json:"flags,omitempty"` Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []ProductSupplierDTO `json:"suppliers"` Suppliers []ProductSupplierDTO `json:"suppliers"`
@@ -35,9 +31,6 @@ type ProductListDTO struct {
SellingPrice *float64 `json:"selling_price,omitempty"` SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,omitempty"` Tax *float64 `json:"tax,omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty"` ExpiryPeriod *int `json:"expiry_period,omitempty"`
Flag *string `json:"flag,omitempty"`
SubFlag *string `json:"sub_flag,omitempty"`
SubFlags []string `json:"sub_flags,omitempty"`
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
@@ -66,13 +59,6 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
for i, f := range e.Flags { for i, f := range e.Flags {
flags[i] = f.Name flags[i] = f.Name
} }
flag, subFlag, subFlags := resolveProductFlagAndSubFlags(flags)
var subFlagsRef *[]string
if len(subFlags) > 0 {
values := make([]string, len(subFlags))
copy(values, subFlags)
subFlagsRef = &values
}
var uomRef *uomDTO.UomRelationDTO var uomRef *uomDTO.UomRelationDTO
if e.Uom.Id != 0 { if e.Uom.Id != 0 {
@@ -91,9 +77,6 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
Name: e.Name, Name: e.Name,
ProductPrice: e.ProductPrice, ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice, SellingPrice: e.SellingPrice,
Flag: flag,
SubFlag: subFlag,
SubFlags: subFlagsRef,
Flags: &flags, Flags: &flags,
Uom: uomRef, Uom: uomRef,
ProductCategory: categoryRef, ProductCategory: categoryRef,
@@ -118,7 +101,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
for i, f := range e.Flags { for i, f := range e.Flags {
flags[i] = f.Name flags[i] = f.Name
} }
flag, subFlag, subFlags := resolveProductFlagAndSubFlags(flags)
var uomRef *uomDTO.UomRelationDTO var uomRef *uomDTO.UomRelationDTO
if e.Uom.Id != 0 { if e.Uom.Id != 0 {
@@ -129,9 +111,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
return ProductListDTO{ return ProductListDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
Flag: flag,
SubFlag: subFlag,
SubFlags: subFlags,
Flags: flags, Flags: flags,
Uom: uomRef, Uom: uomRef,
Brand: e.Brand, Brand: e.Brand,
@@ -162,58 +141,6 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
} }
} }
func resolveProductFlagAndSubFlags(flags []string) (*string, *string, []string) {
normalized := utils.NormalizeFlagTypes(flags)
if len(normalized) == 0 {
return nil, nil, nil
}
available := make(map[utils.FlagType]struct{}, len(normalized))
for _, flag := range normalized {
available[flag] = struct{}{}
}
var selectedFlag utils.FlagType
for _, mainFlag := range utils.ProductMainFlags() {
if _, ok := available[mainFlag]; ok {
selectedFlag = mainFlag
break
}
}
if selectedFlag == "" {
subToMain := utils.ProductSubFlagToFlag()
for _, flag := range normalized {
if parent, ok := subToMain[flag]; ok {
selectedFlag = parent
break
}
}
}
if selectedFlag == "" {
return nil, nil, nil
}
flag := string(selectedFlag)
var subFlag *string
subFlagValues := make([]string, 0)
subFlagsByMain := utils.ProductSubFlagsByFlag()
for _, sub := range subFlagsByMain[selectedFlag] {
if _, ok := available[sub]; ok {
subFlagValues = append(subFlagValues, string(sub))
}
}
if len(subFlagValues) > 0 {
first := subFlagValues[0]
subFlag = &first
}
return &flag, subFlag, subFlagValues
}
func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO { func toProductSupplierDTOs(relations []entity.ProductSupplier) []ProductSupplierDTO {
if len(relations) == 0 { if len(relations) == 0 {
return make([]ProductSupplierDTO, 0) return make([]ProductSupplierDTO, 0)
@@ -41,151 +41,6 @@ func normalizeProductFlags(raw []string) ([]string, error) {
return utils.FlagTypesToStrings(normalized), nil return utils.FlagTypesToStrings(normalized), nil
} }
func productMainFlagOptionsString() []string {
mainFlags := utils.ProductMainFlags()
result := make([]string, len(mainFlags))
for i, flag := range mainFlags {
result[i] = string(flag)
}
return result
}
func productSubFlagOptionsString(flag utils.FlagType) []string {
subFlagsByFlag := utils.ProductSubFlagsByFlag()
subFlags := subFlagsByFlag[flag]
result := make([]string, len(subFlags))
for i, subFlag := range subFlags {
result[i] = string(subFlag)
}
return result
}
func normalizeStructuredSubFlagsInput(subFlagRaw *string, subFlagsRaw []string, hasSubFlagsField bool) ([]utils.FlagType, error) {
values := make([]string, 0, len(subFlagsRaw)+1)
if subFlagRaw != nil {
single := strings.TrimSpace(*subFlagRaw)
if single == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "sub_flag cannot be empty")
}
values = append(values, single)
}
if hasSubFlagsField {
for _, raw := range subFlagsRaw {
item := strings.TrimSpace(raw)
if item == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "sub_flags cannot contain empty value")
}
values = append(values, item)
}
}
if len(values) == 0 {
return nil, nil
}
return utils.NormalizeFlagTypes(values), nil
}
func resolveProductFlagsFromFlagInput(flagRaw *string, subFlagRaw *string, subFlagsRaw []string, hasSubFlagsField bool) ([]string, bool, error) {
if flagRaw == nil && subFlagRaw == nil && !hasSubFlagsField {
return nil, false, nil
}
if flagRaw == nil && (subFlagRaw != nil || hasSubFlagsField) {
return nil, false, fiber.NewError(fiber.StatusBadRequest, "flag is required when sub_flag/sub_flags is provided")
}
flagText := strings.TrimSpace(*flagRaw)
if flagText == "" {
return nil, false, fiber.NewError(fiber.StatusBadRequest, "flag cannot be empty")
}
flag := utils.CanonicalFlagType(flagText)
if !utils.IsProductMainFlag(flag) {
return nil, false, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Invalid product flag: %s. Allowed flags: %s", flagText, strings.Join(productMainFlagOptionsString(), ", ")),
)
}
out := []string{string(flag)}
normalizedSubFlags, err := normalizeStructuredSubFlagsInput(subFlagRaw, subFlagsRaw, hasSubFlagsField)
if err != nil {
return nil, false, err
}
if len(normalizedSubFlags) == 0 {
if !utils.ProductFlagAllowWithoutSubFlag(flag) {
return nil, false, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("sub_flag/sub_flags is required for flag %s", string(flag)),
)
}
return out, true, nil
}
invalidSubFlags := make([]string, 0)
for _, subFlag := range normalizedSubFlags {
if !utils.IsValidProductSubFlag(flag, subFlag) {
invalidSubFlags = append(invalidSubFlags, string(subFlag))
}
}
if len(invalidSubFlags) > 0 {
return nil, false, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Invalid sub_flags %s for flag %s. Allowed sub_flags: %s", strings.Join(invalidSubFlags, ", "), string(flag), strings.Join(productSubFlagOptionsString(flag), ", ")),
)
}
out = append(out, utils.FlagTypesToStrings(normalizedSubFlags)...)
return out, true, nil
}
func resolveCreateProductFlags(req *validation.Create) ([]string, error) {
hasStructuredInput := req.Flag != nil || req.SubFlag != nil || req.SubFlags != nil
if len(req.Flags) > 0 && hasStructuredInput {
return nil, fiber.NewError(fiber.StatusBadRequest, "Use either flags or flag/sub_flag/sub_flags, not both")
}
if len(req.Flags) > 0 {
return normalizeProductFlags(req.Flags)
}
flags, _, err := resolveProductFlagsFromFlagInput(req.Flag, req.SubFlag, req.SubFlags, req.SubFlags != nil)
return flags, err
}
func resolveUpdateProductFlags(req *validation.Update) (bool, []string, error) {
hasStructuredInput := req.Flag != nil || req.SubFlag != nil || req.SubFlags != nil
if req.Flags != nil {
if hasStructuredInput {
if len(*req.Flags) > 0 {
return false, nil, fiber.NewError(fiber.StatusBadRequest, "Use either flags or flag/sub_flag/sub_flags, not both")
}
} else {
flags, err := normalizeProductFlags(*req.Flags)
if err != nil {
return false, nil, err
}
return true, flags, nil
}
}
subFlagsRaw := make([]string, 0)
if req.SubFlags != nil {
subFlagsRaw = *req.SubFlags
}
flags, provided, err := resolveProductFlagsFromFlagInput(req.Flag, req.SubFlag, subFlagsRaw, req.SubFlags != nil)
if err != nil {
return false, nil, err
}
return provided, flags, nil
}
func NewProductService(repo repository.ProductRepository, validate *validator.Validate) ProductService { func NewProductService(repo repository.ProductRepository, validate *validator.Validate) ProductService {
return &productService{ return &productService{
Log: utils.Log, Log: utils.Log,
@@ -322,7 +177,7 @@ func (s *productService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
} }
productFlags, flagErr := resolveCreateProductFlags(req) productFlags, flagErr := normalizeProductFlags(req.Flags)
if flagErr != nil { if flagErr != nil {
return nil, flagErr return nil, flagErr
} }
@@ -482,10 +337,13 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
flagUpdate bool flagUpdate bool
flagValues []string flagValues []string
) )
var flagErr error if req.Flags != nil {
flagUpdate, flagValues, flagErr = resolveUpdateProductFlags(req) flagUpdate = true
if flagErr != nil { var flagErr error
return nil, flagErr flagValues, flagErr = normalizeProductFlags(*req.Flags)
if flagErr != nil {
return nil, flagErr
}
} }
if len(updateBody) == 0 && !supplierUpdate && !flagUpdate { if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
@@ -6,37 +6,31 @@ type SupplierPrice struct {
} }
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3,max=50"`
Brand string `json:"brand" validate:"required_strict,min=2,max=50"` Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
Sku *string `json:"sku,omitempty" validate:"omitempty,max=100"` Sku *string `json:"sku,omitempty" validate:"omitempty,max=100"`
UomID uint `json:"uom_id" validate:"required,gt=0"` UomID uint `json:"uom_id" validate:"required,gt=0"`
ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"` ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"`
ProductPrice float64 `json:"product_price" validate:"required"` ProductPrice float64 `json:"product_price" validate:"required"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"` SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flag *string `json:"flag,omitempty" validate:"omitempty,max=50"` Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
SubFlag *string `json:"sub_flag,omitempty" validate:"omitempty,max=50"`
SubFlags []string `json:"sub_flags,omitempty" validate:"omitempty,dive,max=50"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"` Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Brand *string `json:"brand,omitempty" validate:"omitempty,min=2"` Brand *string `json:"brand,omitempty" validate:"omitempty,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"` Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"` UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
ProductCategoryID *uint `json:"product_category_id,omitempty" validate:"omitempty,gt=0"` ProductCategoryID *uint `json:"product_category_id,omitempty" validate:"omitempty,gt=0"`
ProductPrice *float64 `json:"product_price,omitempty" validate:"omitempty"` ProductPrice *float64 `json:"product_price,omitempty" validate:"omitempty"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"` SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"` Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"` ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"` Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flag *string `json:"flag,omitempty" validate:"omitempty,max=50"` Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
SubFlag *string `json:"sub_flag,omitempty" validate:"omitempty,max=50"`
SubFlags *[]string `json:"sub_flags,omitempty" validate:"omitempty,dive,max=50"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
} }
type Query struct { type Query struct {
@@ -29,8 +29,6 @@ import (
var chickinUsableKey = fifo.UsableKeyProjectChickin var chickinUsableKey = fifo.UsableKeyProjectChickin
const chickinFunctionCodeOut = "CHICKIN_OUT"
type ChickinService interface { type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
@@ -164,32 +162,25 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
if productWarehouse.Product.Id != 0 { if productWarehouse.Product.Id != 0 {
requiredFlags := make([]utils.FlagType, 0, 2) var requiredFlag utils.FlagType
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
requiredFlags = append(requiredFlags, utils.FlagAyam, utils.FlagDOC) requiredFlag = utils.FlagDOC
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
requiredFlags = append(requiredFlags, utils.FlagAyam, utils.FlagPullet) requiredFlag = utils.FlagPullet
} else { } else {
return nil, fmt.Errorf("invalid flock category for chickin") return nil, fmt.Errorf("invalid flock category for chickin")
} }
hasRequiredFlag := false hasRequiredFlag := false
for _, flag := range productWarehouse.Product.Flags { for _, flag := range productWarehouse.Product.Flags {
currentFlag := utils.FlagType(flag.Name) if utils.FlagType(flag.Name) == requiredFlag {
for _, requiredFlag := range requiredFlags { hasRequiredFlag = true
if currentFlag == requiredFlag {
hasRequiredFlag = true
break
}
}
if hasRequiredFlag {
break break
} }
} }
if !hasRequiredFlag { if !hasRequiredFlag {
requiredText := strings.Join(utils.FlagTypesToStrings(requiredFlags), " or ") return nil, fmt.Errorf("product warehouse %d cannot be used for %s chickin. Product must have %s flag (product ID: %d, warehouse ID: %d)", chickinReq.ProductWarehouseId, projectFlockKandang.ProjectFlock.Category, requiredFlag, productWarehouse.Product.Id, productWarehouse.Id)
return nil, fmt.Errorf("product warehouse %d cannot be used for %s chickin. Product must have %s flag (product ID: %d, warehouse ID: %d)", chickinReq.ProductWarehouseId, projectFlockKandang.ProjectFlock.Category, requiredText, productWarehouse.Product.Id, productWarehouse.Id)
} }
} }
@@ -492,9 +483,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
var targetFlag utils.FlagType var targetFlag utils.FlagType
if category == string(utils.ProjectFlockCategoryGrowing) { if category == string(utils.ProjectFlockCategoryGrowing) {
targetFlag = utils.FlagAyam targetFlag = utils.FlagPullet
} else if category == string(utils.ProjectFlockCategoryLaying) { } else if category == string(utils.ProjectFlockCategoryLaying) {
targetFlag = utils.FlagAyam targetFlag = utils.FlagLayer
} else { } else {
continue continue
} }
@@ -630,29 +621,8 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
return nil return nil
} }
route, err := commonSvc.ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx,
tx,
chickin.ProductWarehouseId,
chickinFunctionCodeOut,
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO v2 chickin route")
}
if route == nil {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak mendukung transaksi Chickin pada matrix FIFO v2", chickin.ProductWarehouseId),
)
}
usableKey := fifo.UsableKey(strings.TrimSpace(route.LegacyTypeKey))
if usableKey == "" {
usableKey = chickinUsableKey
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: usableKey, UsableKey: chickinUsableKey,
UsableID: chickin.Id, UsableID: chickin.Id,
ProductWarehouseID: chickin.ProductWarehouseId, ProductWarehouseID: chickin.ProductWarehouseId,
Quantity: desiredQty, Quantity: desiredQty,
@@ -718,34 +688,13 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
return nil return nil
} }
route, err := commonSvc.ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx,
tx,
chickin.ProductWarehouseId,
chickinFunctionCodeOut,
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO v2 chickin route")
}
if route == nil {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak mendukung transaksi Chickin pada matrix FIFO v2", chickin.ProductWarehouseId),
)
}
usableKey := fifo.UsableKey(strings.TrimSpace(route.LegacyTypeKey))
if usableKey == "" {
usableKey = chickinUsableKey
}
var currentUsage float64 var currentUsage float64
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&currentUsage).Error; err != nil { if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&currentUsage).Error; err != nil {
} }
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: usableKey, UsableKey: chickinUsableKey,
UsableID: chickin.Id, UsableID: chickin.Id,
Tx: tx, Tx: tx,
}); err != nil { }); err != nil {
@@ -312,7 +312,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return nil, err return nil, err
} }
feedIDs := recordingutil.CollectWarehouseIDs(req.Stocks, func(st validation.Stock) uint { return st.ProductWarehouseId }) feedIDs := recordingutil.CollectWarehouseIDs(req.Stocks, func(st validation.Stock) uint { return st.ProductWarehouseId })
if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER", "OVK", "OBAT", "VITAMIN", "KIMIA"}, "feed"); err != nil { if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil {
return nil, err return nil, err
} }
depletionIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint { return d.ProductWarehouseId }) depletionIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint { return d.ProductWarehouseId })
@@ -320,7 +320,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return nil, err return nil, err
} }
eggIDs := recordingutil.CollectWarehouseIDs(req.Eggs, func(e validation.Egg) uint { return e.ProductWarehouseId }) eggIDs := recordingutil.CollectWarehouseIDs(req.Eggs, func(e validation.Egg) uint { return e.ProductWarehouseId })
if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR", "TELUR-UTUH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR-PECAH", "TELUR-PAPACAL", "TELUR-JUMBO"}, "egg"); err != nil { if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR-UTUH", "TELUR-PECAH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR"}, "egg"); err != nil {
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
@@ -512,7 +512,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err return err
} }
feedIDs := recordingutil.CollectWarehouseIDs(req.Stocks, func(st validation.Stock) uint { return st.ProductWarehouseId }) feedIDs := recordingutil.CollectWarehouseIDs(req.Stocks, func(st validation.Stock) uint { return st.ProductWarehouseId })
if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER", "OVK", "OBAT", "VITAMIN", "KIMIA"}, "feed"); err != nil { if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil {
return err return err
} }
if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, note, actorID); err != nil { if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, note, actorID); err != nil {
@@ -613,7 +613,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err return err
} }
eggIDs := recordingutil.CollectWarehouseIDs(req.Eggs, func(e validation.Egg) uint { return e.ProductWarehouseId }) eggIDs := recordingutil.CollectWarehouseIDs(req.Eggs, func(e validation.Egg) uint { return e.ProductWarehouseId })
if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR", "TELUR-UTUH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR-PECAH", "TELUR-PAPACAL", "TELUR-JUMBO"}, "egg"); err != nil { if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR-UTUH", "TELUR-PECAH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR"}, "egg"); err != nil {
return err return err
} }
if err := ensureRecordingEggsUnused(existingEggs); err != nil { if err := ensureRecordingEggsUnused(existingEggs); err != nil {
@@ -21,93 +21,7 @@ import (
var recordingStockUsableKey = fifo.UsableKeyRecordingStock var recordingStockUsableKey = fifo.UsableKeyRecordingStock
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
const ( const depletionUsageTolerance = 0.000001
depletionUsageTolerance = 0.000001
recordingFunctionCodeStockOut = "RECORDING_STOCK_OUT"
recordingFunctionCodeDepletionOut = "RECORDING_DEPLETION_OUT"
recordingFunctionCodeDepletionIn = "RECORDING_DEPLETION_IN"
recordingFunctionCodeRecordingEggIn = "RECORDING_EGG_IN"
)
func (s *recordingService) resolveRecordingUsableKey(
ctx context.Context,
tx *gorm.DB,
productWarehouseID uint,
functionCode string,
fallback fifo.UsableKey,
actionLabel string,
) (fifo.UsableKey, error) {
if productWarehouseID == 0 {
return "", fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse tidak valid untuk transaksi %s", actionLabel))
}
route, err := commonSvc.ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx,
tx,
productWarehouseID,
functionCode,
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return "", fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO v2 recording route")
}
if route == nil {
return "", fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak mendukung transaksi %s pada matrix FIFO v2", productWarehouseID, actionLabel),
)
}
usableKey := fifo.UsableKey(strings.TrimSpace(route.LegacyTypeKey))
if usableKey == "" {
usableKey = fallback
}
if usableKey == "" {
return "", fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 recording route misconfiguration")
}
return usableKey, nil
}
func (s *recordingService) resolveRecordingStockableKey(
ctx context.Context,
tx *gorm.DB,
productWarehouseID uint,
functionCode string,
fallback fifo.StockableKey,
actionLabel string,
) (fifo.StockableKey, error) {
if productWarehouseID == 0 {
return "", fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse tidak valid untuk transaksi %s", actionLabel))
}
route, err := commonSvc.ResolveFifoStockV2RouteByProductWarehouseIDAndLane(
ctx,
tx,
productWarehouseID,
functionCode,
commonSvc.FifoStockV2LaneStockable,
)
if err != nil {
return "", fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO v2 recording route")
}
if route == nil {
return "", fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak mendukung transaksi %s pada matrix FIFO v2", productWarehouseID, actionLabel),
)
}
stockableKey := fifo.StockableKey(strings.TrimSpace(route.LegacyTypeKey))
if stockableKey == "" {
stockableKey = fallback
}
if stockableKey == "" {
return "", fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 recording route misconfiguration")
}
return stockableKey, nil
}
func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) { func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) {
if s == nil || s.Log == nil { if s == nil || s.Log == nil {
@@ -211,20 +125,8 @@ func (s *recordingService) consumeRecordingStocks(
} }
desiredTotal := desired + pending desiredTotal := desired + pending
usableKey, err := s.resolveRecordingUsableKey(
ctx,
tx,
stock.ProductWarehouseId,
recordingFunctionCodeStockOut,
recordingStockUsableKey,
"Recording (Stock)",
)
if err != nil {
return err
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: usableKey, UsableKey: recordingStockUsableKey,
UsableID: stock.Id, UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId, ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desiredTotal, Quantity: desiredTotal,
@@ -307,21 +209,9 @@ func (s *recordingService) consumeRecordingDepletions(
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
} }
usableKey, err := s.resolveRecordingUsableKey(
ctx,
tx,
sourceWarehouseID,
recordingFunctionCodeDepletionOut,
recordingDepletionUsableKey,
"Recording Depletion (Source)",
)
if err != nil {
return err
}
desired := depletion.Qty + depletion.PendingQty desired := depletion.Qty + depletion.PendingQty
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: usableKey, UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id, UsableID: depletion.Id,
ProductWarehouseID: sourceWarehouseID, ProductWarehouseID: sourceWarehouseID,
Quantity: desired, Quantity: desired,
@@ -424,20 +314,8 @@ func (s *recordingService) releaseRecordingStocks(
if stock.Id == 0 { if stock.Id == 0 {
continue continue
} }
usableKey, err := s.resolveRecordingUsableKey(
ctx,
tx,
stock.ProductWarehouseId,
recordingFunctionCodeStockOut,
recordingStockUsableKey,
"Recording (Stock)",
)
if err != nil {
return err
}
if stock.UsageQty != nil && *stock.UsageQty > 0 { if stock.UsageQty != nil && *stock.UsageQty > 0 {
activeCount, err := s.countActiveAllocations(ctx, tx, usableKey, stock.Id) activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id)
if err != nil { if err != nil {
return err return err
} }
@@ -448,16 +326,16 @@ func (s *recordingService) releaseRecordingStocks(
} }
continue continue
} }
if err := s.resyncStockableUsageFromAllocations(ctx, tx, usableKey, stock.Id); err != nil { if err := s.resyncStockableUsageFromAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id); err != nil {
return err return err
} }
if err := s.ensureActiveAllocations(ctx, tx, usableKey, stock.Id); err != nil { if err := s.ensureActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id); err != nil {
return err return err
} }
} }
s.logStockTrace("release:start", stock, "") s.logStockTrace("release:start", stock, "")
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: usableKey, UsableKey: recordingStockUsableKey,
UsableID: stock.Id, UsableID: stock.Id,
Tx: tx, Tx: tx,
}); err != nil { }); err != nil {
@@ -522,28 +400,8 @@ func (s *recordingService) releaseRecordingDepletions(
if depletion.Id == 0 { if depletion.Id == 0 {
continue continue
} }
sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil {
sourceWarehouseID = *depletion.SourceProductWarehouseId
}
if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
}
usableKey, err := s.resolveRecordingUsableKey(
ctx,
tx,
sourceWarehouseID,
recordingFunctionCodeDepletionOut,
recordingDepletionUsableKey,
"Recording Depletion (Source)",
)
if err != nil {
return err
}
if depletion.UsageQty > 0 { if depletion.UsageQty > 0 {
activeCount, err := s.countActiveAllocations(ctx, tx, usableKey, depletion.Id) activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id)
if err != nil { if err != nil {
return err return err
} }
@@ -560,10 +418,10 @@ func (s *recordingService) releaseRecordingDepletions(
} }
continue continue
} }
if err := s.resyncStockableUsageFromAllocations(ctx, tx, usableKey, depletion.Id); err != nil { if err := s.resyncStockableUsageFromAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id); err != nil {
return err return err
} }
if err := s.ensureActiveAllocations(ctx, tx, usableKey, depletion.Id); err != nil { if err := s.ensureActiveAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id); err != nil {
return err return err
} }
} }
@@ -573,8 +431,15 @@ func (s *recordingService) releaseRecordingDepletions(
return err return err
} }
sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil {
sourceWarehouseID = *depletion.SourceProductWarehouseId
}
if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: usableKey, UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id, UsableID: depletion.Id,
Tx: tx, Tx: tx,
}); err != nil { }); err != nil {
@@ -765,20 +630,9 @@ func (s *recordingService) replenishRecordingEggs(
if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 { if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
continue continue
} }
stockableKey, err := s.resolveRecordingStockableKey(
ctx,
tx,
egg.ProductWarehouseId,
recordingFunctionCodeRecordingEggIn,
fifo.StockableKeyRecordingEgg,
"Recording Egg",
)
if err != nil {
return err
}
s.logEggTrace("replenish:start", egg, "") s.logEggTrace("replenish:start", egg, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: stockableKey, StockableKey: fifo.StockableKeyRecordingEgg,
StockableID: egg.Id, StockableID: egg.Id,
ProductWarehouseID: egg.ProductWarehouseId, ProductWarehouseID: egg.ProductWarehouseId,
Quantity: float64(egg.Qty), Quantity: float64(egg.Qty),
@@ -836,20 +690,9 @@ func (s *recordingService) replenishRecordingDepletions(
if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 { if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 {
continue continue
} }
stockableKey, err := s.resolveRecordingStockableKey(
ctx,
tx,
depletion.ProductWarehouseId,
recordingFunctionCodeDepletionIn,
fifo.StockableKeyRecordingDepletion,
"Recording Depletion (Destination)",
)
if err != nil {
return err
}
s.logDepletionTrace("replenish:start", depletion, "") s.logDepletionTrace("replenish:start", depletion, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: stockableKey, StockableKey: fifo.StockableKeyRecordingDepletion,
StockableID: depletion.Id, StockableID: depletion.Id,
ProductWarehouseID: depletion.ProductWarehouseId, ProductWarehouseID: depletion.ProductWarehouseId,
Quantity: depletion.Qty, Quantity: depletion.Qty,
@@ -881,20 +724,9 @@ func (s *recordingService) reduceRecordingDepletions(
if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 { if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 {
continue continue
} }
stockableKey, err := s.resolveRecordingStockableKey(
ctx,
tx,
depletion.ProductWarehouseId,
recordingFunctionCodeDepletionIn,
fifo.StockableKeyRecordingDepletion,
"Recording Depletion (Destination)",
)
if err != nil {
return err
}
s.logDepletionTrace("reduce:start", depletion, "") s.logDepletionTrace("reduce:start", depletion, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{ if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: stockableKey, StockableKey: fifo.StockableKeyRecordingDepletion,
StockableID: depletion.Id, StockableID: depletion.Id,
ProductWarehouseID: depletion.ProductWarehouseId, ProductWarehouseID: depletion.ProductWarehouseId,
Quantity: -depletion.Qty, Quantity: -depletion.Qty,
@@ -926,20 +758,9 @@ func (s *recordingService) reduceRecordingEggs(
if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 { if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
continue continue
} }
stockableKey, err := s.resolveRecordingStockableKey(
ctx,
tx,
egg.ProductWarehouseId,
recordingFunctionCodeRecordingEggIn,
fifo.StockableKeyRecordingEgg,
"Recording Egg",
)
if err != nil {
return err
}
s.logEggTrace("reduce:start", egg, "") s.logEggTrace("reduce:start", egg, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{ if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: stockableKey, StockableKey: fifo.StockableKeyRecordingEgg,
StockableID: egg.Id, StockableID: egg.Id,
ProductWarehouseID: egg.ProductWarehouseId, ProductWarehouseID: egg.ProductWarehouseId,
Quantity: -float64(egg.Qty), Quantity: -float64(egg.Qty),
@@ -43,8 +43,7 @@ type PurchaseService interface {
} }
const ( const (
priceTolerance = 0.0001 priceTolerance = 0.0001
purchaseFunctionCodeIn = "PURCHASE_IN"
) )
type purchaseService struct { type purchaseService struct {
@@ -406,14 +405,6 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
indexMap[key] = len(aggregated) - 1 indexMap[key] = len(aggregated) - 1
} }
routeValidationProducts := make([]uint, 0, len(aggregated))
for _, item := range aggregated {
routeValidationProducts = append(routeValidationProducts, item.productId)
}
if err := s.ensurePurchaseRouteSupport(c.Context(), s.PurchaseRepo.DB(), routeValidationProducts); err != nil {
return nil, err
}
var dueDate *time.Time var dueDate *time.Time
now := time.Now().UTC() now := time.Now().UTC()
d := now.AddDate(0, 0, req.CreditTerm) d := now.AddDate(0, 0, req.CreditTerm)
@@ -996,17 +987,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return nil, utils.BadRequest("Receiving data must be provided for all purchase items") return nil, utils.BadRequest("Receiving data must be provided for all purchase items")
} }
receivingProductIDs := make([]uint, 0, len(prepared))
for _, prep := range prepared {
if prep.item == nil || prep.item.ProductId == 0 {
continue
}
receivingProductIDs = append(receivingProductIDs, prep.item.ProductId)
}
if err := s.ensurePurchaseRouteSupport(c.Context(), s.PurchaseRepo.DB(), receivingProductIDs); err != nil {
return nil, err
}
receivingAction := action receivingAction := action
completedAction := entity.ApprovalActionApproved completedAction := entity.ApprovalActionApproved
approvalSvc := commonSvc.NewApprovalService( approvalSvc := commonSvc.NewApprovalService(
@@ -1854,15 +1834,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
productSupplierCache := make(map[uint]bool) productSupplierCache := make(map[uint]bool)
newItems := make([]*entity.PurchaseItem, 0, len(newPayloads)) newItems := make([]*entity.PurchaseItem, 0, len(newPayloads))
emptyVehicle := "" emptyVehicle := ""
newProductIDs := make([]uint, 0, len(newPayloads))
for _, payload := range newPayloads {
if payload.ProductID > 0 {
newProductIDs = append(newProductIDs, payload.ProductID)
}
}
if err := s.ensurePurchaseRouteSupport(ctx, s.PurchaseRepo.DB(), newProductIDs); err != nil {
return nil, err
}
for _, payload := range newPayloads { for _, payload := range newPayloads {
if payload.ProductID == 0 || payload.WarehouseID == 0 { if payload.ProductID == 0 || payload.WarehouseID == 0 {
@@ -1947,48 +1918,6 @@ func calculateTotalPrice(quantity float64, price float64, provided *float64, ref
return *provided, nil return *provided, nil
} }
func (s *purchaseService) ensurePurchaseRouteSupport(ctx context.Context, db *gorm.DB, productIDs []uint) error {
if len(productIDs) == 0 {
return nil
}
seen := make(map[uint]struct{}, len(productIDs))
for _, productID := range productIDs {
if productID == 0 {
continue
}
if _, ok := seen[productID]; ok {
continue
}
seen[productID] = struct{}{}
route, err := commonSvc.ResolveFifoStockV2RouteByProductIDAndLane(
ctx,
db,
productID,
purchaseFunctionCodeIn,
commonSvc.FifoStockV2LaneStockable,
)
if err != nil {
s.Log.Errorf("Failed to resolve FIFO v2 PURCHASE_IN route for product %d: %+v", productID, err)
return utils.Internal("Failed to resolve FIFO v2 purchase route")
}
if route == nil {
return utils.BadRequest(fmt.Sprintf("Product %d tidak mendukung transaksi Purchase pada matrix FIFO v2", productID))
}
if !strings.EqualFold(strings.TrimSpace(route.SourceTable), "purchase_items") {
s.Log.Errorf(
"Invalid FIFO v2 PURCHASE_IN route source table for product %d: expected purchase_items got %s",
productID,
route.SourceTable,
)
return utils.Internal("FIFO v2 purchase route misconfiguration")
}
}
return nil
}
func (s *purchaseService) attachLatestApproval(ctx context.Context, item *entity.Purchase) error { func (s *purchaseService) attachLatestApproval(ctx context.Context, item *entity.Purchase) error {
if item == nil || item.Id == 0 || s.ApprovalSvc == nil { if item == nil || item.Id == 0 || s.ApprovalSvc == nil {
return nil return nil
@@ -92,7 +92,7 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[u
for _, flag := range mdp.MarketingProduct.ProductWarehouse.Product.Flags { for _, flag := range mdp.MarketingProduct.ProductWarehouse.Product.Flags {
ft := utils.FlagType(flag.Name) ft := utils.FlagType(flag.Name)
if ft == utils.FlagAyam || ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati || if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati ||
ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer { ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer {
hasAyam = true hasAyam = true
} }
+9 -185
View File
@@ -14,18 +14,9 @@ type FlagType string
type FlagGroup string type FlagGroup string
type ProductFlagOption struct {
Flag FlagType `json:"flag"`
SubFlags []FlagType `json:"sub_flags"`
AllowWithoutSubFlag bool `json:"allow_without_sub_flag"`
}
const ( const (
FlagIsActive FlagType = "IS_ACTIVE" FlagIsActive FlagType = "IS_ACTIVE"
FlagAyam FlagType = "AYAM"
// Legacy AYAM flags kept for backward compatibility with existing production data.
FlagDOC FlagType = "DOC" FlagDOC FlagType = "DOC"
FlagPullet FlagType = "PULLET" FlagPullet FlagType = "PULLET"
FlagLayer FlagType = "LAYER" FlagLayer FlagType = "LAYER"
@@ -45,13 +36,11 @@ const (
FlagAyamMati FlagType = "AYAM-MATI" FlagAyamMati FlagType = "AYAM-MATI"
//flag telur //flag telur
FlagTelur FlagType = "TELUR" FlagTelur FlagType = "TELUR"
FlagTelurUtuh FlagType = "TELUR-UTUH" FlagTelurUtuh FlagType = "TELUR-UTUH"
FlagTelurPecah FlagType = "TELUR-PECAH" FlagTelurPecah FlagType = "TELUR-PECAH"
FlagTelurPutih FlagType = "TELUR-PUTIH" FlagTelurPutih FlagType = "TELUR-PUTIH"
FlagTelurRetak FlagType = "TELUR-RETAK" FlagTelurRetak FlagType = "TELUR-RETAK"
FlagTelurPapacal FlagType = "TELUR-PAPACAL"
FlagTelurJumbo FlagType = "TELUR-JUMBO"
) )
const ( const (
@@ -61,10 +50,9 @@ const (
var flagGroupOptions = map[FlagGroup][]FlagType{ var flagGroupOptions = map[FlagGroup][]FlagType{
FlagGroupProduct: { FlagGroupProduct: {
FlagAyam, FlagDOC,
FlagAyamAfkir, FlagPullet,
FlagAyamCulling, FlagLayer,
FlagAyamMati,
FlagPakan, FlagPakan,
FlagPreStarter, FlagPreStarter,
FlagStarter, FlagStarter,
@@ -73,82 +61,12 @@ var flagGroupOptions = map[FlagGroup][]FlagType{
FlagObat, FlagObat,
FlagVitamin, FlagVitamin,
FlagKimia, FlagKimia,
FlagTelur,
FlagTelurUtuh,
FlagTelurPutih,
FlagTelurRetak,
FlagTelurPecah,
FlagTelurPapacal,
FlagTelurJumbo,
}, },
FlagGroupNonstock: { FlagGroupNonstock: {
FlagEkspedisi, FlagEkspedisi,
}, },
} }
var legacyFlagTypeAliases = map[FlagType]FlagType{
FlagDOC: FlagAyam,
FlagPullet: FlagAyam,
FlagLayer: FlagAyam,
}
var productMainFlags = []FlagType{
FlagAyam,
FlagPakan,
FlagOVK,
FlagTelur,
}
var productSubFlagsByFlag = map[FlagType][]FlagType{
FlagAyam: {
FlagAyamAfkir,
FlagAyamCulling,
FlagAyamMati,
},
FlagPakan: {
FlagPreStarter,
FlagStarter,
FlagFinisher,
},
FlagOVK: {
FlagObat,
FlagVitamin,
FlagKimia,
},
FlagTelur: {
FlagTelurUtuh,
FlagTelurPutih,
FlagTelurRetak,
FlagTelurPecah,
FlagTelurPapacal,
FlagTelurJumbo,
},
}
var productSubFlagToFlag = func() map[FlagType]FlagType {
out := make(map[FlagType]FlagType)
for flag, subFlags := range productSubFlagsByFlag {
for _, subFlag := range subFlags {
out[subFlag] = flag
}
}
return out
}()
var productAllowWithoutSubFlagByFlag = map[FlagType]bool{
FlagAyam: true,
FlagPakan: false,
FlagOVK: false,
FlagTelur: false,
}
func canonicalizeFlagType(flag FlagType) FlagType {
if canonical, ok := legacyFlagTypeAliases[flag]; ok {
return canonical
}
return flag
}
var allFlagTypes = func() map[FlagType]struct{} { var allFlagTypes = func() map[FlagType]struct{} {
m := map[FlagType]struct{}{ m := map[FlagType]struct{}{
FlagIsActive: {}, FlagIsActive: {},
@@ -165,95 +83,6 @@ func AllFlagTypes() map[FlagType]struct{} {
return allFlagTypes return allFlagTypes
} }
func CanonicalFlagType(v string) FlagType {
normalized := FlagType(strings.ToUpper(strings.TrimSpace(v)))
if normalized == "" {
return ""
}
return canonicalizeFlagType(normalized)
}
func LegacyFlagTypeAliases() map[FlagType]FlagType {
out := make(map[FlagType]FlagType, len(legacyFlagTypeAliases))
for legacy, canonical := range legacyFlagTypeAliases {
out[legacy] = canonical
}
return out
}
func ProductMainFlags() []FlagType {
out := make([]FlagType, len(productMainFlags))
copy(out, productMainFlags)
return out
}
func ProductSubFlagsByFlag() map[FlagType][]FlagType {
out := make(map[FlagType][]FlagType, len(productSubFlagsByFlag))
for flag, subFlags := range productSubFlagsByFlag {
dup := make([]FlagType, len(subFlags))
copy(dup, subFlags)
out[flag] = dup
}
return out
}
func ProductSubFlagToFlag() map[FlagType]FlagType {
out := make(map[FlagType]FlagType, len(productSubFlagToFlag))
for subFlag, flag := range productSubFlagToFlag {
out[subFlag] = flag
}
return out
}
func ProductFlagOptions() []ProductFlagOption {
result := make([]ProductFlagOption, 0, len(productMainFlags))
for _, flag := range productMainFlags {
subFlags := productSubFlagsByFlag[flag]
dup := make([]FlagType, len(subFlags))
copy(dup, subFlags)
result = append(result, ProductFlagOption{
Flag: flag,
SubFlags: dup,
AllowWithoutSubFlag: productAllowWithoutSubFlagByFlag[flag],
})
}
return result
}
func ProductFlagAllowWithoutSubFlag(flag FlagType) bool {
canonical := canonicalizeFlagType(flag)
allow, ok := productAllowWithoutSubFlagByFlag[canonical]
if !ok {
return false
}
return allow
}
func IsProductMainFlag(flag FlagType) bool {
canonical := canonicalizeFlagType(flag)
for _, f := range productMainFlags {
if f == canonical {
return true
}
}
return false
}
func IsValidProductSubFlag(flag FlagType, subFlag FlagType) bool {
canonicalFlag := canonicalizeFlagType(flag)
canonicalSubFlag := canonicalizeFlagType(subFlag)
allowedSubFlags, ok := productSubFlagsByFlag[canonicalFlag]
if !ok {
return false
}
for _, allowed := range allowedSubFlags {
if allowed == canonicalSubFlag {
return true
}
}
return false
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// WarehouseType // WarehouseType
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -792,11 +621,7 @@ const (
// ------------------------------------------------------------------- // -------------------------------------------------------------------
func IsValidFlagType(v string) bool { func IsValidFlagType(v string) bool {
flag := FlagType(strings.ToUpper(strings.TrimSpace(v))) _, ok := allFlagTypes[FlagType(strings.ToUpper(strings.TrimSpace(v)))]
if _, ok := allFlagTypes[flag]; ok {
return true
}
_, ok := legacyFlagTypeAliases[flag]
return ok return ok
} }
@@ -842,7 +667,6 @@ func NormalizeFlagTypes(flags []string) []FlagType {
if normalized == "" { if normalized == "" {
continue continue
} }
normalized = canonicalizeFlagType(normalized)
if _, exists := seen[normalized]; exists { if _, exists := seen[normalized]; exists {
continue continue
} }