mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Merge branch 'Feat/BE/implement-new-trf' into 'dev/fifo-v2'
fix: reimplement transfer to laying logics separating effective financial date... See merge request mbugroup/lti-api!361
This commit is contained in:
@@ -141,6 +141,9 @@ func (s *fifoStockV2Service) allocateInternal(ctx context.Context, tx *gorm.DB,
|
|||||||
if remaining <= 0 {
|
if remaining <= 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if shouldSkipStockableForUsable(req, lot.Ref.LegacyTypeKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if lot.AvailableQuantity <= 0 {
|
if lot.AvailableQuantity <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -207,6 +210,20 @@ func (s *fifoStockV2Service) allocateInternal(ctx context.Context, tx *gorm.DB,
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldSkipStockableForUsable(req AllocateRequest, stockableType string) bool {
|
||||||
|
usableType := strings.ToUpper(strings.TrimSpace(req.Usable.LegacyTypeKey))
|
||||||
|
functionCode := strings.ToUpper(strings.TrimSpace(req.Usable.FunctionCode))
|
||||||
|
stockable := strings.ToUpper(strings.TrimSpace(stockableType))
|
||||||
|
|
||||||
|
// CHICKIN_OUT must consume physical stock sources, not population lots,
|
||||||
|
// otherwise approved chickin can consume its own just-created population.
|
||||||
|
if (usableType == "PROJECT_CHICKIN" || functionCode == "CHICKIN_OUT") && stockable == "PROJECT_FLOCK_POPULATION" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *fifoStockV2Service) Rollback(ctx context.Context, req RollbackRequest) (*RollbackResult, error) {
|
func (s *fifoStockV2Service) Rollback(ctx context.Context, req RollbackRequest) (*RollbackResult, error) {
|
||||||
if err := s.validateRollbackRequest(req); err != nil {
|
if err := s.validateRollbackRequest(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_economic_cutoff_date;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS economic_cutoff_date;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN IF NOT EXISTS economic_cutoff_date DATE;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_economic_cutoff_date
|
||||||
|
ON laying_transfers(economic_cutoff_date);
|
||||||
|
|
||||||
|
UPDATE laying_transfers
|
||||||
|
SET economic_cutoff_date = COALESCE(economic_cutoff_date, effective_move_date, transfer_date)
|
||||||
|
WHERE economic_cutoff_date IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_route_rules
|
||||||
|
SET
|
||||||
|
is_active = TRUE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE flag_group_code = 'AYAM'
|
||||||
|
AND lane = 'USABLE'
|
||||||
|
AND function_code = 'TRANSFER_TO_LAYING_OUT'
|
||||||
|
AND source_table = 'laying_transfer_sources';
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_route_rules
|
||||||
|
SET
|
||||||
|
is_active = FALSE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE flag_group_code = 'AYAM'
|
||||||
|
AND lane = 'USABLE'
|
||||||
|
AND function_code = 'TRANSFER_TO_LAYING_OUT'
|
||||||
|
AND source_table = 'laying_transfers';
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_traits
|
||||||
|
SET is_active = TRUE
|
||||||
|
WHERE source_table = 'laying_transfer_sources'
|
||||||
|
AND lane = 'USABLE';
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_traits
|
||||||
|
SET is_active = FALSE
|
||||||
|
WHERE source_table = 'laying_transfers'
|
||||||
|
AND lane = 'USABLE';
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_source_project_flock_kandang_id;
|
||||||
|
DROP INDEX IF EXISTS idx_laying_transfers_source_product_warehouse_id;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_laying_transfers_source_project_flock_kandang_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT fk_laying_transfers_source_project_flock_kandang_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_laying_transfers_source_product_warehouse_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP CONSTRAINT fk_laying_transfers_source_product_warehouse_id;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
DROP COLUMN IF EXISTS source_project_flock_kandang_id,
|
||||||
|
DROP COLUMN IF EXISTS source_product_warehouse_id,
|
||||||
|
DROP COLUMN IF EXISTS source_requested_qty,
|
||||||
|
DROP COLUMN IF EXISTS source_usage_qty,
|
||||||
|
DROP COLUMN IF EXISTS source_pending_usage_qty;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD COLUMN IF NOT EXISTS source_project_flock_kandang_id BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS source_product_warehouse_id BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS source_requested_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS source_usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS source_pending_usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_laying_transfers_source_project_flock_kandang_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_transfers_source_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (source_project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON DELETE RESTRICT
|
||||||
|
ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_laying_transfers_source_product_warehouse_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_transfers_source_product_warehouse_id
|
||||||
|
FOREIGN KEY (source_product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_source_project_flock_kandang_id
|
||||||
|
ON laying_transfers(source_project_flock_kandang_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_source_product_warehouse_id
|
||||||
|
ON laying_transfers(source_product_warehouse_id);
|
||||||
|
|
||||||
|
WITH single_source AS (
|
||||||
|
SELECT
|
||||||
|
lts.laying_transfer_id,
|
||||||
|
MIN(lts.source_project_flock_kandang_id) AS source_project_flock_kandang_id,
|
||||||
|
MIN(lts.product_warehouse_id) AS source_product_warehouse_id
|
||||||
|
FROM laying_transfer_sources lts
|
||||||
|
WHERE lts.deleted_at IS NULL
|
||||||
|
GROUP BY lts.laying_transfer_id
|
||||||
|
HAVING COUNT(*) = 1
|
||||||
|
)
|
||||||
|
UPDATE laying_transfers lt
|
||||||
|
SET
|
||||||
|
source_project_flock_kandang_id = ss.source_project_flock_kandang_id,
|
||||||
|
source_product_warehouse_id = ss.source_product_warehouse_id
|
||||||
|
FROM single_source ss
|
||||||
|
WHERE lt.id = ss.laying_transfer_id
|
||||||
|
AND (lt.source_project_flock_kandang_id IS NULL OR lt.source_project_flock_kandang_id = 0);
|
||||||
|
|
||||||
|
WITH source_totals AS (
|
||||||
|
SELECT
|
||||||
|
laying_transfer_id,
|
||||||
|
COALESCE(SUM(requested_qty), 0) AS requested_qty,
|
||||||
|
COALESCE(SUM(usage_qty), 0) AS usage_qty,
|
||||||
|
COALESCE(SUM(pending_usage_qty), 0) AS pending_qty
|
||||||
|
FROM laying_transfer_sources
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
GROUP BY laying_transfer_id
|
||||||
|
)
|
||||||
|
UPDATE laying_transfers lt
|
||||||
|
SET
|
||||||
|
source_requested_qty = CASE
|
||||||
|
WHEN lt.source_requested_qty = 0 THEN st.requested_qty
|
||||||
|
ELSE lt.source_requested_qty
|
||||||
|
END,
|
||||||
|
source_usage_qty = CASE
|
||||||
|
WHEN lt.source_usage_qty = 0 THEN st.usage_qty
|
||||||
|
ELSE lt.source_usage_qty
|
||||||
|
END,
|
||||||
|
source_pending_usage_qty = CASE
|
||||||
|
WHEN lt.source_pending_usage_qty = 0 THEN st.pending_qty
|
||||||
|
ELSE lt.source_pending_usage_qty
|
||||||
|
END
|
||||||
|
FROM source_totals st
|
||||||
|
WHERE lt.id = st.laying_transfer_id;
|
||||||
|
|
||||||
|
WITH target_totals AS (
|
||||||
|
SELECT
|
||||||
|
laying_transfer_id,
|
||||||
|
COALESCE(SUM(total_qty), 0) AS total_qty
|
||||||
|
FROM laying_transfer_targets
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
GROUP BY laying_transfer_id
|
||||||
|
)
|
||||||
|
UPDATE laying_transfers lt
|
||||||
|
SET source_requested_qty = tt.total_qty
|
||||||
|
FROM target_totals tt
|
||||||
|
WHERE lt.id = tt.laying_transfer_id
|
||||||
|
AND (lt.source_requested_qty IS NULL OR lt.source_requested_qty = 0);
|
||||||
|
|
||||||
|
INSERT INTO fifo_stock_v2_traits(
|
||||||
|
source_table,
|
||||||
|
lane,
|
||||||
|
date_table,
|
||||||
|
date_join_left_col,
|
||||||
|
date_join_right_col,
|
||||||
|
date_column,
|
||||||
|
fallback_date_column,
|
||||||
|
sort_priority,
|
||||||
|
id_column
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('laying_transfers', 'USABLE', NULL, NULL, NULL, 'transfer_date', NULL, 25, 'id')
|
||||||
|
ON CONFLICT (source_table, lane) DO UPDATE
|
||||||
|
SET
|
||||||
|
date_table = EXCLUDED.date_table,
|
||||||
|
date_join_left_col = EXCLUDED.date_join_left_col,
|
||||||
|
date_join_right_col = EXCLUDED.date_join_right_col,
|
||||||
|
date_column = EXCLUDED.date_column,
|
||||||
|
fallback_date_column = EXCLUDED.fallback_date_column,
|
||||||
|
sort_priority = EXCLUDED.sort_priority,
|
||||||
|
id_column = EXCLUDED.id_column,
|
||||||
|
is_active = TRUE;
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_traits
|
||||||
|
SET is_active = FALSE
|
||||||
|
WHERE source_table = 'laying_transfer_sources'
|
||||||
|
AND lane = 'USABLE';
|
||||||
|
|
||||||
|
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_transfers', 'id', 'source_product_warehouse_id', 'source_usage_qty', NULL, 'source_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_route_rules
|
||||||
|
SET
|
||||||
|
is_active = FALSE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE flag_group_code = 'AYAM'
|
||||||
|
AND lane = 'USABLE'
|
||||||
|
AND function_code = 'TRANSFER_TO_LAYING_OUT'
|
||||||
|
AND source_table = 'laying_transfer_sources';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_route_rules
|
||||||
|
SET
|
||||||
|
is_active = FALSE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE flag_group_code = 'AYAM'
|
||||||
|
AND lane = 'STOCKABLE'
|
||||||
|
AND function_code = 'POPULATION_IN'
|
||||||
|
AND source_table = 'project_flock_populations'
|
||||||
|
AND legacy_type_key = 'PROJECT_FLOCK_POPULATION';
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_traits
|
||||||
|
SET is_active = FALSE
|
||||||
|
WHERE source_table = 'project_flock_populations'
|
||||||
|
AND lane = 'STOCKABLE';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
INSERT INTO fifo_stock_v2_traits(
|
||||||
|
source_table,
|
||||||
|
lane,
|
||||||
|
date_table,
|
||||||
|
date_join_left_col,
|
||||||
|
date_join_right_col,
|
||||||
|
date_column,
|
||||||
|
fallback_date_column,
|
||||||
|
sort_priority,
|
||||||
|
id_column,
|
||||||
|
is_active
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
('project_flock_populations', 'STOCKABLE', 'project_chickins', 'project_chickin_id', 'id', 'chick_in_date', 'created_at', 55, 'id', TRUE)
|
||||||
|
ON CONFLICT (source_table, lane) DO UPDATE
|
||||||
|
SET
|
||||||
|
date_table = EXCLUDED.date_table,
|
||||||
|
date_join_left_col = EXCLUDED.date_join_left_col,
|
||||||
|
date_join_right_col = EXCLUDED.date_join_right_col,
|
||||||
|
date_column = EXCLUDED.date_column,
|
||||||
|
fallback_date_column = EXCLUDED.fallback_date_column,
|
||||||
|
sort_priority = EXCLUDED.sort_priority,
|
||||||
|
id_column = EXCLUDED.id_column,
|
||||||
|
is_active = TRUE;
|
||||||
|
|
||||||
|
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', 'STOCKABLE', 'POPULATION_IN', 'project_flock_populations', 'id', 'product_warehouse_id', 'total_qty', 'total_used_qty', NULL, NULL, 'PROJECT_FLOCK_POPULATION', 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 project_flock_populations p
|
||||||
|
SET total_used_qty = COALESCE(a.used, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, SUM(qty) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||||
|
AND status = 'ACTIVE'
|
||||||
|
AND allocation_purpose = 'CONSUME'
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) a
|
||||||
|
WHERE p.id = a.stockable_id;
|
||||||
|
|
||||||
|
UPDATE project_flock_populations p
|
||||||
|
SET total_used_qty = 0
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_allocations sa
|
||||||
|
WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||||
|
AND sa.status = 'ACTIVE'
|
||||||
|
AND sa.allocation_purpose = 'CONSUME'
|
||||||
|
AND sa.stockable_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE fifo_stock_v2_route_rules
|
||||||
|
SET
|
||||||
|
is_active = FALSE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE flag_group_code = 'AYAM'
|
||||||
|
AND lane = 'USABLE'
|
||||||
|
AND function_code = 'CHICKIN_OUT'
|
||||||
|
AND source_table = 'project_chickins';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
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', 'CHICKIN_OUT', 'project_chickins', 'id', 'product_warehouse_id', 'usage_qty', NULL, 'pending_usage_qty', 'deleted_at IS NULL', 'PROJECT_CHICKIN', 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();
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -7,25 +7,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type LayingTransfer struct {
|
type LayingTransfer struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
TransferNumber string `gorm:"uniqueIndex;not null"`
|
TransferNumber string `gorm:"uniqueIndex;not null"`
|
||||||
FromProjectFlockId uint `gorm:"not null"`
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
ToProjectFlockId uint `gorm:"not null"`
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
TransferDate time.Time `gorm:"type:date;not null"`
|
SourceProjectFlockKandangId *uint `gorm:"index"`
|
||||||
EffectiveMoveDate *time.Time `gorm:"type:date"`
|
SourceProductWarehouseId *uint `gorm:"index"`
|
||||||
ExecutedAt *time.Time `gorm:"type:timestamptz"`
|
SourceRequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||||
ExecutedBy *uint `gorm:"index"`
|
SourceUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||||
Notes string `gorm:"type:text"`
|
SourcePendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
EconomicCutoffDate *time.Time `gorm:"type:date"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
EffectiveMoveDate *time.Time `gorm:"type:date"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
ExecutedAt *time.Time `gorm:"type:timestamptz"`
|
||||||
|
ExecutedBy *uint `gorm:"index"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
|
||||||
ExecutedUser *User `gorm:"foreignKey:ExecutedBy;references:Id"`
|
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseId;references:Id"`
|
||||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
ExecutedUser *User `gorm:"foreignKey:ExecutedBy;references:Id"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,4 +43,6 @@ type Recording struct {
|
|||||||
StandardEggMass *float64 `gorm:"-"`
|
StandardEggMass *float64 `gorm:"-"`
|
||||||
StandardEggWeight *float64 `gorm:"-"`
|
StandardEggWeight *float64 `gorm:"-"`
|
||||||
StandardFcr *float64 `gorm:"-"`
|
StandardFcr *float64 `gorm:"-"`
|
||||||
|
PopulationCanChange *bool `gorm:"-"`
|
||||||
|
TransferExecuted *bool `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1291,8 +1291,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
|||||||
COALESCE(p.product_price, 0) AS price
|
COALESCE(p.product_price, 0) AS price
|
||||||
`).
|
`).
|
||||||
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
||||||
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
|
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lt.source_product_warehouse_id").
|
||||||
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id").
|
|
||||||
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
|
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
|
||||||
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
|
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
|
||||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||||
@@ -1352,8 +1351,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
|||||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||||
COALESCE(p.product_price, 0) AS price
|
COALESCE(p.product_price, 0) AS price
|
||||||
`).
|
`).
|
||||||
Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
Joins("JOIN laying_transfers lt ON lt.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
||||||
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
|
|
||||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
|
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
|
||||||
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
|
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
|
||||||
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
|
||||||
@@ -1365,7 +1363,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
|||||||
Where("w.kandang_id = ?", kandangID).
|
Where("w.kandang_id = ?", kandangID).
|
||||||
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
|
||||||
Where("f.name IN ?", sapronakFlagsAll).
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
|
Group("lt.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
|
||||||
outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p")
|
outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p")
|
||||||
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
|
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
productRepo := rproduct.NewProductRepository(db)
|
productRepo := rproduct.NewProductRepository(db)
|
||||||
adjustmentStockRepo := rAdjustmentStock.NewAdjustmentStockRepository(db)
|
adjustmentStockRepo := rAdjustmentStock.NewAdjustmentStockRepository(db)
|
||||||
@@ -40,6 +41,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
fifoStockV2Service,
|
fifoStockV2Service,
|
||||||
validate,
|
validate,
|
||||||
projectFlockKandangRepo,
|
projectFlockKandangRepo,
|
||||||
|
projectFlockPopulationRepo,
|
||||||
)
|
)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
adjustmentStockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
|
adjustmentStockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories"
|
||||||
@@ -21,6 +22,7 @@ import (
|
|||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,15 +33,16 @@ type AdjustmentService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type adjustmentService struct {
|
type adjustmentService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||||
ProductRepo productRepo.ProductRepository
|
ProductRepo productRepo.ProductRepository
|
||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
AdjustmentStockRepository adjustmentStockRepo.AdjustmentStockRepository
|
ProjectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository
|
||||||
FifoStockV2Svc common.FifoStockV2Service
|
AdjustmentStockRepository adjustmentStockRepo.AdjustmentStockRepository
|
||||||
|
FifoStockV2Svc common.FifoStockV2Service
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -57,17 +60,19 @@ func NewAdjustmentService(
|
|||||||
fifoStockV2Svc common.FifoStockV2Service,
|
fifoStockV2Svc common.FifoStockV2Service,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||||
|
projectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository,
|
||||||
) AdjustmentService {
|
) AdjustmentService {
|
||||||
return &adjustmentService{
|
return &adjustmentService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
StockLogsRepository: stockLogsRepo,
|
StockLogsRepository: stockLogsRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
ProductRepo: productRepo,
|
ProductRepo: productRepo,
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
AdjustmentStockRepository: adjustmentStockRepo,
|
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
|
||||||
FifoStockV2Svc: fifoStockV2Svc,
|
AdjustmentStockRepository: adjustmentStockRepo,
|
||||||
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +314,22 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion destination adjustment stock")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion destination adjustment stock")
|
||||||
}
|
}
|
||||||
|
consumedPopulationQty := refreshedSource.UsageQty + refreshedSource.PendingQty
|
||||||
|
if consumedPopulationQty > 0 {
|
||||||
|
if err := s.allocatePopulationForDepletionAdjustment(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
*projectFlockKandangID,
|
||||||
|
sourcePW.Id,
|
||||||
|
refreshedSource.Id,
|
||||||
|
consumedPopulationQty,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.resyncProjectFlockPopulationUsage(ctx, tx, *projectFlockKandangID); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to resync project flock population usage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.createAdjustmentStockLog(
|
if err := s.createAdjustmentStockLog(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -614,6 +635,98 @@ func (s *adjustmentService) createAdjustmentStockLog(
|
|||||||
return stockLogRepo.CreateOne(ctx, newLog, nil)
|
return stockLogRepo.CreateOne(ctx, newLog, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *adjustmentService) allocatePopulationForDepletionAdjustment(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
projectFlockKandangID uint,
|
||||||
|
sourceProductWarehouseID uint,
|
||||||
|
adjustmentID uint,
|
||||||
|
consumeQty float64,
|
||||||
|
) error {
|
||||||
|
if consumeQty <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return errors.New("transaction is required")
|
||||||
|
}
|
||||||
|
if projectFlockKandangID == 0 || sourceProductWarehouseID == 0 || adjustmentID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid depletion adjustment population context")
|
||||||
|
}
|
||||||
|
if s.ProjectFlockPopulationRepo == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
popRepoTx := s.ProjectFlockPopulationRepo.WithTx(tx)
|
||||||
|
populations, err := popRepoTx.GetByProjectFlockKandangIDAndProductWarehouseID(ctx, projectFlockKandangID, sourceProductWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(populations) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk depletion adjustment")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fifoV2.AllocatePopulationConsumption(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
populations,
|
||||||
|
sourceProductWarehouseID,
|
||||||
|
fifo.UsableKeyAdjustmentOut.String(),
|
||||||
|
adjustmentID,
|
||||||
|
consumeQty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *adjustmentService) resyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||||
|
if tx == nil || projectFlockKandangID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idsSubquery := `
|
||||||
|
SELECT pfp.id
|
||||||
|
FROM project_flock_populations pfp
|
||||||
|
JOIN project_chickins pc ON pc.id = pfp.project_chickin_id
|
||||||
|
WHERE pc.project_flock_kandang_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
updateWithAlloc := `
|
||||||
|
UPDATE project_flock_populations p
|
||||||
|
SET total_used_qty = COALESCE(a.used, 0)
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, SUM(qty) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||||
|
AND status = 'ACTIVE'
|
||||||
|
AND allocation_purpose = 'CONSUME'
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) a
|
||||||
|
WHERE p.id = a.stockable_id
|
||||||
|
AND p.id IN (` + idsSubquery + `)
|
||||||
|
`
|
||||||
|
|
||||||
|
resetMissing := `
|
||||||
|
UPDATE project_flock_populations p
|
||||||
|
SET total_used_qty = 0
|
||||||
|
WHERE p.id IN (` + idsSubquery + `)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_allocations sa
|
||||||
|
WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||||
|
AND sa.status = 'ACTIVE'
|
||||||
|
AND sa.allocation_purpose = 'CONSUME'
|
||||||
|
AND sa.stockable_id = p.id
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
db := tx.WithContext(ctx)
|
||||||
|
if err := db.Exec(updateWithAlloc, projectFlockKandangID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Exec(resetMissing, projectFlockKandangID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
|
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
|
||||||
if err := s.Validate.Struct(query); err != nil {
|
if err := s.Validate.Struct(query); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
@@ -26,11 +26,15 @@ import (
|
|||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/jackc/pgconn"
|
||||||
|
pgconnv5 "github.com/jackc/pgx/v5/pgconn"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena sudah memiliki population. Lakukan rollback/penyesuaian population terlebih dahulu"
|
||||||
|
|
||||||
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)
|
||||||
@@ -189,31 +193,31 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d belongs to different flock. Only product warehouses with project_flock_kandang_id = NULL or = %d can be used", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d belongs to different flock. Only product warehouses with project_flock_kandang_id = NULL or = %d can be used", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if productWarehouse.Product.Id != 0 {
|
if productWarehouse.Product.Id != 0 {
|
||||||
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
||||||
if category != string(utils.ProjectFlockCategoryGrowing) && category != string(utils.ProjectFlockCategoryLaying) {
|
if category != string(utils.ProjectFlockCategoryGrowing) && category != string(utils.ProjectFlockCategoryLaying) {
|
||||||
return nil, fmt.Errorf("invalid flock category for chickin")
|
return nil, fmt.Errorf("invalid flock category for chickin")
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAyamFlag := false
|
hasAyamFlag := false
|
||||||
for _, flag := range productWarehouse.Product.Flags {
|
for _, flag := range productWarehouse.Product.Flags {
|
||||||
if utils.CanonicalFlagType(flag.Name) == utils.FlagAyam {
|
if utils.CanonicalFlagType(flag.Name) == utils.FlagAyam {
|
||||||
hasAyamFlag = true
|
hasAyamFlag = true
|
||||||
break
|
break
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasAyamFlag {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"product warehouse %d cannot be used for %s chickin. Product must have AYAM flag (or legacy alias DOC/PULLET/LAYER) (product ID: %d, warehouse ID: %d)",
|
|
||||||
chickinReq.ProductWarehouseId,
|
|
||||||
projectFlockKandang.ProjectFlock.Category,
|
|
||||||
productWarehouse.Product.Id,
|
|
||||||
productWarehouse.Id,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !hasAyamFlag {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"product warehouse %d cannot be used for %s chickin. Product must have AYAM flag (or legacy alias DOC/PULLET/LAYER) (product ID: %d, warehouse ID: %d)",
|
||||||
|
chickinReq.ProductWarehouseId,
|
||||||
|
projectFlockKandang.ProjectFlock.Category,
|
||||||
|
productWarehouse.Product.Id,
|
||||||
|
productWarehouse.Id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate)
|
chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||||
@@ -421,6 +425,14 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
if err := s.ensureNotTransferred(c.Context(), chickin.ProjectFlockKandangId); err != nil {
|
if err := s.ensureNotTransferred(c.Context(), chickin.ProjectFlockKandangId); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
hasPopulation, err := s.ProjectflockPopulationRepo.ExistsByProjectChickinID(c.Context(), chickin.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to check population by chickin %d: %+v", chickin.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi population chickin")
|
||||||
|
}
|
||||||
|
if hasPopulation {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, chickinDeletePopulationGuardMessage)
|
||||||
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -429,17 +441,35 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
chickinRepoTx := repository.NewChickinRepository(tx)
|
chickinRepoTx := repository.NewChickinRepository(tx)
|
||||||
|
|
||||||
if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 {
|
if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 {
|
||||||
if err := s.ReleaseChickinStocks(c.Context(), tx, chickin, actorID); err != nil {
|
if err := s.ReleaseChickinStocks(c.Context(), tx, chickin, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
|
note := "delete chickin rollback"
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("usable_type = ? AND usable_id = ? AND status = ?",
|
||||||
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
|
chickin.Id,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": entity.StockAllocationStatusReleased,
|
||||||
|
"released_at": now,
|
||||||
|
"note": note,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal release alokasi FIFO chickin")
|
||||||
|
}
|
||||||
|
|
||||||
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
}
|
}
|
||||||
|
if isForeignKeyViolation(err) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, chickinDeletePopulationGuardMessage)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +489,24 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isForeignKeyViolation(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if errors.As(err, &pgErr) {
|
||||||
|
return pgErr.Code == "23503"
|
||||||
|
}
|
||||||
|
|
||||||
|
var pgErrV5 *pgconnv5.PgError
|
||||||
|
if errors.As(err, &pgErrV5) {
|
||||||
|
return pgErrV5.Code == "23503"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
|
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -69,22 +69,24 @@ type RecordingWarehouseDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RecordingRelationDTO struct {
|
type RecordingRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlock RecordingProjectFlockDTO `json:"project_flock"`
|
ProjectFlock RecordingProjectFlockDTO `json:"project_flock"`
|
||||||
RecordDatetime time.Time `json:"record_datetime"`
|
RecordDatetime time.Time `json:"record_datetime"`
|
||||||
Day int `json:"day"`
|
Day int `json:"day"`
|
||||||
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
||||||
TotalDepletionCumQty float64 `json:"total_depletion_cum_qty"`
|
TotalDepletionCumQty float64 `json:"total_depletion_cum_qty"`
|
||||||
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
||||||
DepletionRate float64 `json:"depletion_rate"`
|
DepletionRate float64 `json:"depletion_rate"`
|
||||||
CumIntake int `json:"cum_intake"`
|
CumIntake int `json:"cum_intake"`
|
||||||
FcrValue float64 `json:"fcr_value"`
|
FcrValue float64 `json:"fcr_value"`
|
||||||
HenDay float64 `json:"hen_day"`
|
HenDay float64 `json:"hen_day"`
|
||||||
HenHouse float64 `json:"hen_house"`
|
HenHouse float64 `json:"hen_house"`
|
||||||
FeedIntake float64 `json:"feed_intake"`
|
FeedIntake float64 `json:"feed_intake"`
|
||||||
EggMass float64 `json:"egg_mass"`
|
EggMass float64 `json:"egg_mass"`
|
||||||
EggWeight float64 `json:"egg_weight"`
|
EggWeight float64 `json:"egg_weight"`
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
PopulationCanChange bool `json:"population_can_change"`
|
||||||
|
TransferExecuted *bool `json:"transfer_executed,omitempty"`
|
||||||
|
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingListDTO struct {
|
type RecordingListDTO struct {
|
||||||
@@ -228,22 +230,24 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return RecordingRelationDTO{
|
return RecordingRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlock: toRecordingProjectFlockDTO(e),
|
ProjectFlock: toRecordingProjectFlockDTO(e),
|
||||||
RecordDatetime: e.RecordDatetime,
|
RecordDatetime: e.RecordDatetime,
|
||||||
Day: intValue(e.Day),
|
Day: intValue(e.Day),
|
||||||
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
||||||
TotalDepletionCumQty: floatValue(e.TotalDepletionCumQty),
|
TotalDepletionCumQty: floatValue(e.TotalDepletionCumQty),
|
||||||
CumDepletionRate: roundFloatValue(e.CumDepletionRate, 2),
|
CumDepletionRate: roundFloatValue(e.CumDepletionRate, 2),
|
||||||
DepletionRate: roundFloatValue(e.DepletionRate, 2),
|
DepletionRate: roundFloatValue(e.DepletionRate, 2),
|
||||||
CumIntake: intValue(e.CumIntake),
|
CumIntake: intValue(e.CumIntake),
|
||||||
FcrValue: floatValue(e.FcrValue),
|
FcrValue: floatValue(e.FcrValue),
|
||||||
HenDay: floatValue(e.HenDay),
|
HenDay: floatValue(e.HenDay),
|
||||||
HenHouse: floatValue(e.HenHouse),
|
HenHouse: floatValue(e.HenHouse),
|
||||||
FeedIntake: floatValue(e.FeedIntake),
|
FeedIntake: floatValue(e.FeedIntake),
|
||||||
EggMass: floatValue(e.EggMass),
|
EggMass: floatValue(e.EggMass),
|
||||||
EggWeight: floatValue(e.EggWeight),
|
EggWeight: floatValue(e.EggWeight),
|
||||||
Approval: latestApproval,
|
PopulationCanChange: boolValueDefault(e.PopulationCanChange, true),
|
||||||
|
TransferExecuted: e.TransferExecuted,
|
||||||
|
Approval: latestApproval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,6 +453,13 @@ func intValue(value *int) int {
|
|||||||
return *value
|
return *value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func boolValueDefault(value *bool, fallback bool) bool {
|
||||||
|
if value == nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
||||||
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
|
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
|
||||||
result := approvalDTO.ApprovalRelationDTO{}
|
result := approvalDTO.ApprovalRelationDTO{}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package recordings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -24,8 +25,10 @@ import (
|
|||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||||
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
|
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -48,6 +51,8 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
chickinRepo := rChickin.NewChickinRepository(db)
|
chickinRepo := rChickin.NewChickinRepository(db)
|
||||||
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
|
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
|
||||||
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
|
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
|
||||||
|
layingTransferSourceRepo := rTransferLaying.NewLayingTransferSourceRepository(db)
|
||||||
|
layingTransferTargetRepo := rTransferLaying.NewLayingTransferTargetRepository(db)
|
||||||
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
||||||
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
||||||
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||||
@@ -61,6 +66,42 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
)
|
)
|
||||||
|
|
||||||
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
|
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||||
|
|
||||||
|
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
|
Key: fifo.StockableKeyTransferToLayingIn,
|
||||||
|
Table: "laying_transfer_targets",
|
||||||
|
Columns: fifo.StockableColumns{
|
||||||
|
ID: "id",
|
||||||
|
ProductWarehouseID: "product_warehouse_id",
|
||||||
|
TotalQuantity: "total_qty",
|
||||||
|
TotalUsedQuantity: "total_used",
|
||||||
|
CreatedAt: "created_at",
|
||||||
|
},
|
||||||
|
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||||
|
}); err != nil {
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||||
|
panic(fmt.Sprintf("failed to register transfer to laying stockable workflow: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
|
Key: fifo.UsableKeyTransferToLayingOut,
|
||||||
|
Table: "laying_transfers",
|
||||||
|
Columns: fifo.UsableColumns{
|
||||||
|
ID: "id",
|
||||||
|
ProductWarehouseID: "source_product_warehouse_id",
|
||||||
|
UsageQuantity: "source_usage_qty",
|
||||||
|
PendingQuantity: "source_pending_usage_qty",
|
||||||
|
CreatedAt: "created_at",
|
||||||
|
},
|
||||||
|
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||||
|
}); err != nil {
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||||
|
panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
@@ -103,6 +144,21 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
fifoStockV2Service,
|
fifoStockV2Service,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
transferLayingService := sTransferLaying.NewTransferLayingService(
|
||||||
|
transferLayingRepo,
|
||||||
|
layingTransferSourceRepo,
|
||||||
|
layingTransferTargetRepo,
|
||||||
|
projectFlockRepo,
|
||||||
|
projectFlockKandangRepo,
|
||||||
|
projectFlockPopulationRepo,
|
||||||
|
productWarehouseRepo,
|
||||||
|
warehouseRepo,
|
||||||
|
approvalService,
|
||||||
|
fifoService,
|
||||||
|
fifoStockV2Service,
|
||||||
|
validate,
|
||||||
|
)
|
||||||
|
|
||||||
recordingService := sRecording.NewRecordingService(
|
recordingService := sRecording.NewRecordingService(
|
||||||
recordingRepo,
|
recordingRepo,
|
||||||
projectFlockKandangRepo,
|
projectFlockKandangRepo,
|
||||||
@@ -116,6 +172,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
projectFlockService,
|
projectFlockService,
|
||||||
chickinService,
|
chickinService,
|
||||||
transferLayingRepo,
|
transferLayingRepo,
|
||||||
|
transferLayingService,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
|
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -56,6 +57,7 @@ type recordingService struct {
|
|||||||
ProjectFlockSvc sProjectFlock.ProjectflockService
|
ProjectFlockSvc sProjectFlock.ProjectflockService
|
||||||
ChickinSvc sChickin.ChickinService
|
ChickinSvc sChickin.ChickinService
|
||||||
TransferLayingRepo rTransferLaying.TransferLayingRepository
|
TransferLayingRepo rTransferLaying.TransferLayingRepository
|
||||||
|
TransferLayingSvc sTransferLaying.TransferLayingService
|
||||||
FifoStockV2Svc commonSvc.FifoStockV2Service
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ func NewRecordingService(
|
|||||||
projectFlockSvc sProjectFlock.ProjectflockService,
|
projectFlockSvc sProjectFlock.ProjectflockService,
|
||||||
chickinSvc sChickin.ChickinService,
|
chickinSvc sChickin.ChickinService,
|
||||||
transferLayingRepo rTransferLaying.TransferLayingRepository,
|
transferLayingRepo rTransferLaying.TransferLayingRepository,
|
||||||
|
transferLayingSvc sTransferLaying.TransferLayingService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) RecordingService {
|
) RecordingService {
|
||||||
return &recordingService{
|
return &recordingService{
|
||||||
@@ -88,6 +91,7 @@ func NewRecordingService(
|
|||||||
ProjectFlockSvc: projectFlockSvc,
|
ProjectFlockSvc: projectFlockSvc,
|
||||||
ChickinSvc: chickinSvc,
|
ChickinSvc: chickinSvc,
|
||||||
TransferLayingRepo: transferLayingRepo,
|
TransferLayingRepo: transferLayingRepo,
|
||||||
|
TransferLayingSvc: transferLayingSvc,
|
||||||
FifoStockV2Svc: fifoStockV2Svc,
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
StockLogRepo: stockLogRepo,
|
StockLogRepo: stockLogRepo,
|
||||||
}
|
}
|
||||||
@@ -180,6 +184,13 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
totalChick := totalChickMap[recordings[i].ProjectFlockKandangId]
|
totalChick := totalChickMap[recordings[i].ProjectFlockKandangId]
|
||||||
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
||||||
recordings[i].DepletionRate = &rate
|
recordings[i].DepletionRate = &rate
|
||||||
|
|
||||||
|
populationCanChange, transferExecuted, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), &recordings[i])
|
||||||
|
if stateErr != nil {
|
||||||
|
return nil, 0, stateErr
|
||||||
|
}
|
||||||
|
recordings[i].PopulationCanChange = boolPtr(populationCanChange)
|
||||||
|
recordings[i].TransferExecuted = boolPtr(transferExecuted)
|
||||||
}
|
}
|
||||||
return recordings, total, nil
|
return recordings, total, nil
|
||||||
}
|
}
|
||||||
@@ -239,6 +250,14 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
|
|||||||
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
||||||
recording.DepletionRate = &rate
|
recording.DepletionRate = &rate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
populationCanChange, transferExecuted, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
|
||||||
|
if stateErr != nil {
|
||||||
|
return nil, stateErr
|
||||||
|
}
|
||||||
|
recording.PopulationCanChange = boolPtr(populationCanChange)
|
||||||
|
recording.TransferExecuted = boolPtr(transferExecuted)
|
||||||
|
|
||||||
return recording, nil
|
return recording, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +312,12 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
category := strings.ToUpper(pfk.ProjectFlock.Category)
|
category := strings.ToUpper(pfk.ProjectFlock.Category)
|
||||||
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
||||||
|
|
||||||
if err := s.enforceTransferRecordingRoute(ctx, pfk, recordTime); err != nil {
|
if err := s.tryAutoExecuteTransferForRecordingCreate(c, pfk, recordTime); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
routePayload := buildRecordingRoutePayloadFromCreate(req)
|
||||||
|
if err := s.enforceTransferRecordingRoute(ctx, pfk, recordTime, routePayload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,8 +442,8 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
if err := s.reflowApplyRecordingDepletionsIn(ctx, tx, mappedDepletions); err != nil {
|
if err := s.reflowApplyRecordingDepletionsIn(ctx, tx, mappedDepletions); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, createdRecording.ProjectFlockKandangId); err != nil {
|
if err := s.resyncPopulationUsageForDepletions(ctx, tx, createdRecording.ProjectFlockKandangId, mappedDepletions); err != nil {
|
||||||
s.Log.Errorf("Failed to resync project flock population usage: %+v", err)
|
s.Log.Errorf("Failed to resync depletion source population usage: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +518,26 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
recordingEntity = recording
|
recordingEntity = recording
|
||||||
|
if err := s.ensurePopulationMutationAllowed(ctx, recordingEntity, "ubah"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pfkForRoute := recordingEntity.ProjectFlockKandang
|
||||||
|
if pfkForRoute == nil || pfkForRoute.Id == 0 {
|
||||||
|
fetchedPfk, fetchErr := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recordingEntity.ProjectFlockKandangId)
|
||||||
|
if fetchErr != nil {
|
||||||
|
if errors.Is(fetchErr, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to fetch project flock kandang for route validation: %+v", fetchErr)
|
||||||
|
return fetchErr
|
||||||
|
}
|
||||||
|
pfkForRoute = fetchedPfk
|
||||||
|
}
|
||||||
|
routePayload := buildRecordingRoutePayloadFromUpdate(req, recordingEntity)
|
||||||
|
if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
hasStockChanges := req.Stocks != nil
|
hasStockChanges := req.Stocks != nil
|
||||||
hasDepletionChanges := req.Depletions != nil
|
hasDepletionChanges := req.Depletions != nil
|
||||||
hasEggChanges := req.Eggs != nil
|
hasEggChanges := req.Eggs != nil
|
||||||
@@ -501,6 +545,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
var existingStocks []entity.RecordingStock
|
var existingStocks []entity.RecordingStock
|
||||||
var existingDepletions []entity.RecordingDepletion
|
var existingDepletions []entity.RecordingDepletion
|
||||||
var existingEggs []entity.RecordingEgg
|
var existingEggs []entity.RecordingEgg
|
||||||
|
var mappedDepletions []entity.RecordingDepletion
|
||||||
|
|
||||||
note := recordingutil.RecordingNote("Edit", recordingEntity.Id)
|
note := recordingutil.RecordingNote("Edit", recordingEntity.Id)
|
||||||
|
|
||||||
@@ -545,6 +590,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
if match {
|
if match {
|
||||||
hasDepletionChanges = false
|
hasDepletionChanges = false
|
||||||
} else {
|
} else {
|
||||||
|
if err := s.ensureDepletionMutationAllowed(ctx, recordingEntity, "ubah"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, nil); err != nil {
|
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -564,7 +612,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
|
mappedDepletions = recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
|
||||||
if len(mappedDepletions) > 0 {
|
if len(mappedDepletions) > 0 {
|
||||||
if err := s.ensureDepletionWithinPopulation(ctx, tx, recordingEntity.ProjectFlockKandangId, sumDepletionQty(mappedDepletions), sumDepletionQty(existingDepletions)); err != nil {
|
if err := s.ensureDepletionWithinPopulation(ctx, tx, recordingEntity.ProjectFlockKandangId, sumDepletionQty(mappedDepletions), sumDepletionQty(existingDepletions)); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -655,8 +703,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, recordingEntity.ProjectFlockKandangId); err != nil {
|
if err := s.resyncPopulationUsageForDepletions(ctx, tx, recordingEntity.ProjectFlockKandangId, append(existingDepletions, mappedDepletions...)); err != nil {
|
||||||
s.Log.Errorf("Failed to resync project flock population usage: %+v", err)
|
s.Log.Errorf("Failed to resync depletion source population usage: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,6 +856,11 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
|
|||||||
|
|
||||||
if action == entity.ApprovalActionRejected {
|
if action == entity.ApprovalActionRejected {
|
||||||
note := recordingutil.RecordingNote("Reject", id)
|
note := recordingutil.RecordingNote("Reject", id)
|
||||||
|
existingDepletions, err := s.Repository.ListDepletions(tx, id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list depletions before reject rollback %d: %+v", id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.reflowRollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
|
if err := s.reflowRollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -821,8 +874,8 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
|
|||||||
s.Log.Errorf("Failed to recompute recording metrics after reject %d: %+v", id, err)
|
s.Log.Errorf("Failed to recompute recording metrics after reject %d: %+v", id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, recording.ProjectFlockKandangId); err != nil {
|
if err := s.resyncPopulationUsageForDepletions(ctx, tx, recording.ProjectFlockKandangId, existingDepletions); err != nil {
|
||||||
s.Log.Errorf("Failed to resync project flock population usage after reject %d: %+v", id, err)
|
s.Log.Errorf("Failed to resync depletion source population usage after reject %d: %+v", id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
||||||
@@ -878,6 +931,19 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
s.Log.Errorf("Failed to find recording: %+v", err)
|
s.Log.Errorf("Failed to find recording: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := s.ensurePopulationMutationAllowed(ctx, recording, "hapus"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existingDepletions, err := s.Repository.ListDepletions(tx, recording.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(existingDepletions) > 0 {
|
||||||
|
if err := s.ensureDepletionMutationAllowed(ctx, recording, "hapus"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.reflowRollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
|
if err := s.reflowRollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -891,8 +957,8 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, recording.ProjectFlockKandangId); err != nil {
|
if err := s.resyncPopulationUsageForDepletions(ctx, tx, recording.ProjectFlockKandangId, existingDepletions); err != nil {
|
||||||
s.Log.Errorf("Failed to resync project flock population usage: %+v", err)
|
s.Log.Errorf("Failed to resync depletion source population usage after delete: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,10 +971,201 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) resolveRecordingCategory(ctx context.Context, recording *entity.Recording) (string, error) {
|
||||||
|
if recording == nil || recording.ProjectFlockKandangId == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if recording.ProjectFlockKandang != nil && recording.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
|
return strings.ToUpper(strings.TrimSpace(recording.ProjectFlockKandang.ProjectFlock.Category)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recording.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) evaluatePopulationMutationState(ctx context.Context, recording *entity.Recording) (bool, bool, *entity.LayingTransfer, time.Time, error) {
|
||||||
|
if recording == nil || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
|
||||||
|
return true, false, nil, time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
category, err := s.resolveRecordingCategory(ctx, recording)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to resolve recording category for population mutation check (recording=%d): %+v", recording.Id, err)
|
||||||
|
return true, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||||
|
}
|
||||||
|
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||||
|
return true, false, nil, time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return true, false, nil, time.Time{}, nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve approved transfer by source kandang for recording %d: %+v", recording.Id, err)
|
||||||
|
return true, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||||
|
}
|
||||||
|
if transfer == nil {
|
||||||
|
return true, false, nil, time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transferDate := transferPhysicalMoveDate(transfer)
|
||||||
|
if transferDate.IsZero() {
|
||||||
|
return true, false, transfer, transferDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transferExecuted := transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||||
|
recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
|
||||||
|
populationCanChange := !(transferExecuted && !recordDate.Before(transferDate))
|
||||||
|
|
||||||
|
return populationCanChange, transferExecuted, transfer, transferDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) ensurePopulationMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
|
||||||
|
populationCanChange, _, transfer, transferDate, err := s.evaluatePopulationMutationState(ctx, recording)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if populationCanChange {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transferNumber := "-"
|
||||||
|
if transfer != nil && strings.TrimSpace(transfer.TransferNumber) != "" {
|
||||||
|
transferNumber = transfer.TransferNumber
|
||||||
|
}
|
||||||
|
recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
|
||||||
|
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Recording growing tanggal %s tidak dapat di%s karena transfer laying %s sudah dieksekusi sejak %s. Perubahan populasi tidak diizinkan.",
|
||||||
|
recordDate.Format("2006-01-02"),
|
||||||
|
operation,
|
||||||
|
transferNumber,
|
||||||
|
transferDate.Format("2006-01-02"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) ensureDepletionMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
|
||||||
|
if recording == nil || recording.Id == 0 || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
category := ""
|
||||||
|
if recording.ProjectFlockKandang != nil && recording.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
|
category = strings.ToUpper(strings.TrimSpace(recording.ProjectFlockKandang.ProjectFlock.Category))
|
||||||
|
} else {
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recording.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to load project flock kandang %d for depletion guard: %+v", recording.ProjectFlockKandangId, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan deplesi")
|
||||||
|
}
|
||||||
|
category = strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
transfer *entity.LayingTransfer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
switch category {
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, recording.ProjectFlockKandangId)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve transfer laying for depletion guard recording %d: %+v", recording.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan deplesi")
|
||||||
|
}
|
||||||
|
if transfer == nil || transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
|
||||||
|
physicalMoveDate := transferPhysicalMoveDate(transfer)
|
||||||
|
if physicalMoveDate.IsZero() || recordDate.Before(physicalMoveDate) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Deplesi recording tanggal %s tidak dapat di%s karena sudah mempengaruhi transfer laying %s yang sudah dieksekusi. Lakukan unexecute transfer terlebih dahulu bila belum ada pemakaian downstream.",
|
||||||
|
recordDate.Format("2006-01-02"),
|
||||||
|
operation,
|
||||||
|
transfer.TransferNumber,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) tryAutoExecuteTransferForRecordingCreate(c *fiber.Ctx, pfk *entity.ProjectFlockKandang, recordTime time.Time) error {
|
||||||
|
if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil || s.TransferLayingSvc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Context()
|
||||||
|
recordDate := normalizeDateOnlyUTC(recordTime)
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||||
|
|
||||||
|
var (
|
||||||
|
transfer *entity.LayingTransfer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
switch category {
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, pfk.Id)
|
||||||
|
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
||||||
|
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, pfk.Id)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to resolve approved transfer for recording create (pfk=%d): %+v", pfk.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
|
}
|
||||||
|
if transfer == nil || (transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalMoveDate := transferPhysicalMoveDate(transfer)
|
||||||
|
if physicalMoveDate.IsZero() || recordDate.Before(physicalMoveDate) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.TransferLayingSvc.ExecuteWithBusinessDate(c, transfer.Id, recordDate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *recordingService) enforceTransferRecordingRoute(
|
func (s *recordingService) enforceTransferRecordingRoute(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pfk *entity.ProjectFlockKandang,
|
pfk *entity.ProjectFlockKandang,
|
||||||
recordTime time.Time,
|
recordTime time.Time,
|
||||||
|
payload recordingRoutePayload,
|
||||||
) error {
|
) error {
|
||||||
if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil {
|
if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -928,22 +1185,35 @@ func (s *recordingService) enforceTransferRecordingRoute(
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
}
|
}
|
||||||
|
|
||||||
effectiveDate := effectiveTransferDate(transfer)
|
physicalMoveDate, economicCutoffDate := transferRecordingWindow(transfer)
|
||||||
if effectiveDate.IsZero() {
|
if physicalMoveDate.IsZero() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if recordDate.Before(effectiveDate) {
|
if recordDate.Before(physicalMoveDate) {
|
||||||
return fiber.NewError(
|
return fiber.NewError(
|
||||||
fiber.StatusBadRequest,
|
fiber.StatusBadRequest,
|
||||||
fmt.Sprintf("Recording kandang laying hanya bisa dimulai pada %s. Sebelumnya gunakan kandang growing", effectiveDate.Format("2006-01-02")),
|
fmt.Sprintf("Recording kandang laying hanya bisa dimulai pada %s (tanggal pindah fisik). Sebelumnya gunakan kandang growing", physicalMoveDate.Format("2006-01-02")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
||||||
return fiber.NewError(
|
return fiber.NewError(
|
||||||
fiber.StatusBadRequest,
|
fiber.StatusBadRequest,
|
||||||
fmt.Sprintf("Transfer laying %s sudah efektif pada %s tetapi belum dieksekusi. Eksekusi transfer terlebih dahulu", transfer.TransferNumber, effectiveDate.Format("2006-01-02")),
|
fmt.Sprintf("Transfer laying %s dengan tanggal pindah fisik %s belum dieksekusi. Eksekusi transfer terlebih dahulu", transfer.TransferNumber, physicalMoveDate.Format("2006-01-02")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordDate.Before(economicCutoffDate) && payload.StockCount > 0 {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Periode transisi transfer laying %s (%s s.d. %s): input PAKAN/OVK harus dicatat di kandang growing hingga %s. Recording kandang laying pada periode ini hanya untuk deplesi (dan telur bila ada).",
|
||||||
|
transfer.TransferNumber,
|
||||||
|
physicalMoveDate.Format("2006-01-02"),
|
||||||
|
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
||||||
|
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,22 +1227,38 @@ func (s *recordingService) enforceTransferRecordingRoute(
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||||
}
|
}
|
||||||
|
|
||||||
if transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero() {
|
physicalMoveDate, economicCutoffDate := transferRecordingWindow(transfer)
|
||||||
return fiber.NewError(
|
if physicalMoveDate.IsZero() {
|
||||||
fiber.StatusBadRequest,
|
|
||||||
"Project flock kandang sudah dipindahkan ke laying",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveDate := effectiveTransferDate(transfer)
|
|
||||||
if effectiveDate.IsZero() {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !recordDate.Before(effectiveDate) {
|
if recordDate.Before(physicalMoveDate) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if transfer.ExecutedAt == nil || transfer.ExecutedAt.IsZero() {
|
||||||
return fiber.NewError(
|
return fiber.NewError(
|
||||||
fiber.StatusBadRequest,
|
fiber.StatusBadRequest,
|
||||||
fmt.Sprintf("Recording kandang growing hanya diperbolehkan sampai %s. Gunakan kandang laying mulai %s", effectiveDate.AddDate(0, 0, -1).Format("2006-01-02"), effectiveDate.Format("2006-01-02")),
|
fmt.Sprintf("Transfer laying %s sudah memasuki tanggal pindah fisik %s namun belum dieksekusi. Eksekusi transfer lalu lakukan recording transisi sesuai aturan", transfer.TransferNumber, physicalMoveDate.Format("2006-01-02")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !recordDate.Before(economicCutoffDate) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Recording kandang growing hanya diperbolehkan sampai %s. Gunakan kandang laying mulai %s", economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"), economicCutoffDate.Format("2006-01-02")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.DepletionCount > 0 {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Periode transisi transfer laying %s (%s s.d. %s): deplesi harus dicatat di kandang laying tujuan agar mapping tidak ambigu. Kandang growing pada periode ini hanya untuk PAKAN/OVK.",
|
||||||
|
transfer.TransferNumber,
|
||||||
|
physicalMoveDate.Format("2006-01-02"),
|
||||||
|
economicCutoffDate.AddDate(0, 0, -1).Format("2006-01-02"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -980,23 +1266,138 @@ func (s *recordingService) enforceTransferRecordingRoute(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func effectiveTransferDate(transfer *entity.LayingTransfer) time.Time {
|
type recordingRoutePayload struct {
|
||||||
|
StockCount int
|
||||||
|
DepletionCount int
|
||||||
|
EggCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRecordingRoutePayloadFromCreate(req *validation.Create) recordingRoutePayload {
|
||||||
|
payload := recordingRoutePayload{}
|
||||||
|
if req == nil {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
for _, stock := range req.Stocks {
|
||||||
|
if stock.Qty > 0 {
|
||||||
|
payload.StockCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, depletion := range req.Depletions {
|
||||||
|
if depletion.Qty > 0 {
|
||||||
|
payload.DepletionCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, egg := range req.Eggs {
|
||||||
|
if egg.Qty > 0 {
|
||||||
|
payload.EggCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRecordingRoutePayloadFromUpdate(req *validation.Update, existing *entity.Recording) recordingRoutePayload {
|
||||||
|
payload := recordingRoutePayload{}
|
||||||
|
if req == nil && existing == nil {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
if req != nil && req.Stocks != nil {
|
||||||
|
for _, stock := range req.Stocks {
|
||||||
|
if stock.Qty > 0 {
|
||||||
|
payload.StockCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if existing != nil {
|
||||||
|
for _, stock := range existing.Stocks {
|
||||||
|
usageQty := 0.0
|
||||||
|
if stock.UsageQty != nil {
|
||||||
|
usageQty = *stock.UsageQty
|
||||||
|
}
|
||||||
|
pendingQty := 0.0
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pendingQty = *stock.PendingQty
|
||||||
|
}
|
||||||
|
if usageQty > 0 || pendingQty > 0 {
|
||||||
|
payload.StockCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req != nil && req.Depletions != nil {
|
||||||
|
for _, depletion := range req.Depletions {
|
||||||
|
if depletion.Qty > 0 {
|
||||||
|
payload.DepletionCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if existing != nil {
|
||||||
|
for _, depletion := range existing.Depletions {
|
||||||
|
if depletion.Qty > 0 {
|
||||||
|
payload.DepletionCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req != nil && req.Eggs != nil {
|
||||||
|
for _, egg := range req.Eggs {
|
||||||
|
if egg.Qty > 0 {
|
||||||
|
payload.EggCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if existing != nil {
|
||||||
|
for _, egg := range existing.Eggs {
|
||||||
|
if egg.Qty > 0 {
|
||||||
|
payload.EggCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferPhysicalMoveDate(transfer *entity.LayingTransfer) time.Time {
|
||||||
if transfer == nil {
|
if transfer == nil {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
|
|
||||||
return normalizeDateOnlyUTC(*transfer.EffectiveMoveDate)
|
|
||||||
}
|
|
||||||
if !transfer.TransferDate.IsZero() {
|
if !transfer.TransferDate.IsZero() {
|
||||||
return normalizeDateOnlyUTC(transfer.TransferDate)
|
return normalizeDateOnlyUTC(transfer.TransferDate)
|
||||||
}
|
}
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func transferEconomicCutoffDate(transfer *entity.LayingTransfer) time.Time {
|
||||||
|
if transfer == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() {
|
||||||
|
return normalizeDateOnlyUTC(*transfer.EconomicCutoffDate)
|
||||||
|
}
|
||||||
|
if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
|
||||||
|
return normalizeDateOnlyUTC(*transfer.EffectiveMoveDate)
|
||||||
|
}
|
||||||
|
return transferPhysicalMoveDate(transfer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferRecordingWindow(transfer *entity.LayingTransfer) (time.Time, time.Time) {
|
||||||
|
physicalMoveDate := transferPhysicalMoveDate(transfer)
|
||||||
|
economicCutoffDate := transferEconomicCutoffDate(transfer)
|
||||||
|
if economicCutoffDate.IsZero() {
|
||||||
|
economicCutoffDate = physicalMoveDate
|
||||||
|
}
|
||||||
|
if !physicalMoveDate.IsZero() && economicCutoffDate.Before(physicalMoveDate) {
|
||||||
|
economicCutoffDate = physicalMoveDate
|
||||||
|
}
|
||||||
|
return physicalMoveDate, economicCutoffDate
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||||
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func boolPtr(value bool) *bool {
|
||||||
|
v := value
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
|
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
|
||||||
idSet := make(map[uint]struct{})
|
idSet := make(map[uint]struct{})
|
||||||
|
|
||||||
@@ -2184,6 +2585,119 @@ func sumDepletionQty(items []entity.RecordingDepletion) float64 {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) resyncPopulationUsageForDepletions(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
recordingProjectFlockKandangID uint,
|
||||||
|
depletions []entity.RecordingDepletion,
|
||||||
|
) error {
|
||||||
|
kandangIDs := map[uint]struct{}{}
|
||||||
|
if recordingProjectFlockKandangID != 0 {
|
||||||
|
kandangIDs[recordingProjectFlockKandangID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWarehouseIDs := make([]uint, 0)
|
||||||
|
sourceWarehouseSeen := map[uint]struct{}{}
|
||||||
|
for _, dep := range depletions {
|
||||||
|
if dep.SourceProductWarehouseId == nil || *dep.SourceProductWarehouseId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pwID := *dep.SourceProductWarehouseId
|
||||||
|
if _, exists := sourceWarehouseSeen[pwID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sourceWarehouseSeen[pwID] = struct{}{}
|
||||||
|
sourceWarehouseIDs = append(sourceWarehouseIDs, pwID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sourceWarehouseIDs) > 0 {
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
if tx != nil {
|
||||||
|
db = tx.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceKandangIDs []uint
|
||||||
|
if err := db.Table("project_flock_populations pfp").
|
||||||
|
Select("DISTINCT pc.project_flock_kandang_id").
|
||||||
|
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||||
|
Where("pfp.product_warehouse_id IN ?", sourceWarehouseIDs).
|
||||||
|
Where("pfp.deleted_at IS NULL").
|
||||||
|
Where("pc.deleted_at IS NULL").
|
||||||
|
Pluck("pc.project_flock_kandang_id", &sourceKandangIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kandangID := range sourceKandangIDs {
|
||||||
|
if kandangID != 0 {
|
||||||
|
kandangIDs[kandangID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for kandangID := range kandangIDs {
|
||||||
|
if err := s.resyncPopulationUsageByProjectFlockKandang(ctx, tx, kandangID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
if tx != nil {
|
||||||
|
db = tx.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var populationIDs []uint
|
||||||
|
if err := db.Table("project_flock_populations pfp").
|
||||||
|
Select("pfp.id").
|
||||||
|
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||||
|
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Pluck("pfp.id", &populationIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(populationIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type usageRow struct {
|
||||||
|
StockableID uint `gorm:"column:stockable_id"`
|
||||||
|
Used float64 `gorm:"column:used"`
|
||||||
|
}
|
||||||
|
var usageRows []usageRow
|
||||||
|
if err := db.Table("stock_allocations").
|
||||||
|
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
||||||
|
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||||
|
Where("status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
|
Where("stockable_id IN ?", populationIDs).
|
||||||
|
Group("stockable_id").
|
||||||
|
Scan(&usageRows).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||||
|
Where("id IN ?", populationIDs).
|
||||||
|
Update("total_used_qty", 0).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range usageRows {
|
||||||
|
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
||||||
|
Where("id = ?", row.StockableID).
|
||||||
|
Update("total_used_qty", row.Used).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *recordingService) ensureDepletionWithinPopulation(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, newTotal float64, existingTotal float64) error {
|
func (s *recordingService) ensureDepletionWithinPopulation(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, newTotal float64, existingTotal float64) error {
|
||||||
if projectFlockKandangId == 0 || newTotal <= 0 {
|
if projectFlockKandangId == 0 || newTotal <= 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustDate(t *testing.T, value string) time.Time {
|
||||||
|
t.Helper()
|
||||||
|
parsed, err := time.Parse("2006-01-02", value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed parsing date %s: %v", value, err)
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferRecordingWindow(t *testing.T) {
|
||||||
|
t.Run("early transfer keeps transition until economic cutoff", func(t *testing.T) {
|
||||||
|
physical := mustDate(t, "2026-04-08")
|
||||||
|
cutoff := mustDate(t, "2026-05-13")
|
||||||
|
transfer := &entity.LayingTransfer{
|
||||||
|
TransferDate: physical,
|
||||||
|
EconomicCutoffDate: &cutoff,
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPhysical, gotCutoff := transferRecordingWindow(transfer)
|
||||||
|
if gotPhysical.Format("2006-01-02") != "2026-04-08" {
|
||||||
|
t.Fatalf("unexpected physical date: %s", gotPhysical.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
if gotCutoff.Format("2006-01-02") != "2026-05-13" {
|
||||||
|
t.Fatalf("unexpected cutoff date: %s", gotCutoff.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("standard transfer has no transition window", func(t *testing.T) {
|
||||||
|
physical := mustDate(t, "2026-05-13")
|
||||||
|
cutoff := mustDate(t, "2026-05-13")
|
||||||
|
transfer := &entity.LayingTransfer{
|
||||||
|
TransferDate: physical,
|
||||||
|
EconomicCutoffDate: &cutoff,
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPhysical, gotCutoff := transferRecordingWindow(transfer)
|
||||||
|
if gotPhysical.Format("2006-01-02") != "2026-05-13" {
|
||||||
|
t.Fatalf("unexpected physical date: %s", gotPhysical.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
if gotCutoff.Format("2006-01-02") != "2026-05-13" {
|
||||||
|
t.Fatalf("unexpected cutoff date: %s", gotCutoff.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("late transfer clamps economic cutoff to physical move", func(t *testing.T) {
|
||||||
|
physical := mustDate(t, "2026-06-03")
|
||||||
|
cutoff := mustDate(t, "2026-05-13")
|
||||||
|
transfer := &entity.LayingTransfer{
|
||||||
|
TransferDate: physical,
|
||||||
|
EconomicCutoffDate: &cutoff,
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPhysical, gotCutoff := transferRecordingWindow(transfer)
|
||||||
|
if gotPhysical.Format("2006-01-02") != "2026-06-03" {
|
||||||
|
t.Fatalf("unexpected physical date: %s", gotPhysical.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
if gotCutoff.Format("2006-01-02") != "2026-06-03" {
|
||||||
|
t.Fatalf("unexpected cutoff date: %s", gotCutoff.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("legacy data falls back to effective move date", func(t *testing.T) {
|
||||||
|
physical := mustDate(t, "2026-04-08")
|
||||||
|
legacyEffective := mustDate(t, "2026-05-13")
|
||||||
|
transfer := &entity.LayingTransfer{
|
||||||
|
TransferDate: physical,
|
||||||
|
EffectiveMoveDate: &legacyEffective,
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPhysical, gotCutoff := transferRecordingWindow(transfer)
|
||||||
|
if gotPhysical.Format("2006-01-02") != "2026-04-08" {
|
||||||
|
t.Fatalf("unexpected physical date: %s", gotPhysical.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
if gotCutoff.Format("2006-01-02") != "2026-05-13" {
|
||||||
|
t.Fatalf("unexpected cutoff date: %s", gotCutoff.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+22
@@ -208,6 +208,28 @@ func (u *TransferLayingController) Execute(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) Unexecute(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.TransferLayingService.Unexecute(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Unexecute transfer laying successfully",
|
||||||
|
Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, result.LatestApproval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
||||||
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type TransferLayingRelationDTO struct {
|
type TransferLayingRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
TransferNumber string `json:"transfer_number"`
|
TransferNumber string `json:"transfer_number"`
|
||||||
TransferDate time.Time `json:"transfer_date"`
|
TransferDate time.Time `json:"transfer_date"`
|
||||||
EffectiveMoveDate *time.Time `json:"effective_move_date,omitempty"`
|
EconomicCutoffDate *time.Time `json:"economic_cutoff_date,omitempty"`
|
||||||
ExecutedAt *time.Time `json:"executed_at,omitempty"`
|
EffectiveMoveDate *time.Time `json:"effective_move_date,omitempty"`
|
||||||
Notes string `json:"notes"`
|
ExecutedAt *time.Time `json:"executed_at,omitempty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockKandangWithKandangDTO struct {
|
type ProjectFlockKandangWithKandangDTO struct {
|
||||||
@@ -92,12 +93,13 @@ type MaxTargetQtyForTransferDTO struct {
|
|||||||
|
|
||||||
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO {
|
func ToTransferLayingRelationDTO(e entity.LayingTransfer) TransferLayingRelationDTO {
|
||||||
return TransferLayingRelationDTO{
|
return TransferLayingRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
TransferNumber: e.TransferNumber,
|
TransferNumber: e.TransferNumber,
|
||||||
TransferDate: e.TransferDate,
|
TransferDate: e.TransferDate,
|
||||||
EffectiveMoveDate: e.EffectiveMoveDate,
|
EconomicCutoffDate: e.EconomicCutoffDate,
|
||||||
ExecutedAt: e.ExecutedAt,
|
EffectiveMoveDate: e.EffectiveMoveDate,
|
||||||
Notes: e.Notes,
|
ExecutedAt: e.ExecutedAt,
|
||||||
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +152,46 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toLayingTransferSourceDTOsFromTransfer(e entity.LayingTransfer) []LayingTransferSourceDTO {
|
||||||
|
if len(e.Sources) > 0 {
|
||||||
|
return ToLayingTransferSourceDTOs(e.Sources)
|
||||||
|
}
|
||||||
|
if e.SourceProjectFlockKandangId == nil || *e.SourceProjectFlockKandangId == 0 {
|
||||||
|
return []LayingTransferSourceDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayQty := e.SourceRequestedQty
|
||||||
|
if e.SourceUsageQty > 0 {
|
||||||
|
displayQty = e.SourceUsageQty
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkDTO := &ProjectFlockKandangWithKandangDTO{
|
||||||
|
Id: *e.SourceProjectFlockKandangId,
|
||||||
|
}
|
||||||
|
if e.SourceProjectFlockKandang != nil && e.SourceProjectFlockKandang.Id != 0 {
|
||||||
|
pfkDTO.KandangId = e.SourceProjectFlockKandang.KandangId
|
||||||
|
pfkDTO.ProjectFlockId = e.SourceProjectFlockKandang.ProjectFlockId
|
||||||
|
if e.SourceProjectFlockKandang.Kandang.Id != 0 {
|
||||||
|
kandangMapped := kandangDTO.ToKandangRelationDTO(e.SourceProjectFlockKandang.Kandang)
|
||||||
|
pfkDTO.Kandang = &kandangMapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pwDTO *productWarehouseDTO.ProductWarehouseRelationDTO
|
||||||
|
if e.SourceProductWarehouse != nil && e.SourceProductWarehouse.Id != 0 {
|
||||||
|
mapped := productWarehouseDTO.ToProductWarehouseRelationDTO(*e.SourceProductWarehouse)
|
||||||
|
pwDTO = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return []LayingTransferSourceDTO{
|
||||||
|
{
|
||||||
|
SourceProjectFlockKandang: pfkDTO,
|
||||||
|
Qty: displayQty,
|
||||||
|
ProductWarehouse: pwDTO,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
|
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
|
||||||
var pfkDTO *ProjectFlockKandangWithKandangDTO
|
var pfkDTO *ProjectFlockKandangWithKandangDTO
|
||||||
if target.TargetProjectFlockKandang != nil && target.TargetProjectFlockKandang.Id != 0 {
|
if target.TargetProjectFlockKandang != nil && target.TargetProjectFlockKandang.Id != 0 {
|
||||||
@@ -254,7 +296,7 @@ func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Appro
|
|||||||
|
|
||||||
return TransferLayingDetailDTO{
|
return TransferLayingDetailDTO{
|
||||||
TransferLayingListDTO: ToTransferLayingListDTO(e),
|
TransferLayingListDTO: ToTransferLayingListDTO(e),
|
||||||
Sources: ToLayingTransferSourceDTOs(e.Sources),
|
Sources: toLayingTransferSourceDTOsFromTransfer(e),
|
||||||
Targets: ToLayingTransferTargetDTOs(e.Targets),
|
Targets: ToLayingTransferTargetDTOs(e.Targets),
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
}
|
}
|
||||||
@@ -276,7 +318,7 @@ func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approv
|
|||||||
|
|
||||||
return TransferLayingDetailDTO{
|
return TransferLayingDetailDTO{
|
||||||
TransferLayingListDTO: ToTransferLayingListDTO(e),
|
TransferLayingListDTO: ToTransferLayingListDTO(e),
|
||||||
Sources: ToLayingTransferSourceDTOs(e.Sources),
|
Sources: toLayingTransferSourceDTOsFromTransfer(e),
|
||||||
Targets: ToLayingTransferTargetDTOs(e.Targets),
|
Targets: ToLayingTransferTargetDTOs(e.Targets),
|
||||||
Approval: mappedApproval,
|
Approval: mappedApproval,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
@@ -60,12 +60,12 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
|||||||
// daftarin jadi usable
|
// daftarin jadi usable
|
||||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
Key: fifo.UsableKeyTransferToLayingOut,
|
Key: fifo.UsableKeyTransferToLayingOut,
|
||||||
Table: "laying_transfer_sources",
|
Table: "laying_transfers",
|
||||||
Columns: fifo.UsableColumns{
|
Columns: fifo.UsableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
ProductWarehouseID: "product_warehouse_id",
|
ProductWarehouseID: "source_product_warehouse_id",
|
||||||
UsageQuantity: "usage_qty",
|
UsageQuantity: "source_usage_qty",
|
||||||
PendingQuantity: "pending_usage_qty",
|
PendingQuantity: "source_pending_usage_qty",
|
||||||
CreatedAt: "created_at",
|
CreatedAt: "created_at",
|
||||||
},
|
},
|
||||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||||
|
|||||||
+7
-3
@@ -166,6 +166,9 @@ func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, of
|
|||||||
q = q.Offset(offset).Limit(limit).
|
q = q.Offset(offset).Limit(limit).
|
||||||
Preload("FromProjectFlock").
|
Preload("FromProjectFlock").
|
||||||
Preload("ToProjectFlock").
|
Preload("ToProjectFlock").
|
||||||
|
Preload("SourceProjectFlockKandang").
|
||||||
|
Preload("SourceProjectFlockKandang.Kandang").
|
||||||
|
Preload("SourceProductWarehouse").
|
||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
Preload("ExecutedUser").
|
Preload("ExecutedUser").
|
||||||
Preload("Sources").
|
Preload("Sources").
|
||||||
@@ -193,11 +196,12 @@ func (r *TransferLayingRepositoryImpl) GetLatestApprovedBySourceKandang(ctx cont
|
|||||||
var transfer entity.LayingTransfer
|
var transfer entity.LayingTransfer
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Model(&entity.LayingTransfer{}).
|
Model(&entity.LayingTransfer{}).
|
||||||
Joins("JOIN laying_transfer_sources lts ON lts.laying_transfer_id = laying_transfers.id AND lts.deleted_at IS NULL").
|
Distinct("laying_transfers.*").
|
||||||
Where("lts.source_project_flock_kandang_id = ?", sourceProjectFlockKandangID).
|
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = laying_transfers.id AND lts.deleted_at IS NULL").
|
||||||
|
Where("(laying_transfers.source_project_flock_kandang_id = ? OR lts.source_project_flock_kandang_id = ?)", sourceProjectFlockKandangID, sourceProjectFlockKandangID).
|
||||||
Where("laying_transfers.deleted_at IS NULL").
|
Where("laying_transfers.deleted_at IS NULL").
|
||||||
Where(`(
|
Where(`(
|
||||||
SELECT a.action
|
SELECT a.action
|
||||||
FROM approvals a
|
FROM approvals a
|
||||||
WHERE a.approvable_type = ?
|
WHERE a.approvable_type = ?
|
||||||
AND a.approvable_id = laying_transfers.id
|
AND a.approvable_id = laying_transfers.id
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
|
|||||||
route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
|
route.Delete("/:id", m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne)
|
||||||
route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
|
route.Post("/approvals", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval)
|
||||||
route.Post("/:id/execute", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Execute)
|
route.Post("/:id/execute", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Execute)
|
||||||
|
route.Post("/:id/unexecute", m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Unexecute)
|
||||||
route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
|
route.Get("/project-flocks/:project_flock_id/available-qty", m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang)
|
||||||
route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang)
|
route.Get("/project-flocks/:project_flock_id/max-target-qty", m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.GetMaxTargetQtyPerKandang)
|
||||||
}
|
}
|
||||||
|
|||||||
+693
-401
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -14,7 +14,7 @@ type Create struct {
|
|||||||
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
|
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
|
||||||
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
|
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
|
||||||
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
|
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
|
||||||
SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"`
|
SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,max=1,dive,required"`
|
||||||
TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"`
|
TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"`
|
||||||
Reason string `json:"reason" validate:"omitempty,max=1000"`
|
Reason string `json:"reason" validate:"omitempty,max=1000"`
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ type Update struct {
|
|||||||
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
|
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
|
||||||
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
|
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
|
||||||
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
|
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
|
||||||
SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"`
|
SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,max=1,dive,required"`
|
||||||
TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"`
|
TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"`
|
||||||
Reason string `json:"reason" validate:"omitempty,max=1000"`
|
Reason string `json:"reason" validate:"omitempty,max=1000"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user