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

This reverts merge request !340
This commit is contained in:
Hafizh A. Y.
2026-02-27 09:37:03 +00:00
parent 915302c445
commit f6c88b773d
28 changed files with 163 additions and 1468 deletions
@@ -10,11 +10,6 @@ type FifoStockV2Service = fifoStockV2.Service
type FifoStockV2Lane = fifoStockV2.Lane
const (
FifoStockV2LaneStockable FifoStockV2Lane = fifoStockV2.LaneStockable
FifoStockV2LaneUsable FifoStockV2Lane = fifoStockV2.LaneUsable
)
type FifoStockV2Ref = fifoStockV2.Ref
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)
VALUES
('AYAM', 'AYAM', 5),
('DOC', 'AYAM', 10),
('PULLET', 'AYAM', 20),
('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",
Price: 7500,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagAyam},
Flags: []utils.FlagType{utils.FlagDOC, utils.FlagPullet, utils.FlagLayer},
IsVisible: true,
},
{
@@ -58,17 +58,17 @@ type SapronakReportDTO struct {
// Simplified view for project-level sapronak response
type SapronakCategoryRowDTO struct {
ID int `json:"id"`
Date string `json:"date"`
ReferenceNumber string `json:"reference_number"`
QtyIn float64 `json:"qty_in"`
QtyOut float64 `json:"qty_out"`
QtyUsed float64 `json:"qty_used"`
Description string `json:"description"`
ID int `json:"id"`
Date string `json:"date"`
ReferenceNumber string `json:"reference_number"`
QtyIn float64 `json:"qty_in"`
QtyOut float64 `json:"qty_out"`
QtyUsed float64 `json:"qty_used"`
Description string `json:"description"`
ProductCategory []string `json:"product_category"`
UnitPrice float64 `json:"unit_price"`
TotalAmount float64 `json:"total_amount"`
Notes string `json:"notes"`
UnitPrice float64 `json:"unit_price"`
TotalAmount float64 `json:"total_amount"`
Notes string `json:"notes"`
}
type SapronakCategoryTotalDTO struct {
@@ -148,7 +148,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
normalizeFlag := func(raw string) string {
normalized := strings.ToUpper(strings.TrimSpace(raw))
if normalized == "AYAM" || normalized == "PULLET" {
if normalized == "PULLET" {
return "DOC"
}
return normalized
@@ -177,7 +177,6 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
}
flagOrder := map[string]int{
"AYAM": 0,
"DOC": 0,
"PAKAN": 0,
"OVK": 0,
@@ -446,7 +446,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -459,7 +459,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -495,7 +495,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -508,7 +508,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -545,7 +545,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -558,7 +558,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -595,7 +595,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -608,7 +608,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -645,7 +645,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -658,7 +658,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -685,7 +685,7 @@ WHERE pw.warehouse_id IN ?
FROM flags f
WHERE f.flagable_id = pw.product_id
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,
' ' ORDER BY
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
END,
f.name
@@ -715,7 +715,7 @@ SELECT
f.name,
' ' ORDER BY
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
END,
f.name
@@ -743,7 +743,7 @@ WHERE pw.project_flock_kandang_id IN ?
FROM flags f
WHERE f.flagable_id = pw.product_id
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 (
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)
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 {
@@ -808,8 +808,7 @@ func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlia
Where("flagable_type = ?", entity.FlagableTypeProduct).
Where("name IN ?", sapronakFlagsAll).
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",
utils.FlagAyam,
"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.FlagDOC,
utils.FlagPullet,
utils.FlagPakan,
@@ -1239,7 +1238,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
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")
outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
outgoing, err := scanAndGroupDetails(outgoingQuery)
@@ -856,7 +856,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
// 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)
if err != nil {
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
}
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age)
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age)
if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording
}
@@ -432,8 +432,8 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
return true
}
candidate := strings.ToUpper(f)
if filterFlag == "AYAM" || filterFlag == "DOC" || filterFlag == "PULLET" {
return candidate == "AYAM" || candidate == "DOC" || candidate == "PULLET"
if filterFlag == "DOC" || filterFlag == "PULLET" {
return candidate == "DOC" || candidate == "PULLET"
}
return candidate == filterFlag
}
@@ -474,8 +474,7 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if !isLaying {
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
for _, row := range chickinUsageRows {
flag := strings.ToUpper(row.Flag)
if flag == "AYAM" || flag == "DOC" {
if strings.ToUpper(row.Flag) == "DOC" {
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))
for pid, rows := range chickinUsageDetailsRows {
for _, d := range rows {
flag := strings.ToUpper(d.Flag)
if flag == "AYAM" || flag == "DOC" {
if strings.ToUpper(d.Flag) == "DOC" {
filteredDetail[pid] = append(filteredDetail[pid], d)
}
}
@@ -5,5 +5,5 @@ type CountSapronakQuery struct {
KandangID uint `query:"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"`
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) {
flagSet := make(map[string]struct{})
for _, f := range utils.AllowedFlagTypes(utils.FlagGroupProduct) {
flagSet[string(f)] = struct{}{}
}
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)
flagList := make([]string, 0)
for f := range utils.AllFlagTypes() {
flagList = append(flagList, string(f))
}
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 {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
@@ -115,13 +78,7 @@ func (r *ConstantRepositoryImpl) GetConstants() (map[string]interface{}, error)
adjustmentSubtypesByType := utils.AdjustmentTransactionSubtypesByTypeForFrontend()
return map[string]interface{}{
"flags": flagList,
"legacy_flag_aliases": legacyFlagAliases,
"product_flag_mapping": map[string]interface{}{
"flags": productMainFlags,
"options": productFlagOptions,
"sub_flag_to_flag": productSubFlagToFlag,
},
"flags": flagList,
"warehouse_types": []string{
"AREA",
"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 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").
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 >= ? AND pi.received_date < ?", start, end)
@@ -179,7 +179,6 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, cu
flag == string(utils.FlagTelurPecah) ||
flag == string(utils.FlagTelurPutih) ||
flag == string(utils.FlagTelurRetak) ||
flag == string(utils.FlagAyam) ||
flag == string(utils.FlagAyamAfkir) ||
flag == string(utils.FlagAyamCulling) ||
flag == string(utils.FlagAyamMati) {
@@ -144,7 +144,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
for _, t := range marketingTypes {
switch t {
case string(utils.MarketingTypeAyamPullet):
flagSet[string(utils.FlagAyam)] = struct{}{}
flagSet[string(utils.FlagDOC)] = struct{}{}
flagSet[string(utils.FlagPullet)] = struct{}{}
flagSet[string(utils.FlagLayer)] = struct{}{}
@@ -50,16 +50,6 @@ type transferService struct {
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 {
return &transferService{
Log: utils.Log,
@@ -454,9 +444,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}
}
routePairsByProductID := map[uint]transferRoutePair{}
if len(req.Products) > 0 {
routePairsByProductID, err = s.resolveTransferRoutes(c.Context(), tx, req.Products)
pakanProducts := map[uint]bool{}
if s.FifoStockV2Svc != nil && len(req.Products) > 0 {
pakanProducts, err = s.resolvePakanProducts(c.Context(), tx, req.Products)
if err != nil {
return err
}
@@ -464,17 +454,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
for _, product := range req.Products {
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
outPendingQty := 0.0
useFifoV2 := s.FifoStockV2Svc != nil
useFifoV2 := s.FifoStockV2Svc != nil && pakanProducts[uint(product.ProductID)]
if useFifoV2 {
s.Log.Infof(
"[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,
)
reflowResult, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: routePair.Usable.FlagGroupCode,
FlagGroupCode: "PAKAN",
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Usable: commonSvc.FifoStockV2Ref{
ID: uint(detail.Id),
LegacyTypeKey: routePair.Usable.LegacyTypeKey,
FunctionCode: routePair.Usable.FunctionCode,
LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
FunctionCode: "STOCK_TRANSFER_OUT",
},
DesiredQty: product.ProductQty,
Tx: tx,
@@ -508,12 +491,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
outPendingQty,
)
} else {
usableKey := fifo.UsableKey(strings.TrimSpace(routePair.Usable.LegacyTypeKey))
if usableKey == "" {
usableKey = fifo.UsableKeyStockTransferOut
}
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: usableKey,
UsableKey: fifo.UsableKeyStockTransferOut,
UsableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Quantity: product.ProductQty,
@@ -574,12 +553,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
product.ProductQty,
)
}
stockableKey := fifo.StockableKey(strings.TrimSpace(routePair.Stockable.LegacyTypeKey))
if stockableKey == "" {
stockableKey = fifo.StockableKeyStockTransferIn
}
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: stockableKey,
StockableKey: fifo.StockableKeyStockTransferIn,
StockableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
Quantity: product.ProductQty,
@@ -682,72 +657,50 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return result, nil
}
func (s *transferService) resolveTransferRoutes(
func (s *transferService) resolvePakanProducts(
ctx context.Context,
tx *gorm.DB,
products []validation.TransferProduct,
) (map[uint]transferRoutePair, error) {
out := make(map[uint]transferRoutePair, len(products))
) (map[uint]bool, error) {
out := make(map[uint]bool, len(products))
if len(products) == 0 {
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 {
if product.ProductID == 0 {
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 {
usableRoute, err := commonSvc.ResolveFifoStockV2RouteByProductIDAndLane(
ctx,
tx,
productID,
transferFunctionCodeOut,
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return nil, err
}
if usableRoute == nil {
return nil, fiber.NewError(
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,
}
type row struct {
ProductID uint `gorm:"column:product_id"`
}
var rows []row
err := tx.WithContext(ctx).
Table("flags f").
Select("DISTINCT f.flagable_id AS product_id").
Where("f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}).
Where("f.flagable_id IN ?", productIDs).
Scan(&rows).Error
if err != nil {
return nil, err
}
for _, row := range rows {
out[row.ProductID] = true
}
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").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("flags.name IN (?)", []string{
string(utils.FlagAyam),
string(utils.FlagAyamAfkir),
string(utils.FlagAyamCulling),
string(utils.FlagPullet),
@@ -159,12 +158,9 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanByCategory(c
string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
string(utils.FlagTelurPapacal),
string(utils.FlagTelurJumbo),
})
} else {
db = db.Where("flags.name IN (?)", []string{
string(utils.FlagAyam),
string(utils.FlagDOC),
string(utils.FlagPullet),
string(utils.FlagLayer),
@@ -331,12 +327,12 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
switch filters.MarketingType {
case "ayam":
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":
db = db.Where("flags.name IN (?)", []string{
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":
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")
}
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{
UsableKey: usableKey,
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: marketingProduct.ProductWarehouseId,
Quantity: requestedQty,
@@ -624,27 +603,6 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
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)
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
if err != nil {
@@ -656,7 +614,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: usableKey,
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: tx,
}); err != nil {
@@ -48,8 +48,6 @@ type salesOrdersService struct {
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,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{
@@ -76,36 +74,6 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB {
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) {
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err
@@ -408,12 +376,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if qtyDiff < 0 {
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
} else if qtyDiff > 0 {
usableKey, err := s.resolveMarketingUsableKey(c.Context(), dbTransaction, rp.ProductWarehouseId)
if err != nil {
return err
}
_, err = s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: usableKey,
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: rp.ProductWarehouseId,
Quantity: qtyDiff,
@@ -474,13 +438,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if deliveryProduct.DeliveryDate != nil {
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{
UsableKey: usableKey,
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: dbTransaction,
}); err != nil {
@@ -562,22 +522,9 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
if len(marketing.Products) > 0 {
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
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 {
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{
UsableKey: usableKey,
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: dp.Id,
Tx: dbTransaction,
}); err != nil {
@@ -7,7 +7,6 @@ import (
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"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === DTO Structs ===
@@ -18,9 +17,6 @@ type ProductRelationDTO struct {
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
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"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []ProductSupplierDTO `json:"suppliers"`
@@ -35,9 +31,6 @@ type ProductListDTO struct {
SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,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"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
@@ -66,13 +59,6 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
for i, f := range e.Flags {
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
if e.Uom.Id != 0 {
@@ -91,9 +77,6 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
Name: e.Name,
ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice,
Flag: flag,
SubFlag: subFlag,
SubFlags: subFlagsRef,
Flags: &flags,
Uom: uomRef,
ProductCategory: categoryRef,
@@ -118,7 +101,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
for i, f := range e.Flags {
flags[i] = f.Name
}
flag, subFlag, subFlags := resolveProductFlagAndSubFlags(flags)
var uomRef *uomDTO.UomRelationDTO
if e.Uom.Id != 0 {
@@ -129,9 +111,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
return ProductListDTO{
Id: e.Id,
Name: e.Name,
Flag: flag,
SubFlag: subFlag,
SubFlags: subFlags,
Flags: flags,
Uom: uomRef,
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 {
if len(relations) == 0 {
return make([]ProductSupplierDTO, 0)
@@ -41,151 +41,6 @@ func normalizeProductFlags(raw []string) ([]string, error) {
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 {
return &productService{
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 {
return nil, flagErr
}
@@ -482,10 +337,13 @@ func (s productService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
flagUpdate bool
flagValues []string
)
var flagErr error
flagUpdate, flagValues, flagErr = resolveUpdateProductFlags(req)
if flagErr != nil {
return nil, flagErr
if req.Flags != nil {
flagUpdate = true
var flagErr error
flagValues, flagErr = normalizeProductFlags(*req.Flags)
if flagErr != nil {
return nil, flagErr
}
}
if len(updateBody) == 0 && !supplierUpdate && !flagUpdate {
@@ -6,37 +6,31 @@ type SupplierPrice struct {
}
type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
Sku *string `json:"sku,omitempty" validate:"omitempty,max=100"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"`
ProductPrice float64 `json:"product_price" validate:"required"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Name string `json:"name" validate:"required_strict,min=3,max=50"`
Brand string `json:"brand" validate:"required_strict,min=2,max=50"`
Sku *string `json:"sku,omitempty" validate:"omitempty,max=100"`
UomID uint `json:"uom_id" validate:"required,gt=0"`
ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"`
ProductPrice float64 `json:"product_price" validate:"required"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers []SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flag *string `json:"flag,omitempty" validate:"omitempty,max=50"`
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"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Brand *string `json:"brand,omitempty" validate:"omitempty,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID *uint `json:"uom_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"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Brand *string `json:"brand,omitempty" validate:"omitempty,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID *uint `json:"uom_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"`
SellingPrice *float64 `json:"selling_price,omitempty" validate:"omitempty"`
Tax *float64 `json:"tax,omitempty" validate:"omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty" validate:"omitempty,gt=0"`
Suppliers *[]SupplierPrice `json:"suppliers,omitempty" validate:"omitempty,dive"`
Flag *string `json:"flag,omitempty" validate:"omitempty,max=50"`
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"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive"`
}
type Query struct {
@@ -29,8 +29,6 @@ import (
var chickinUsableKey = fifo.UsableKeyProjectChickin
const chickinFunctionCodeOut = "CHICKIN_OUT"
type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, 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 {
requiredFlags := make([]utils.FlagType, 0, 2)
var requiredFlag utils.FlagType
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) {
requiredFlags = append(requiredFlags, utils.FlagAyam, utils.FlagPullet)
requiredFlag = utils.FlagPullet
} else {
return nil, fmt.Errorf("invalid flock category for chickin")
}
hasRequiredFlag := false
for _, flag := range productWarehouse.Product.Flags {
currentFlag := utils.FlagType(flag.Name)
for _, requiredFlag := range requiredFlags {
if currentFlag == requiredFlag {
hasRequiredFlag = true
break
}
}
if hasRequiredFlag {
if utils.FlagType(flag.Name) == requiredFlag {
hasRequiredFlag = true
break
}
}
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, requiredText, 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, requiredFlag, productWarehouse.Product.Id, productWarehouse.Id)
}
}
@@ -492,9 +483,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
var targetFlag utils.FlagType
if category == string(utils.ProjectFlockCategoryGrowing) {
targetFlag = utils.FlagAyam
targetFlag = utils.FlagPullet
} else if category == string(utils.ProjectFlockCategoryLaying) {
targetFlag = utils.FlagAyam
targetFlag = utils.FlagLayer
} else {
continue
}
@@ -630,29 +621,8 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
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{
UsableKey: usableKey,
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
ProductWarehouseID: chickin.ProductWarehouseId,
Quantity: desiredQty,
@@ -718,34 +688,13 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
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
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{
UsableKey: usableKey,
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
Tx: tx,
}); err != nil {
@@ -312,7 +312,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return nil, err
}
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
}
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
}
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
}
actorID, err := m.ActorIDFromContext(c)
@@ -512,7 +512,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err
}
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
}
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
}
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
}
if err := ensureRecordingEggsUnused(existingEggs); err != nil {
@@ -21,93 +21,7 @@ import (
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
const (
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
}
const depletionUsageTolerance = 0.000001
func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) {
if s == nil || s.Log == nil {
@@ -211,20 +125,8 @@ func (s *recordingService) consumeRecordingStocks(
}
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{
UsableKey: usableKey,
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desiredTotal,
@@ -307,21 +209,9 @@ func (s *recordingService) consumeRecordingDepletions(
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
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: usableKey,
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
ProductWarehouseID: sourceWarehouseID,
Quantity: desired,
@@ -424,20 +314,8 @@ func (s *recordingService) releaseRecordingStocks(
if stock.Id == 0 {
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 {
activeCount, err := s.countActiveAllocations(ctx, tx, usableKey, stock.Id)
activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id)
if err != nil {
return err
}
@@ -448,16 +326,16 @@ func (s *recordingService) releaseRecordingStocks(
}
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
}
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
}
}
s.logStockTrace("release:start", stock, "")
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: usableKey,
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
Tx: tx,
}); err != nil {
@@ -522,28 +400,8 @@ func (s *recordingService) releaseRecordingDepletions(
if depletion.Id == 0 {
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 {
activeCount, err := s.countActiveAllocations(ctx, tx, usableKey, depletion.Id)
activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id)
if err != nil {
return err
}
@@ -560,10 +418,10 @@ func (s *recordingService) releaseRecordingDepletions(
}
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
}
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
}
}
@@ -573,8 +431,15 @@ func (s *recordingService) releaseRecordingDepletions(
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{
UsableKey: usableKey,
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
Tx: tx,
}); err != nil {
@@ -765,20 +630,9 @@ func (s *recordingService) replenishRecordingEggs(
if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
continue
}
stockableKey, err := s.resolveRecordingStockableKey(
ctx,
tx,
egg.ProductWarehouseId,
recordingFunctionCodeRecordingEggIn,
fifo.StockableKeyRecordingEgg,
"Recording Egg",
)
if err != nil {
return err
}
s.logEggTrace("replenish:start", egg, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: stockableKey,
StockableKey: fifo.StockableKeyRecordingEgg,
StockableID: egg.Id,
ProductWarehouseID: egg.ProductWarehouseId,
Quantity: float64(egg.Qty),
@@ -836,20 +690,9 @@ func (s *recordingService) replenishRecordingDepletions(
if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 {
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, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: stockableKey,
StockableKey: fifo.StockableKeyRecordingDepletion,
StockableID: depletion.Id,
ProductWarehouseID: depletion.ProductWarehouseId,
Quantity: depletion.Qty,
@@ -881,20 +724,9 @@ func (s *recordingService) reduceRecordingDepletions(
if depletion.Id == 0 || depletion.ProductWarehouseId == 0 || depletion.Qty <= 0 {
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, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: stockableKey,
StockableKey: fifo.StockableKeyRecordingDepletion,
StockableID: depletion.Id,
ProductWarehouseID: depletion.ProductWarehouseId,
Quantity: -depletion.Qty,
@@ -926,20 +758,9 @@ func (s *recordingService) reduceRecordingEggs(
if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 {
continue
}
stockableKey, err := s.resolveRecordingStockableKey(
ctx,
tx,
egg.ProductWarehouseId,
recordingFunctionCodeRecordingEggIn,
fifo.StockableKeyRecordingEgg,
"Recording Egg",
)
if err != nil {
return err
}
s.logEggTrace("reduce:start", egg, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: stockableKey,
StockableKey: fifo.StockableKeyRecordingEgg,
StockableID: egg.Id,
ProductWarehouseID: egg.ProductWarehouseId,
Quantity: -float64(egg.Qty),
@@ -43,8 +43,7 @@ type PurchaseService interface {
}
const (
priceTolerance = 0.0001
purchaseFunctionCodeIn = "PURCHASE_IN"
priceTolerance = 0.0001
)
type purchaseService struct {
@@ -406,14 +405,6 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
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
now := time.Now().UTC()
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")
}
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
completedAction := entity.ApprovalActionApproved
approvalSvc := commonSvc.NewApprovalService(
@@ -1854,15 +1834,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
productSupplierCache := make(map[uint]bool)
newItems := make([]*entity.PurchaseItem, 0, len(newPayloads))
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 {
if payload.ProductID == 0 || payload.WarehouseID == 0 {
@@ -1947,48 +1918,6 @@ func calculateTotalPrice(quantity float64, price float64, provided *float64, ref
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 {
if item == nil || item.Id == 0 || s.ApprovalSvc == nil {
return nil
@@ -92,7 +92,7 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[u
for _, flag := range mdp.MarketingProduct.ProductWarehouse.Product.Flags {
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 {
hasAyam = true
}
+9 -185
View File
@@ -14,18 +14,9 @@ type FlagType string
type FlagGroup string
type ProductFlagOption struct {
Flag FlagType `json:"flag"`
SubFlags []FlagType `json:"sub_flags"`
AllowWithoutSubFlag bool `json:"allow_without_sub_flag"`
}
const (
FlagIsActive FlagType = "IS_ACTIVE"
FlagAyam FlagType = "AYAM"
// Legacy AYAM flags kept for backward compatibility with existing production data.
FlagDOC FlagType = "DOC"
FlagPullet FlagType = "PULLET"
FlagLayer FlagType = "LAYER"
@@ -45,13 +36,11 @@ const (
FlagAyamMati FlagType = "AYAM-MATI"
//flag telur
FlagTelur FlagType = "TELUR"
FlagTelurUtuh FlagType = "TELUR-UTUH"
FlagTelurPecah FlagType = "TELUR-PECAH"
FlagTelurPutih FlagType = "TELUR-PUTIH"
FlagTelurRetak FlagType = "TELUR-RETAK"
FlagTelurPapacal FlagType = "TELUR-PAPACAL"
FlagTelurJumbo FlagType = "TELUR-JUMBO"
FlagTelur FlagType = "TELUR"
FlagTelurUtuh FlagType = "TELUR-UTUH"
FlagTelurPecah FlagType = "TELUR-PECAH"
FlagTelurPutih FlagType = "TELUR-PUTIH"
FlagTelurRetak FlagType = "TELUR-RETAK"
)
const (
@@ -61,10 +50,9 @@ const (
var flagGroupOptions = map[FlagGroup][]FlagType{
FlagGroupProduct: {
FlagAyam,
FlagAyamAfkir,
FlagAyamCulling,
FlagAyamMati,
FlagDOC,
FlagPullet,
FlagLayer,
FlagPakan,
FlagPreStarter,
FlagStarter,
@@ -73,82 +61,12 @@ var flagGroupOptions = map[FlagGroup][]FlagType{
FlagObat,
FlagVitamin,
FlagKimia,
FlagTelur,
FlagTelurUtuh,
FlagTelurPutih,
FlagTelurRetak,
FlagTelurPecah,
FlagTelurPapacal,
FlagTelurJumbo,
},
FlagGroupNonstock: {
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{} {
m := map[FlagType]struct{}{
FlagIsActive: {},
@@ -165,95 +83,6 @@ func AllFlagTypes() map[FlagType]struct{} {
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
// -------------------------------------------------------------------
@@ -792,11 +621,7 @@ const (
// -------------------------------------------------------------------
func IsValidFlagType(v string) bool {
flag := FlagType(strings.ToUpper(strings.TrimSpace(v)))
if _, ok := allFlagTypes[flag]; ok {
return true
}
_, ok := legacyFlagTypeAliases[flag]
_, ok := allFlagTypes[FlagType(strings.ToUpper(strings.TrimSpace(v)))]
return ok
}
@@ -842,7 +667,6 @@ func NormalizeFlagTypes(flags []string) []FlagType {
if normalized == "" {
continue
}
normalized = canonicalizeFlagType(normalized)
if _, exists := seen[normalized]; exists {
continue
}