Merge branch 'development' into 'staging'

Development

See merge request mbugroup/lti-api!310
This commit is contained in:
Adnan Zahir
2026-02-05 10:32:25 +07:00
45 changed files with 1019 additions and 438 deletions
+73 -36
View File
@@ -743,8 +743,6 @@ func (s *fifoService) releaseUsagePortion(
if expectedWarehouseID == 0 || alloc.ProductWarehouseId == expectedWarehouseID { if expectedWarehouseID == 0 || alloc.ProductWarehouseId == expectedWarehouseID {
continue continue
} }
fmt.Printf("WARN[FIFO] ALLOC WAREHOUSE MISMATCH usable_key=%s usable_id=%d alloc_id=%d expected_pw=%d actual_pw=%d\n",
usableKey.String(), usableID, alloc.Id, expectedWarehouseID, alloc.ProductWarehouseId)
if err := tx.Model(&entities.StockAllocation{}). if err := tx.Model(&entities.StockAllocation{}).
Where("id = ?", alloc.Id). Where("id = ?", alloc.Id).
Update("product_warehouse_id", expectedWarehouseID).Error; err != nil { Update("product_warehouse_id", expectedWarehouseID).Error; err != nil {
@@ -848,41 +846,80 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p
cfg.Columns.CreatedAt, cfg.Columns.CreatedAt,
) )
var rows []struct { if cfg.Columns.CreatedAt == cfg.Columns.ID {
ID uint var rows []struct {
Pending float64 ID uint
CreatedAt time.Time Pending float64
} CreatedAt int64
}
query := tx.Table(cfg.Table).
Select(selectStmt). query := tx.Table(cfg.Table).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID). Select(selectStmt).
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)). Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
Limit(s.pendingBatchPerUsable) Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
Limit(s.pendingBatchPerUsable)
if cfg.Scope != nil {
query = cfg.Scope(query) if cfg.Scope != nil {
} query = cfg.Scope(query)
}
for _, order := range s.orderClauses(cfg.OrderBy) {
query = query.Order(order) for _, order := range s.orderClauses(cfg.OrderBy) {
} query = query.Order(order)
}
if err := query.Find(&rows).Error; err != nil {
return nil, err if err := query.Find(&rows).Error; err != nil {
} return nil, err
}
for _, row := range rows {
if row.Pending <= 0 { for _, row := range rows {
continue if row.Pending <= 0 {
continue
}
candidates = append(candidates, pendingCandidate{
UsableKey: key,
Config: cfg,
UsableID: row.ID,
Pending: row.Pending,
CreatedAt: time.Unix(0, row.CreatedAt),
})
}
} else {
var rows []struct {
ID uint
Pending float64
CreatedAt time.Time
}
query := tx.Table(cfg.Table).
Select(selectStmt).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
Limit(s.pendingBatchPerUsable)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
for _, order := range s.orderClauses(cfg.OrderBy) {
query = query.Order(order)
}
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if row.Pending <= 0 {
continue
}
candidates = append(candidates, pendingCandidate{
UsableKey: key,
Config: cfg,
UsableID: row.ID,
Pending: row.Pending,
CreatedAt: row.CreatedAt,
})
} }
candidates = append(candidates, pendingCandidate{
UsableKey: key,
Config: cfg,
UsableID: row.ID,
Pending: row.Pending,
CreatedAt: row.CreatedAt,
})
} }
} }
@@ -1,5 +1,5 @@
-- Create sequence for transfer laying movement number -- Create sequence for transfer laying movement number
CREATE SEQUENCE transfer_laying_seq START CREATE SEQUENCE IF NOT EXISTS transfer_laying_seq START
WITH WITH
1 INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999 NO CYCLE; 1 INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999 NO CYCLE;
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE adjustment_stocks
DROP COLUMN adj_number;
COMMIT;
@@ -0,0 +1,10 @@
BEGIN;
ALTER TABLE adjustment_stocks
ADD COLUMN adj_number VARCHAR(255);
UPDATE adjustment_stocks
SET adj_number = CONCAT('ADJ-', LPAD(id::text, 5, '0'))
WHERE adj_number IS NULL;
COMMIT;
@@ -0,0 +1,8 @@
-- Remove columns from marketing_products
ALTER TABLE marketing_products
DROP COLUMN IF EXISTS week,
DROP COLUMN IF EXISTS weight_per_convertion,
DROP COLUMN IF EXISTS convertion_unit;
-- Remove column from marketings
ALTER TABLE marketings DROP COLUMN IF EXISTS marketing_type;
@@ -0,0 +1,9 @@
-- Add marketing_type to marketings table
ALTER TABLE marketings
ADD COLUMN IF NOT EXISTS marketing_type VARCHAR(50);
-- Add convertion fields to marketing_products table
ALTER TABLE marketing_products
ADD COLUMN IF NOT EXISTS convertion_unit VARCHAR(20),
ADD COLUMN IF NOT EXISTS weight_per_convertion NUMERIC(15, 3),
ADD COLUMN IF NOT EXISTS week INTEGER;
@@ -0,0 +1,47 @@
BEGIN;
DO $$
DECLARE
fallback_fcr_id BIGINT;
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flocks'
AND column_name = 'fcr_id'
) THEN
ALTER TABLE project_flocks
ADD COLUMN fcr_id BIGINT;
END IF;
SELECT id INTO fallback_fcr_id
FROM fcrs
ORDER BY id ASC
LIMIT 1;
IF fallback_fcr_id IS NOT NULL THEN
UPDATE project_flocks
SET fcr_id = fallback_fcr_id
WHERE fcr_id IS NULL;
ALTER TABLE project_flocks
ALTER COLUMN fcr_id SET NOT NULL;
END IF;
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'project_flocks_fcr_id_fkey'
) THEN
ALTER TABLE project_flocks
DROP CONSTRAINT project_flocks_fcr_id_fkey;
END IF;
ALTER TABLE project_flocks
ADD CONSTRAINT project_flocks_fcr_id_fkey
FOREIGN KEY (fcr_id) REFERENCES fcrs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END $$;
COMMIT;
@@ -0,0 +1,26 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flocks'
AND column_name = 'fcr_id'
) THEN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'project_flocks_fcr_id_fkey'
) THEN
ALTER TABLE project_flocks
DROP CONSTRAINT project_flocks_fcr_id_fkey;
END IF;
ALTER TABLE project_flocks
DROP COLUMN fcr_id;
END IF;
END $$;
COMMIT;
+1
View File
@@ -11,6 +11,7 @@ type AdjustmentStock struct {
PendingQty float64 `gorm:"column:pending_qty;default:0"` PendingQty float64 `gorm:"column:pending_qty;default:0"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
AdjNumber string `gorm:"column:adj_number;uniqueIndex;not null"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"` StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"`
+1
View File
@@ -14,6 +14,7 @@ type Marketing struct {
SoDate time.Time `gorm:"type:date;not null"` SoDate time.Time `gorm:"type:date;not null"`
SalesPersonId uint `gorm:"not null"` SalesPersonId uint `gorm:"not null"`
Notes string `gorm:"type:text"` Notes string `gorm:"type:text"`
MarketingType string `gorm:"type:varchar(50)"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+11 -8
View File
@@ -1,14 +1,17 @@
package entities package entities
type MarketingProduct struct { type MarketingProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
MarketingId uint `gorm:"not null"` MarketingId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"` ProductWarehouseId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"` ConvertionUnit *string `gorm:"type:varchar(20)"`
AvgWeight float64 `gorm:"type:numeric(15,3);not null"` WeightPerConvertion *float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3);not null"` Week *int `gorm:"type:integer"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"` UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
-2
View File
@@ -11,7 +11,6 @@ type ProjectFlock struct {
FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"` FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
Category string `gorm:"type:varchar(20);not null"` Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"`
ProductionStandardId uint `gorm:"column:production_standard_id"` ProductionStandardId uint `gorm:"column:production_standard_id"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
@@ -20,7 +19,6 @@ type ProjectFlock struct {
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Area Area `gorm:"foreignKey:AreaId;references:Id"` Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"` ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
@@ -24,7 +24,6 @@ type ClosingRepository interface {
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
@@ -102,12 +101,12 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
if len(params.WarehouseIDs) == 0 { if len(params.WarehouseIDs) == 0 {
return []SapronakRow{}, 0, nil return []SapronakRow{}, 0, nil
} }
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL) unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
args = append(args, params.WarehouseIDs, params.WarehouseIDs) args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing: case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 { if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL) unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
args = append(args, params.WarehouseIDs) args = append(args, params.WarehouseIDs, params.WarehouseIDs)
} }
if len(params.ProjectFlockKandangIDs) > 0 { if len(params.ProjectFlockKandangIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
@@ -174,12 +173,12 @@ func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params S
if len(params.WarehouseIDs) == 0 { if len(params.WarehouseIDs) == 0 {
return []SapronakSummaryRow{}, nil return []SapronakSummaryRow{}, nil
} }
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL) unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
args = append(args, params.WarehouseIDs, params.WarehouseIDs) args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing: case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 { if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL) unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
args = append(args, params.WarehouseIDs) args = append(args, params.WarehouseIDs, params.WarehouseIDs)
} }
if len(params.ProjectFlockKandangIDs) > 0 { if len(params.ProjectFlockKandangIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
@@ -393,22 +392,6 @@ func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFla
return agg.TotalQty, nil return agg.TotalQty, nil
} }
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
if fcrID == 0 {
return []entity.FcrStandard{}, nil
}
var standards []entity.FcrStandard
if err := r.DB().WithContext(ctx).
Where("fcr_id = ?", fcrID).
Order("weight ASC").
Find(&standards).Error; err != nil {
return nil, err
}
return standards, nil
}
func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) { func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
db := r.DB().WithContext(ctx) db := r.DB().WithContext(ctx)
@@ -456,7 +439,7 @@ SELECT
COALESCE(pi.received_date, '1970-01-01') AS sort_date, COALESCE(pi.received_date, '1970-01-01') AS sort_date,
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text, COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
COALESCE(p.po_number, '') AS reference_number, COALESCE(p.po_number, '') AS reference_number,
'Purchase' AS transaction_type, 'Pembelian' AS transaction_type,
prod.name AS product_name, prod.name AS product_name,
COALESCE(( COALESCE((
SELECT string_agg( SELECT string_agg(
@@ -505,7 +488,7 @@ SELECT
st.transfer_date AS sort_date, st.transfer_date AS sort_date,
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
st.movement_number AS reference_number, st.movement_number AS reference_number,
'Internal Transfer In' AS transaction_type, 'Mutasi' AS transaction_type,
prod.name AS product_name, prod.name AS product_name,
COALESCE(( COALESCE((
SELECT string_agg( SELECT string_agg(
@@ -549,13 +532,63 @@ JOIN uoms u ON u.id = prod.uom_id
WHERE st.to_warehouse_id IN ? WHERE st.to_warehouse_id IN ?
` `
sapronakIncomingAdjustmentsSQL = `
SELECT
CAST(ast.id AS BIGINT) AS id,
ast.created_at AS sort_date,
COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text,
COALESCE(ast.adj_number, '') AS reference_number,
'Adjustment stock' AS transaction_type,
prod.name AS product_name,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_category,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
'-' AS source_warehouse,
COALESCE(w.name, '') AS destination_warehouse,
'' AS destination,
COALESCE(ast.total_qty, 0) AS quantity,
u.id AS unit_id,
u.name AS unit,
'-' AS notes
FROM adjustment_stocks ast
JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id
JOIN warehouses w ON w.id = pw.warehouse_id
JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id
WHERE pw.warehouse_id IN ?
AND COALESCE(ast.total_qty, 0) <> 0
`
sapronakOutgoingTransfersSQL = ` sapronakOutgoingTransfersSQL = `
SELECT SELECT
CAST(st.id AS BIGINT) AS id, CAST(st.id AS BIGINT) AS id,
st.transfer_date AS sort_date, st.transfer_date AS sort_date,
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
st.movement_number AS reference_number, st.movement_number AS reference_number,
'Internal Transfer Out' AS transaction_type, 'Mutasi' AS transaction_type,
prod.name AS product_name, prod.name AS product_name,
COALESCE(( COALESCE((
SELECT string_agg( SELECT string_agg(
@@ -599,13 +632,70 @@ JOIN uoms u ON u.id = prod.uom_id
WHERE st.from_warehouse_id IN ? WHERE st.from_warehouse_id IN ?
` `
sapronakOutgoingAdjustmentsSQL = `
SELECT
CAST(ast.id AS BIGINT) AS id,
ast.created_at AS sort_date,
COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text,
COALESCE(ast.adj_number, '') AS reference_number,
'Adjustment stock' AS transaction_type,
prod.name AS product_name,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_category,
COALESCE((
SELECT string_agg(
f.name,
' ' ORDER BY
CASE
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
ELSE 1
END,
f.name
)
FROM flags f
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category,
COALESCE(w.name, '') AS source_warehouse,
'-' AS destination_warehouse,
'' AS destination,
COALESCE(ast.usage_qty, 0) AS quantity,
u.id AS unit_id,
u.name AS unit,
'-' AS notes
FROM adjustment_stocks ast
JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id
JOIN warehouses w ON w.id = pw.warehouse_id
JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id
WHERE pw.warehouse_id IN ?
AND COALESCE(ast.usage_qty, 0) <> 0
AND EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
)
`
sapronakOutgoingMarketingsSQL = ` sapronakOutgoingMarketingsSQL = `
SELECT SELECT
CAST(mp.id AS BIGINT) AS id, CAST(mp.id AS BIGINT) AS id,
m.so_date AS sort_date, m.so_date AS sort_date,
TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text, TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
m.so_number AS reference_number, m.so_number AS reference_number,
'Trading Sales' AS transaction_type, 'Penjualan' AS transaction_type,
prod.name AS product_name, prod.name AS product_name,
COALESCE(( COALESCE((
SELECT string_agg( SELECT string_agg(
@@ -653,7 +743,7 @@ WHERE pw.project_flock_kandang_id IN ?
FROM flags f FROM flags f
WHERE f.flagable_id = pw.product_id WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products' AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET') AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
) )
` `
) )
@@ -836,14 +836,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
finalPopulation := population - claimCulling finalPopulation := population - claimCulling
var standards []entity.FcrStandard
if project.FcrId > 0 {
standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId)
if err != nil {
s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data")
}
}
age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID, kandangID) age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID, kandangID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err)
@@ -893,7 +885,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
chickenDepletion = 0 chickenDepletion = 0
} }
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age)
if fcrActFromRecording != nil { if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording chickenPerformance.FcrAct = *fcrActFromRecording
} }
@@ -943,7 +935,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
eggDepletion = 0 eggDepletion = 0
} }
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age)
if fcrActFromRecording != nil { if fcrActFromRecording != nil {
eggPerf.FcrAct = *fcrActFromRecording eggPerf.FcrAct = *fcrActFromRecording
} }
@@ -1001,10 +993,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
performance.EggMass = eggMass performance.EggMass = eggMass
} }
} }
performance.DeffFcr = performance.FcrStd - performance.FcrAct
if productionStandardDetail != nil { if productionStandardDetail != nil {
if productionStandardDetail.StandardFCR != nil { if productionStandardDetail.StandardFCR != nil {
performance.FcrStd = *productionStandardDetail.StandardFCR performance.FcrStd = *productionStandardDetail.StandardFCR
performance.DeffFcr = performance.FcrStd - performance.FcrAct
} }
if !isGrowing { if !isGrowing {
if productionStandardDetail.TargetHenDayProduction != nil { if productionStandardDetail.TargetHenDayProduction != nil {
@@ -1091,8 +1083,8 @@ func (s closingService) determineProductionWeek(ctx context.Context, projectFloc
return week, nil return week, nil
} }
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64) dto.ClosingPerformanceDTO {
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) mortalityStd, fcrStd := 0.0, 0.0
fcrAct := 0.0 fcrAct := 0.0
if totalWeight > 0 { if totalWeight > 0 {
@@ -1124,21 +1116,3 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
AwgAct: awg, AwgAct: awg,
} }
} }
func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) {
if len(standards) == 0 || averageWeight <= 0 {
return 0, 0
}
closest := standards[0]
minDiff := math.Abs(closest.Weight - averageWeight)
for _, std := range standards[1:] {
diff := math.Abs(std.Weight - averageWeight)
if diff < minDiff {
minDiff = diff
closest = std
}
}
return closest.Mortality, closest.FcrNumber
}
@@ -444,30 +444,6 @@ func (r *DashboardRepositoryImpl) standardIDSubquery(filters *validation.Dashboa
return db return db
} }
func (r *DashboardRepositoryImpl) standardSourceSubquery(filters *validation.DashboardFilter) *gorm.DB {
db := r.DB().
Table("project_flocks AS pf").
Select("DISTINCT pf.production_standard_id, pf.fcr_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Where("pf.production_standard_id > 0").
Where("pf.fcr_id > 0")
if filters != nil {
if len(filters.FlockIds) > 0 {
db = db.Where("pf.id IN ?", filters.FlockIds)
}
if len(filters.KandangIds) > 0 {
db = db.Where("k.id IN ?", filters.KandangIds)
}
if len(filters.LokasiIds) > 0 {
db = db.Where("k.location_id IN ?", filters.LokasiIds)
}
}
return db
}
func (r *DashboardRepositoryImpl) GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error) { func (r *DashboardRepositoryImpl) GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error) {
seriesExpr, labelExpr, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType) seriesExpr, labelExpr, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
if err != nil { if err != nil {
@@ -24,46 +24,81 @@ func NewTransactionController(transactionService service.TransactionService) *Tr
} }
func (u *TransactionController) GetAll(c *fiber.Ctx) error { func (u *TransactionController) GetAll(c *fiber.Ctx) error {
parseOptionalUint := func(key string) (*uint, error) { parseUintListParam := func(key string) ([]uint, error) {
raw := strings.TrimSpace(c.Query(key, "")) raw := strings.TrimSpace(c.Query(key, ""))
if raw == "" { if raw == "" {
return nil, nil return nil, nil
} }
parsed, err := strconv.ParseUint(raw, 10, 64) parts := strings.Split(raw, ",")
if err != nil { ids := make([]uint, 0, len(parts))
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid "+key) for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
return nil, strconv.ErrSyntax
}
parsed, err := strconv.ParseUint(trimmed, 10, 64)
if err != nil {
return nil, err
}
if parsed == 0 {
continue
}
ids = append(ids, uint(parsed))
} }
if parsed == 0 { if len(ids) == 0 {
return nil, nil return nil, nil
} }
value := uint(parsed) return ids, nil
return &value, nil
} }
bankId, err := parseOptionalUint("bank_id") parseStringListParam := func(key string) ([]string, error) {
if err != nil { raw := strings.TrimSpace(c.Query(key, ""))
return err if raw == "" {
return nil, nil
}
parts := strings.Split(raw, ",")
values := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
return nil, strconv.ErrSyntax
}
values = append(values, trimmed)
}
if len(values) == 0 {
return nil, nil
}
return values, nil
} }
customerId, err := parseOptionalUint("customer_id")
bankIDs, err := parseUintListParam("bank_ids")
if err != nil { if err != nil {
return err return fiber.NewError(fiber.StatusBadRequest, "Invalid bank_ids")
} }
supplierId, err := parseOptionalUint("supplier_id") customerIDs, err := parseUintListParam("customer_ids")
if err != nil { if err != nil {
return err return fiber.NewError(fiber.StatusBadRequest, "Invalid customer_ids")
}
supplierIDs, err := parseUintListParam("supplier_ids")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_ids")
}
transactionTypes, err := parseStringListParam("transaction_types")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_types")
} }
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
TransactionType: c.Query("transaction_type", ""), TransactionTypes: transactionTypes,
BankId: bankId, BankIDs: bankIDs,
CustomerId: customerId, CustomerIDs: customerIDs,
SupplierId: supplierId, SupplierIDs: supplierIDs,
SortDate: c.Query("sort_date", ""), SortDate: c.Query("sort_date", ""),
StartDate: c.Query("start_date", ""), StartDate: c.Query("start_date", ""),
EndDate: c.Query("end_date", ""), EndDate: c.Query("end_date", ""),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -74,33 +74,58 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en
if params.Search != "" { if params.Search != "" {
like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%" like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%"
db = db.Joins(
"LEFT JOIN customers ON customers.id = payments.party_id AND payments.party_type = ? AND customers.deleted_at IS NULL",
string(utils.PaymentPartyCustomer),
).Joins(
"LEFT JOIN suppliers ON suppliers.id = payments.party_id AND payments.party_type = ? AND suppliers.deleted_at IS NULL",
string(utils.PaymentPartySupplier),
).Joins(
"LEFT JOIN banks ON banks.id = payments.bank_id AND banks.deleted_at IS NULL",
)
db = db.Where( db = db.Where(
`LOWER(payment_code) LIKE ? OR `LOWER(payment_code) LIKE ? OR
LOWER(COALESCE(reference_number, '')) LIKE ? OR LOWER(COALESCE(reference_number, '')) LIKE ? OR
LOWER(COALESCE(transaction_type, '')) LIKE ? OR LOWER(COALESCE(transaction_type, '')) LIKE ? OR
LOWER(COALESCE(notes, '')) LIKE ?`, LOWER(COALESCE(notes, '')) LIKE ? OR
like, like, like, like, LOWER(COALESCE(customers.name, '')) LIKE ? OR
LOWER(COALESCE(suppliers.name, '')) LIKE ? OR
LOWER(COALESCE(banks.name, '')) LIKE ?`,
like, like, like, like, like, like, like,
) )
} }
if strings.TrimSpace(params.TransactionType) != "" { if len(params.TransactionTypes) > 0 {
db = db.Where("transaction_type = ?", strings.ToUpper(strings.TrimSpace(params.TransactionType))) types := make([]string, 0, len(params.TransactionTypes))
for _, transactionType := range params.TransactionTypes {
normalized := strings.ToUpper(strings.TrimSpace(transactionType))
if normalized == "" {
continue
}
types = append(types, normalized)
}
if len(types) > 0 {
db = db.Where("transaction_type IN ?", types)
}
} }
if params.BankId != nil { if len(params.BankIDs) > 0 {
db = db.Where("bank_id = ?", *params.BankId) db = db.Where("bank_id IN ?", params.BankIDs)
} }
if params.CustomerId != nil && params.SupplierId != nil { customerIDs := params.CustomerIDs
supplierIDs := params.SupplierIDs
if len(customerIDs) > 0 && len(supplierIDs) > 0 {
db = db.Where( db = db.Where(
"(party_type = ? AND party_id = ?) OR (party_type = ? AND party_id = ?)", "(party_type = ? AND party_id IN ?) OR (party_type = ? AND party_id IN ?)",
string(utils.PaymentPartyCustomer), *params.CustomerId, string(utils.PaymentPartyCustomer), customerIDs,
string(utils.PaymentPartySupplier), *params.SupplierId, string(utils.PaymentPartySupplier), supplierIDs,
) )
} else if params.CustomerId != nil { } else if len(customerIDs) > 0 {
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartyCustomer), *params.CustomerId) db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartyCustomer), customerIDs)
} else if params.SupplierId != nil { } else if len(supplierIDs) > 0 {
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartySupplier), *params.SupplierId) db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartySupplier), supplierIDs)
} }
if startDate != nil { if startDate != nil {
@@ -1,22 +1,22 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3"` Name string `json:"name" validate:"required_strict,min=3"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"` Name *string `json:"name,omitempty" validate:"omitempty"`
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
TransactionType string `query:"transaction_type" validate:"omitempty,max=50"` TransactionTypes []string `query:"transaction_types" validate:"omitempty,dive,max=50"`
BankId *uint `query:"bank_id" validate:"omitempty,number,gt=0"` BankIDs []uint `query:"bank_ids" validate:"omitempty,dive,gt=0"`
CustomerId *uint `query:"customer_id" validate:"omitempty,number,gt=0"` CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"`
SupplierId *uint `query:"supplier_id" validate:"omitempty,number,gt=0"` SupplierIDs []uint `query:"supplier_ids" validate:"omitempty,dive,gt=0"`
SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"` SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
} }
@@ -2,9 +2,13 @@ package repositories
import ( import (
"context" "context"
"fmt"
"strconv"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type AdjustmentStockRepository interface { type AdjustmentStockRepository interface {
@@ -12,6 +16,7 @@ type AdjustmentStockRepository interface {
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error)
WithTx(tx *gorm.DB) AdjustmentStockRepository WithTx(tx *gorm.DB) AdjustmentStockRepository
DB() *gorm.DB DB() *gorm.DB
GenerateSequentialNumber(ctx context.Context, prefix string) (string, error)
} }
type adjustmentStockRepositoryImpl struct { type adjustmentStockRepositoryImpl struct {
@@ -50,3 +55,71 @@ func (r *adjustmentStockRepositoryImpl) WithTx(tx *gorm.DB) AdjustmentStockRepos
func (r *adjustmentStockRepositoryImpl) DB() *gorm.DB { func (r *adjustmentStockRepositoryImpl) DB() *gorm.DB {
return r.db return r.db
} }
func (r *adjustmentStockRepositoryImpl) GenerateSequentialNumber(ctx context.Context, prefix string) (string, error) {
var values []string
err := r.db.WithContext(ctx).
Model(&entity.AdjustmentStock{}).
Where(fmt.Sprintf("%s ILIKE ?", "adj_number"), prefix+"%").
Select("adj_number").
Order(fmt.Sprintf("%s DESC", "adj_number")).
Limit(20).
Clauses(clause.Locking{Strength: "UPDATE"}).
Pluck("adj_number", &values).Error
if err != nil {
return "", err
}
next := 1
for _, value := range values {
if number, ok := parseNumericSuffix(value, prefix); ok {
next = number + 1
break
}
}
const maxAttempts = 20
for attempt := 0; attempt < maxAttempts; attempt++ {
candidate := fmt.Sprintf("%s%0*d", prefix, 5, next)
exists, err := r.numberExists(ctx, r.db, candidate)
if err != nil {
return "", err
}
if !exists {
return candidate, nil
}
next++
}
return "", fmt.Errorf("unable to generate unique %s", "adj_number")
}
func (r *adjustmentStockRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, value string) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(&entity.AdjustmentStock{}).
Where(fmt.Sprintf("%s = ?", "adj_number"), value).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) {
return 0, false
}
suffix := strings.TrimPrefix(value, prefix)
if suffix == "" {
return 0, false
}
trimmed := strings.TrimLeft(suffix, "0")
if trimmed == "" {
trimmed = "0"
}
number, err := strconv.Atoi(trimmed)
if err != nil {
return 0, false
}
return number, true
}
@@ -200,6 +200,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
adjustmentStock := &entity.AdjustmentStock{ adjustmentStock := &entity.AdjustmentStock{
ProductWarehouseId: productWarehouse.Id, ProductWarehouseId: productWarehouse.Id,
} }
code, err := s.AdjustmentStockRepository.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
if err != nil {
return err
}
adjustmentStock.AdjNumber = code
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil { if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
@@ -32,6 +32,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
Flags: c.Query("flags", ""), Flags: c.Query("flags", ""),
KandangId: uint(c.QueryInt("kandang_id", 0)), KandangId: uint(c.QueryInt("kandang_id", 0)),
TransferContext: c.Query(utils.TransferContextKey, ""), TransferContext: c.Query(utils.TransferContextKey, ""),
Type: c.Query("type", ""),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -6,6 +6,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === DTO Structs === // === DTO Structs ===
@@ -22,6 +23,7 @@ type ProductWarehouseListDTO struct {
Product *productDTO.ProductRelationDTO `json:"product,omitempty"` Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"` ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"`
Week int `json:"week"`
CreatedUser *UserRelationDTO `json:"created_user,omitempty"` CreatedUser *UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -109,6 +111,22 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
} }
dto.ProjectFlockKandang = pfkDTO dto.ProjectFlockKandang = pfkDTO
// Calculate week for AYAM_PULLET/AYAM products
productFlags := make([]string, len(e.Product.Flags))
for i, f := range e.Product.Flags {
productFlags[i] = f.Name
}
var category string
if e.ProjectFlockKandang.ProjectFlock.Id != 0 {
category = e.ProjectFlockKandang.ProjectFlock.Category
}
now := time.Now()
_, ageInWeeks := calculateAgeFromChickin(e.ProjectFlockKandang, &now, productFlags, category)
dto.Week = ageInWeeks
} }
return dto return dto
@@ -138,3 +156,58 @@ func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNeste
Warehouse: &warehouse, Warehouse: &warehouse,
} }
} }
// Helper function to calculate age from chickin (same logic as closingMarketing.dto.go)
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, currentDate *time.Time, productFlags []string, category string) (int, int) {
if projectFlockKandang == nil || currentDate == nil || len(projectFlockKandang.Chickins) == 0 {
return 0, 0
}
// Return 0 for TRADING, TELUR, and AYAM flags (only AYAM_PULLET should have week)
for _, flag := range productFlags {
if flag == string(utils.FlagOVK) ||
flag == string(utils.FlagPakan) ||
flag == string(utils.FlagPreStarter) ||
flag == string(utils.FlagStarter) ||
flag == string(utils.FlagFinisher) ||
flag == string(utils.FlagObat) ||
flag == string(utils.FlagVitamin) ||
flag == string(utils.FlagKimia) ||
flag == string(utils.FlagEkspedisi) ||
flag == string(utils.FlagTelur) ||
flag == string(utils.FlagTelurUtuh) ||
flag == string(utils.FlagTelurPecah) ||
flag == string(utils.FlagTelurPutih) ||
flag == string(utils.FlagTelurRetak) ||
flag == string(utils.FlagAyamAfkir) ||
flag == string(utils.FlagAyamCulling) ||
flag == string(utils.FlagAyamMati) {
return 0, 0
}
}
// Find earliest chickin date
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ChickInDate.Before(earliestChickinDate) {
earliestChickinDate = chickin.ChickInDate
}
}
diff := currentDate.Sub(earliestChickinDate)
ageInDays := int(diff.Hours() / 24)
var ageInWeeks int
if ageInDays <= 0 {
ageInWeeks = 0
} else {
if category == string(utils.ProjectFlockCategoryLaying) {
ageInDays = ageInDays + 119
ageInWeeks = ((ageInDays - 1) / 7) + 1
} else {
ageInWeeks = ((ageInDays - 1) / 7) + 1
}
}
return ageInDays, ageInWeeks
}
@@ -168,9 +168,10 @@ func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []s
} }
return db. return db.
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products p_flag ON p_flag.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products"). Joins("JOIN flags f_flag ON f_flag.flagable_id = p_flag.id AND f_flag.flagable_type = ?", "products").
Where("flags.name IN ?", flags) Where("f_flag.name IN ?", flags).
Distinct()
} }
func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error { func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
@@ -46,7 +46,8 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Warehouse.Area"). Preload("Warehouse.Area").
Preload("Warehouse.Kandang"). Preload("Warehouse.Kandang").
Preload("ProjectFlockKandang"). Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.ProjectFlock") Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.Chickins")
} }
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
@@ -99,6 +100,12 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
if params.Type != "" {
if !utils.IsValidMarketingType(params.Type) {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing type")
}
}
cleanFlags := utils.ParseFlags(params.Flags) cleanFlags := utils.ParseFlags(params.Flags)
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
@@ -128,7 +135,22 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
db = db.Where("warehouse_id = ?", params.WarehouseId) db = db.Where("warehouse_id = ?", params.WarehouseId)
} }
db = s.Repository.ApplyFlagsFilter(db, cleanFlags) if params.Type != "" {
switch params.Type {
case string(utils.MarketingTypeAyamPullet):
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)})
case string(utils.MarketingTypeAyam):
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati)})
case string(utils.MarketingTypeTelur):
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak)})
case string(utils.MarketingTypeTrading):
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher), string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia), string(utils.FlagEkspedisi)})
}
}
if len(cleanFlags) > 0 {
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
}
return db.Order("product_warehouses.id DESC") return db.Order("product_warehouses.id DESC")
}) })
@@ -20,4 +20,5 @@ type Query struct {
Flags string `query:"flags" validate:"omitempty"` Flags string `query:"flags" validate:"omitempty"`
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"` TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
Type string `query:"type" validate:"omitempty,oneof=AYAM TELUR TRADING AYAM_PULLET"`
} }
@@ -2,6 +2,7 @@ package dto
import ( import (
"fmt" "fmt"
"math"
"sort" "sort"
"time" "time"
@@ -76,16 +77,21 @@ type DeliveryGroupDTO struct {
} }
type DeliveryMarketingProductDTO struct { type DeliveryMarketingProductDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
MarketingId uint `json:"marketing_id"` MarketingId uint `json:"marketing_id"`
ProductWarehouseId uint `json:"product_warehouse_id"` ProductWarehouseId uint `json:"product_warehouse_id"`
Qty float64 `json:"qty"` MarketingType string `json:"marketing_type"`
UnitPrice float64 `json:"unit_price"` Qty float64 `json:"qty"`
AvgWeight float64 `json:"avg_weight"` UnitPrice float64 `json:"unit_price"`
TotalWeight float64 `json:"total_weight"` AvgWeight float64 `json:"avg_weight"`
TotalPrice float64 `json:"total_price"` TotalWeight float64 `json:"total_weight"`
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` TotalPrice float64 `json:"total_price"`
VehicleNumber string `json:"vehicle_number,omitempty"` ConvertionUnit *string `json:"convertion_unit,omitempty"`
WeightPerConvertion *float64 `json:"weight_per_convertion,omitempty"`
TotalPeti *float64 `json:"total_peti,omitempty"`
Week *int `json:"week,omitempty"`
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"`
VehicleNumber string `json:"vehicle_number,omitempty"`
} }
func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO { func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
@@ -97,24 +103,36 @@ func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
} }
} }
func ToDeliveryMarketingProductDTO(e entity.MarketingProduct) DeliveryMarketingProductDTO { func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType string) DeliveryMarketingProductDTO {
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
if e.ProductWarehouse.Id != 0 { if e.ProductWarehouse.Id != 0 {
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
productWarehouse = &mapped productWarehouse = &mapped
} }
// Calculate total_peti only for TELUR marketing type
var totalPeti *float64
if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 {
calculated := math.Floor(e.TotalWeight / *e.WeightPerConvertion)
totalPeti = &calculated
}
return DeliveryMarketingProductDTO{ return DeliveryMarketingProductDTO{
Id: e.Id, Id: e.Id,
MarketingId: e.MarketingId, MarketingId: e.MarketingId,
ProductWarehouseId: e.ProductWarehouseId, ProductWarehouseId: e.ProductWarehouseId,
Qty: e.Qty, MarketingType: marketingType,
UnitPrice: e.UnitPrice, Qty: e.Qty,
AvgWeight: e.AvgWeight, UnitPrice: e.UnitPrice,
TotalWeight: e.TotalWeight, AvgWeight: e.AvgWeight,
TotalPrice: e.TotalPrice, TotalWeight: e.TotalWeight,
ProductWarehouse: productWarehouse, TotalPrice: e.TotalPrice,
VehicleNumber: getVehicleNumber(e), ConvertionUnit: e.ConvertionUnit,
WeightPerConvertion: e.WeightPerConvertion,
TotalPeti: totalPeti,
Week: e.Week,
ProductWarehouse: productWarehouse,
VehicleNumber: getVehicleNumber(e),
} }
} }
@@ -161,7 +179,7 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
if len(marketing.Products) > 0 { if len(marketing.Products) > 0 {
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products)) salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
for i, product := range marketing.Products { for i, product := range marketing.Products {
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product) salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType)
} }
} }
@@ -201,7 +219,7 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity
if len(marketing.Products) > 0 { if len(marketing.Products) > 0 {
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products)) salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
for i, product := range marketing.Products { for i, product := range marketing.Products {
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product) salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType)
} }
} }
@@ -1,6 +1,7 @@
package dto package dto
import ( import (
"math"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -10,13 +11,18 @@ import (
// === DTO Structs === // === DTO Structs ===
type MarketingProductDTO struct { type MarketingProductDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Qty float64 `json:"qty"` MarketingType string `json:"marketing_type"`
UnitPrice float64 `json:"unit_price"` Qty float64 `json:"qty"`
AvgWeight float64 `json:"avg_weight"` UnitPrice float64 `json:"unit_price"`
TotalWeight float64 `json:"total_weight"` AvgWeight float64 `json:"avg_weight"`
TotalPrice float64 `json:"total_price"` TotalWeight float64 `json:"total_weight"`
ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` TotalPrice float64 `json:"total_price"`
ConvertionUnit *string `json:"convertion_unit,omitempty"`
WeightPerConvertion *float64 `json:"weight_per_convertion,omitempty"`
TotalPeti *float64 `json:"total_peti,omitempty"`
Week *int `json:"week,omitempty"`
ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"`
} }
type SalesOrdersListDTO struct { type SalesOrdersListDTO struct {
@@ -29,7 +35,7 @@ type SalesOrdersListDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { func ToMarketingProductDTO(e entity.MarketingProduct, marketingType string) MarketingProductDTO {
var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO
if e.ProductWarehouse.Id != 0 { if e.ProductWarehouse.Id != 0 {
@@ -37,21 +43,33 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
productWarehouse = &mapped productWarehouse = &mapped
} }
// Calculate total_peti only for TELUR marketing type
var totalPeti *float64
if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 {
calculated := math.Floor(e.TotalWeight / *e.WeightPerConvertion)
totalPeti = &calculated
}
return MarketingProductDTO{ return MarketingProductDTO{
Id: e.Id, Id: e.Id,
Qty: e.Qty, MarketingType: marketingType,
UnitPrice: e.UnitPrice, Qty: e.Qty,
AvgWeight: e.AvgWeight, UnitPrice: e.UnitPrice,
TotalWeight: e.TotalWeight, AvgWeight: e.AvgWeight,
TotalPrice: e.TotalPrice, TotalWeight: e.TotalWeight,
ProductWarehouse: productWarehouse, TotalPrice: e.TotalPrice,
ConvertionUnit: e.ConvertionUnit,
WeightPerConvertion: e.WeightPerConvertion,
TotalPeti: totalPeti,
Week: e.Week,
ProductWarehouse: productWarehouse,
} }
} }
func ToSalesOrdersListDTO(e entity.Marketing) SalesOrdersListDTO { func ToSalesOrdersListDTO(e entity.Marketing) SalesOrdersListDTO {
products := make([]MarketingProductDTO, len(e.Products)) products := make([]MarketingProductDTO, len(e.Products))
for i, p := range e.Products { for i, p := range e.Products {
products[i] = ToMarketingProductDTO(p) products[i] = ToMarketingProductDTO(p, e.MarketingType)
} }
return SalesOrdersListDTO{ return SalesOrdersListDTO{
@@ -68,7 +86,7 @@ func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO {
if len(e.Products) > 0 { if len(e.Products) > 0 {
salesOrder = make([]MarketingProductDTO, len(e.Products)) salesOrder = make([]MarketingProductDTO, len(e.Products))
for i, product := range e.Products { for i, product := range e.Products {
salesOrder[i] = ToMarketingProductDTO(product) salesOrder[i] = ToMarketingProductDTO(product, e.MarketingType)
} }
} }
@@ -65,6 +65,7 @@ func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Customer"). Preload("Customer").
Preload("SalesPerson"). Preload("SalesPerson").
Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Product").
Preload("Products.ProductWarehouse.Product.Uom").
Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.ProductWarehouse.Warehouse").
Preload("Products.DeliveryProduct") Preload("Products.DeliveryProduct")
} }
@@ -111,6 +112,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
Preload("Customer"). Preload("Customer").
Preload("SalesPerson"). Preload("SalesPerson").
Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Product").
Preload("Products.ProductWarehouse.Product.Uom").
Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.ProductWarehouse.Warehouse").
Preload("Products.DeliveryProduct") Preload("Products.DeliveryProduct")
@@ -237,6 +239,12 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)
marketing, err := marketingRepoTx.GetByID(c.Context(), req.MarketingId, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
}
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
if err != nil { if err != nil {
@@ -283,25 +291,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate itemDeliveryDate = &parsedDate
} }
isPakanOrOVK := false totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, requestedProduct.Qty, requestedProduct.AvgWeight, requestedProduct.UnitPrice, foundMarketingProduct.Week)
if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
var totalPrice float64
if isPakanOrOVK {
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
} else {
totalPrice = totalWeight * requestedProduct.UnitPrice
}
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
@@ -374,6 +364,12 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)
marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
}
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -421,25 +417,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
itemDeliveryDate = deliveryProduct.DeliveryDate itemDeliveryDate = deliveryProduct.DeliveryDate
} }
isPakanOrOVK := false totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, requestedProduct.Qty, requestedProduct.AvgWeight, requestedProduct.UnitPrice, foundMarketingProduct.Week)
if foundMarketingProduct.ProductWarehouse.Id != 0 && foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 {
for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
var totalPrice float64
if isPakanOrOVK {
totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice
} else {
totalPrice = totalWeight * requestedProduct.UnitPrice
}
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
@@ -483,6 +461,20 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return s.getMarketingWithDeliveries(c, id) return s.getMarketingWithDeliveries(c, id)
} }
func (s *deliveryOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) {
if marketingType == string(utils.MarketingTypeTrading) {
totalWeight = 0
totalPrice = qty * unitPrice
} else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 {
totalWeight = qty * avgWeight
totalPrice = unitPrice * float64(*week) * qty
} else {
totalWeight = qty * avgWeight
totalPrice = totalWeight * unitPrice
}
return totalWeight, totalPrice
}
func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64, actorID uint) error { func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64, actorID uint) error {
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"strings" "strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -69,6 +70,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Customer"). Preload("Customer").
Preload("SalesPerson"). Preload("SalesPerson").
Preload("Products.ProductWarehouse.Product.Flags"). Preload("Products.ProductWarehouse.Product.Flags").
Preload("Products.ProductWarehouse.Product.Uom").
Preload("Products.ProductWarehouse.Warehouse") Preload("Products.ProductWarehouse.Warehouse")
} }
@@ -103,6 +105,25 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
return nil, err return nil, err
} }
// Validasi semua product harus punya marketing_type yang sama
if len(req.MarketingProducts) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "marketing_products is required")
}
firstMarketingType := req.MarketingProducts[0].MarketingType
if !utils.IsValidMarketingType(firstMarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid")
}
for i, item := range req.MarketingProducts {
if !utils.IsValidMarketingType(item.MarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1))
}
if item.MarketingType != firstMarketingType {
return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama")
}
}
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -115,6 +136,12 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
} }
for _, item := range req.MarketingProducts { for _, item := range req.MarketingProducts {
if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
}
if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid")
}
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err return nil, err
} }
@@ -149,6 +176,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
SoDate: soDate, SoDate: soDate,
SalesPersonId: req.SalesPersonId, SalesPersonId: req.SalesPersonId,
Notes: req.Notes, Notes: req.Notes,
MarketingType: firstMarketingType,
CreatedBy: actorID, CreatedBy: actorID,
} }
if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil {
@@ -161,10 +189,9 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
if product.ProductWarehouseId != 0 { if product.ProductWarehouseId != 0 {
pwIDs = append(pwIDs, product.ProductWarehouseId) pwIDs = append(pwIDs, product.ProductWarehouseId)
} }
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
} }
} }
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil { if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
return err return err
@@ -207,6 +234,23 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return nil, err return nil, err
} }
// Validasi semua product harus punya marketing_type yang sama
if len(req.MarketingProducts) > 0 {
firstMarketingType := req.MarketingProducts[0].MarketingType
if !utils.IsValidMarketingType(firstMarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid")
}
for i, item := range req.MarketingProducts {
if !utils.IsValidMarketingType(item.MarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1))
}
if item.MarketingType != firstMarketingType {
return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama")
}
}
}
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
return nil, err return nil, err
} }
@@ -234,6 +278,12 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if len(req.MarketingProducts) > 0 { if len(req.MarketingProducts) > 0 {
for _, item := range req.MarketingProducts { for _, item := range req.MarketingProducts {
if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
}
if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid")
}
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err return nil, err
} }
@@ -281,6 +331,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if req.Notes != "" { if req.Notes != "" {
updateBody["notes"] = req.Notes updateBody["notes"] = req.Notes
} }
if len(req.MarketingProducts) > 0 {
updateBody["marketing_type"] = req.MarketingProducts[0].MarketingType
}
if len(updateBody) > 0 { if len(updateBody) > 0 {
if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
@@ -309,38 +362,12 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
for _, rp := range req.MarketingProducts { for _, rp := range req.MarketingProducts {
if old, ok := oldByPW[rp.ProductWarehouseId]; ok { if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
// Get product untuk cek flag PAKAN atau OVK totalWeight, totalPrice := s.calculatePriceByMarketingType(rp.MarketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week)
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
})
if err != nil {
return err
}
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false
if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 {
for _, flag := range productWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
totalWeight := rp.Qty * rp.AvgWeight
var totalPrice float64
if isPakanOrOVK {
totalPrice = rp.Qty * rp.UnitPrice
} else {
totalPrice = totalWeight * rp.UnitPrice
}
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
} }
if err == nil && deliveryProduct.Id != 0 { if err == nil && deliveryProduct.Id != 0 {
oldQty := old.Qty oldQty := old.Qty
newQty := rp.Qty newQty := rp.Qty
@@ -363,12 +390,15 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
updateBody := map[string]any{ updateBody := map[string]any{
"product_warehouse_id": rp.ProductWarehouseId, "product_warehouse_id": rp.ProductWarehouseId,
"qty": rp.Qty, "qty": rp.Qty,
"unit_price": rp.UnitPrice, "unit_price": rp.UnitPrice,
"avg_weight": rp.AvgWeight, "avg_weight": rp.AvgWeight,
"total_weight": totalWeight, "total_weight": totalWeight,
"total_price": totalPrice, "total_price": totalPrice,
"convertion_unit": rp.ConvertionUnit,
"weight_per_convertion": rp.WeightPerConvertion,
"week": rp.Week,
} }
if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
@@ -391,7 +421,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
} }
} else { } else {
if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { if err := s.createMarketingProductWithDelivery(c.Context(), id, rp.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
} }
} }
@@ -399,7 +429,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
for _, old := range oldProducts { for _, old := range oldProducts {
if _, ok := reqByPW[old.ProductWarehouseId]; !ok { if _, ok := reqByPW[old.ProductWarehouseId]; !ok {
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
@@ -682,45 +711,21 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
return updated, nil return updated, nil
} }
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, marketingType string, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
// Get product untuk cek flag PAKAN atau OVK totalWeight, totalPrice := s.calculatePriceByMarketingType(marketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week)
productWarehouse, err := s.ProductWarehouseRepo.GetByID(ctx, rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
return db.Preload("Product.Flags")
})
if err != nil {
return err
}
// Cek apakah product punya flag PAKAN atau OVK
isPakanOrOVK := false
if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 {
for _, flag := range productWarehouse.Product.Flags {
if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) {
isPakanOrOVK = true
break
}
}
}
totalWeight := rp.Qty * rp.AvgWeight
var totalPrice float64
if isPakanOrOVK {
// PAKAN atau OVK: qty × unit_price
totalPrice = rp.Qty * rp.UnitPrice
} else {
// Produk lain: total_weight × unit_price
totalPrice = totalWeight * rp.UnitPrice
}
marketingProduct := &entity.MarketingProduct{ marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId, MarketingId: marketingId,
ProductWarehouseId: rp.ProductWarehouseId, ProductWarehouseId: rp.ProductWarehouseId,
Qty: rp.Qty, Qty: rp.Qty,
UnitPrice: rp.UnitPrice, UnitPrice: rp.UnitPrice,
AvgWeight: rp.AvgWeight, AvgWeight: rp.AvgWeight,
TotalWeight: totalWeight, TotalWeight: totalWeight,
TotalPrice: totalPrice, TotalPrice: totalPrice,
ConvertionUnit: rp.ConvertionUnit,
WeightPerConvertion: rp.WeightPerConvertion,
Week: rp.Week,
} }
if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil {
return err return err
@@ -744,3 +749,17 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
return nil return nil
} }
func (s *salesOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) {
if marketingType == string(utils.MarketingTypeTrading) {
totalWeight = 0
totalPrice = math.Round(qty*unitPrice*100) / 100
} else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 {
totalWeight = math.Round(qty*avgWeight*100) / 100
totalPrice = math.Round(unitPrice*float64(*week)*qty*100) / 100
} else {
totalWeight = math.Round(qty*avgWeight*100) / 100
totalPrice = math.Round(totalWeight*unitPrice*100) / 100
}
return totalWeight, totalPrice
}
@@ -9,11 +9,15 @@ type Create struct {
} }
type CreateMarketingProduct struct { type CreateMarketingProduct struct {
VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` MarketingType string `json:"marketing_type" validate:"required,min=1,max=50"`
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"`
UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` ConvertionUnit *string `json:"convertion_unit" validate:"omitempty,min=1,max=20"`
Qty float64 `json:"qty" validate:"required,gt=0"` WeightPerConvertion *float64 `json:"weight_per_convertion" validate:"omitempty,gt=0"`
AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` Week *int `json:"week" validate:"omitempty,gt=0"`
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"`
UnitPrice float64 `json:"unit_price" validate:"required,gt=0"`
Qty float64 `json:"qty" validate:"required,gt=0"`
AvgWeight float64 `json:"avg_weight" validate:"omitempty,gt=0"`
} }
type Update struct { type Update struct {
@@ -5,7 +5,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" areaRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
@@ -13,6 +12,7 @@ import (
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === DTO Structs (ordered) === // === DTO Structs (ordered) ===
@@ -40,7 +40,7 @@ type ProjectFlockDTO struct {
Category string `json:"category"` Category string `json:"category"`
Flock *flockRelationDTO.FlockRelationDTO `json:"flock"` Flock *flockRelationDTO.FlockRelationDTO `json:"flock"`
Area *areaRelationDTO.AreaRelationDTO `json:"area"` Area *areaRelationDTO.AreaRelationDTO `json:"area"`
Fcr *fcrRelationDTO.FcrRelationDTO `json:"fcr"` StandardFcr *float64 `json:"standard_fcr"`
Location *locationRelationDTO.LocationRelationDTO `json:"location"` Location *locationRelationDTO.LocationRelationDTO `json:"location"`
} }
@@ -97,10 +97,6 @@ func ToAreaDTO(e entity.Area) areaRelationDTO.AreaRelationDTO {
return areaRelationDTO.ToAreaRelationDTO(e) return areaRelationDTO.ToAreaRelationDTO(e)
} }
func ToFcrDTO(e entity.Fcr) fcrRelationDTO.FcrRelationDTO {
return fcrRelationDTO.ToFcrRelationDTO(e)
}
func ToLocationDTO(e entity.Location) locationRelationDTO.LocationRelationDTO { func ToLocationDTO(e entity.Location) locationRelationDTO.LocationRelationDTO {
return locationRelationDTO.ToLocationRelationDTO(e) return locationRelationDTO.ToLocationRelationDTO(e)
} }
@@ -121,11 +117,6 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO {
mapped := areaRelationDTO.ToAreaRelationDTO(e.Area) mapped := areaRelationDTO.ToAreaRelationDTO(e.Area)
area = &mapped area = &mapped
} }
var fcr *fcrRelationDTO.FcrRelationDTO
if e.Fcr.Id != 0 {
mapped := fcrRelationDTO.ToFcrRelationDTO(e.Fcr)
fcr = &mapped
}
var location *locationRelationDTO.LocationRelationDTO var location *locationRelationDTO.LocationRelationDTO
if e.Location.Id != 0 { if e.Location.Id != 0 {
mapped := locationRelationDTO.ToLocationRelationDTO(e.Location) mapped := locationRelationDTO.ToLocationRelationDTO(e.Location)
@@ -137,7 +128,7 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO {
Category: e.Category, Category: e.Category,
Flock: flock, Flock: flock,
Area: area, Area: area,
Fcr: fcr, StandardFcr: resolveProjectFlockStandardFcr(e),
Location: location, Location: location,
} }
} }
@@ -222,6 +213,22 @@ func ToChickinListDTOs(e []entity.ProjectChickin) []ChickinListDTO {
return result return result
} }
func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 {
if e.ProductionStandard.Id == 0 || len(e.ProductionStandard.ProductionStandardDetails) == 0 {
return nil
}
week := 1
if e.Category == string(utils.ProjectFlockCategoryLaying) {
week = 18
}
for _, detail := range e.ProductionStandard.ProductionStandardDetails {
if detail.Week == week && detail.StandardFCR != nil {
return detail.StandardFCR
}
}
return nil
}
func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
result := make([]ChickinSimpleDTO, len(e)) result := make([]ChickinSimpleDTO, len(e))
for i, r := range e { for i, r := range e {
@@ -81,7 +81,7 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB {
Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.Kandang.Pic").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Area").
Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard.ProductionStandardDetails").
Preload("ProjectFlockKandang.ProjectFlock.Location"). Preload("ProjectFlockKandang.ProjectFlock.Location").
Preload("ProjectFlockKandang.ProjectFlock.Location.Area") Preload("ProjectFlockKandang.ProjectFlock.Location.Area")
@@ -8,7 +8,6 @@ import (
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto" productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
@@ -31,7 +30,7 @@ type ProjectFlockDTO struct {
projectFlockDTO.ProjectFlockRelationDTO projectFlockDTO.ProjectFlockRelationDTO
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` StandardFcr *float64 `json:"standard_fcr,omitempty"`
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
@@ -86,7 +85,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO
ProjectFlockRelationDTO: pf.ProjectFlockRelationDTO, ProjectFlockRelationDTO: pf.ProjectFlockRelationDTO,
Area: pf.Area, Area: pf.Area,
Category: pf.Category, Category: pf.Category,
Fcr: pf.Fcr, StandardFcr: pf.StandardFcr,
ProductionStandard: pf.ProductionStandard, ProductionStandard: pf.ProductionStandard,
Location: pf.Location, Location: pf.Location,
CreatedUser: pf.CreatedUser, CreatedUser: pf.CreatedUser,
@@ -6,7 +6,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto" nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
@@ -28,7 +27,7 @@ type ProjectFlockListDTO struct {
ProjectFlockRelationDTO ProjectFlockRelationDTO
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` StandardFcr *float64 `json:"standard_fcr,omitempty"`
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
@@ -99,12 +98,6 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
areaSummary = &mapped areaSummary = &mapped
} }
var fcrSummary *fcrDTO.FcrRelationDTO
if e.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrRelationDTO(e.Fcr)
fcrSummary = &mapped
}
var productionStandardSummary *productionStandardDTO.ProductionStandardRelationDTO var productionStandardSummary *productionStandardDTO.ProductionStandardRelationDTO
if e.ProductionStandard.Id != 0 { if e.ProductionStandard.Id != 0 {
mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProductionStandard) mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProductionStandard)
@@ -129,7 +122,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
Kandangs: kandangSummaries, Kandangs: kandangSummaries,
ProjectBudgets: ToProjectBudgetDTOs(e.Budgets), ProjectBudgets: ToProjectBudgetDTOs(e.Budgets),
Category: e.Category, Category: e.Category,
Fcr: fcrSummary, StandardFcr: resolveProjectFlockStandardFcr(e),
ProductionStandard: productionStandardSummary, ProductionStandard: productionStandardSummary,
Location: locationSummary, Location: locationSummary,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
@@ -204,6 +197,22 @@ func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlo
} }
} }
func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 {
if e.ProductionStandard.Id == 0 || len(e.ProductionStandard.ProductionStandardDetails) == 0 {
return nil
}
week := 1
if e.Category == string(utils.ProjectFlockCategoryLaying) {
week = 18
}
for _, detail := range e.ProductionStandard.ProductionStandardDetails {
if detail.Week == week && detail.StandardFCR != nil {
return detail.StandardFCR
}
}
return nil
}
func ToProjectBudgetDTO(e entity.ProjectBudget) ProjectBudgetDTO { func ToProjectBudgetDTO(e entity.ProjectBudget) ProjectBudgetDTO {
var nonstockRef *nonstockDTO.NonstockRelationDTO var nonstockRef *nonstockDTO.NonstockRelationDTO
if e.Nonstock != nil && e.Nonstock.Id != 0 { if e.Nonstock != nil && e.Nonstock.Id != 0 {
@@ -5,7 +5,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto" productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
@@ -22,7 +21,7 @@ type ProjectFlockWithPivotDTO struct {
ProjectFlockRelationDTO ProjectFlockRelationDTO
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` StandardFcr *float64 `json:"standard_fcr,omitempty"`
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
ProductionStandardId uint `json:"production_standard_id"` ProductionStandardId uint `json:"production_standard_id"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
@@ -67,10 +66,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
mapped := areaDTO.ToAreaRelationDTO(e.ProjectFlock.Area) mapped := areaDTO.ToAreaRelationDTO(e.ProjectFlock.Area)
pfLocal.Area = &mapped pfLocal.Area = &mapped
} }
if e.ProjectFlock.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrRelationDTO(e.ProjectFlock.Fcr)
pfLocal.Fcr = &mapped
}
if e.ProjectFlock.ProductionStandard.Id != 0 { if e.ProjectFlock.ProductionStandard.Id != 0 {
mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProjectFlock.ProductionStandard) mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProjectFlock.ProductionStandard)
pfLocal.ProductionStandard = &mapped pfLocal.ProductionStandard = &mapped
@@ -83,6 +78,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
mapped := userDTO.ToUserRelationDTO(e.ProjectFlock.CreatedUser) mapped := userDTO.ToUserRelationDTO(e.ProjectFlock.CreatedUser)
pfLocal.CreatedUser = &mapped pfLocal.CreatedUser = &mapped
} }
pfLocal.StandardFcr = resolveProjectFlockStandardFcr(e.ProjectFlock)
for _, k := range e.ProjectFlock.Kandangs { for _, k := range e.ProjectFlock.Kandangs {
kb := kandangDTO.ToKandangRelationDTO(k) kb := kandangDTO.ToKandangRelationDTO(k)
@@ -23,7 +23,6 @@ type ProjectflockRepository interface {
GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error) GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error)
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
AreaExists(ctx context.Context, id uint) (bool, error) AreaExists(ctx context.Context, id uint) (bool, error)
FcrExists(ctx context.Context, id uint) (bool, error)
ProductionStandardExists(ctx context.Context, id uint) (bool, error) ProductionStandardExists(ctx context.Context, id uint) (bool, error)
LocationExists(ctx context.Context, id uint) (bool, error) LocationExists(ctx context.Context, id uint) (bool, error)
} }
@@ -67,8 +66,8 @@ func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm
return db. return db.
Preload("CreatedUser"). Preload("CreatedUser").
Preload("Area"). Preload("Area").
Preload("Fcr").
Preload("ProductionStandard"). Preload("ProductionStandard").
Preload("ProductionStandard.ProductionStandardDetails").
Preload("Location"). Preload("Location").
Preload("Kandangs"). Preload("Kandangs").
Preload("KandangHistory"). Preload("KandangHistory").
@@ -134,14 +133,12 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
likeQuery := "%" + normalized + "%" likeQuery := "%" + normalized + "%"
return db. return db.
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id"). Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN production_standards ON production_standards.id = project_flocks.production_standard_id"). Joins("LEFT JOIN production_standards ON production_standards.id = project_flocks.production_standard_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id"). Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by"). Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(` Where(`
LOWER(areas.name) LIKE ? LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ? OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(production_standards.name) LIKE ? OR LOWER(production_standards.name) LIKE ?
OR LOWER(locations.name) LIKE ? OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ? OR LOWER(locations.address) LIKE ?
@@ -172,7 +169,6 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery,
) )
} }
@@ -184,10 +180,6 @@ func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (b
return repository.Exists[entity.Area](ctx, r.DB(), id) return repository.Exists[entity.Area](ctx, r.DB(), id)
} }
func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Fcr](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) ProductionStandardExists(ctx context.Context, id uint) (bool, error) { func (r *ProjectflockRepositoryImpl) ProductionStandardExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductionStandard](ctx, r.DB(), id) return repository.Exists[entity.ProductionStandard](ctx, r.DB(), id)
} }
@@ -117,10 +117,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex
Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\"").
Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\"").
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser"). Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory"). Preload("ProjectFlock.KandangHistory").
Preload("Kandang"). Preload("Kandang").
@@ -208,10 +208,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.
Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\"").
Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\"").
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser"). Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory"). Preload("ProjectFlock.KandangHistory").
Preload("Kandang"). Preload("Kandang").
@@ -324,10 +324,10 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
record := new(entity.ProjectFlockKandang) record := new(entity.ProjectFlockKandang)
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser"). Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory"). Preload("ProjectFlock.KandangHistory").
Preload("Kandang"). Preload("Kandang").
@@ -347,10 +347,10 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID). Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area"). Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location"). Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser"). Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory"). Preload("ProjectFlock.KandangHistory").
Preload("Kandang"). Preload("Kandang").
@@ -282,7 +282,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists}, commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
commonSvc.RelationCheck{Name: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists}, commonSvc.RelationCheck{Name: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
); err != nil { ); err != nil {
@@ -334,7 +333,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
AreaId: req.AreaId, AreaId: req.AreaId,
Category: cat, Category: cat,
FcrId: req.FcrId,
ProductionStandardId: req.ProductionStandardId, ProductionStandardId: req.ProductionStandardId,
LocationId: req.LocationId, LocationId: req.LocationId,
CreatedBy: actorID, CreatedBy: actorID,
@@ -4,7 +4,6 @@ type Create struct {
FlockName string `json:"flock_name" validate:"required_strict"` FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"` Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"` ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
@@ -280,10 +280,10 @@ func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO {
} }
} }
if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil { if pfk.ProjectFlock.ProductionStandard.Id != 0 || e.StandardFcr != nil {
result.Fcr = &RecordingFcrDTO{ result.Fcr = &RecordingFcrDTO{
Id: pfk.ProjectFlock.Fcr.Id, Id: pfk.ProjectFlock.ProductionStandard.Id,
Name: pfk.ProjectFlock.Fcr.Name, Name: pfk.ProjectFlock.ProductionStandard.Name,
FcrStd: floatValue(e.StandardFcr), FcrStd: floatValue(e.StandardFcr),
} }
} }
@@ -46,7 +46,6 @@ type RecordingRepository interface {
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
@@ -92,7 +91,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("ProjectFlockKandang.Kandang.Location"). Preload("ProjectFlockKandang.Kandang.Location").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard"). Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
Preload("ProjectFlockKandang.ProjectFlock.Fcr"). // Preload("ProjectFlockKandang.ProjectFlock.Fcr").
Preload("Depletions"). Preload("Depletions").
Preload("Depletions.ProductWarehouse"). Preload("Depletions.ProductWarehouse").
Preload("Depletions.ProductWarehouse.Product"). Preload("Depletions.ProductWarehouse.Product").
@@ -448,34 +447,6 @@ func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang(
Scan(&result).Error Scan(&result).Error
return result, err return result, err
} }
func (r *RecordingRepositoryImpl) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
if fcrId == 0 || currentWeightKg <= 0 {
return 0, false, nil
}
var standard entity.FcrStandard
err := tx.
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
Order("weight ASC").
First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
err = tx.
Where("fcr_id = ?", fcrId).
Order("weight DESC").
First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, false, nil
}
}
if err != nil {
return 0, false, err
}
return standard.FcrNumber, true, nil
}
func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) {
// Body-weight tracking is removed; keep stub for report compatibility. // Body-weight tracking is removed; keep stub for report compatibility.
return 0, 0, nil return 0, 0, nil
+58 -6
View File
@@ -1,6 +1,7 @@
package dto package dto
import ( import (
"strings"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -24,12 +25,17 @@ type PurchaseRelationDTO struct {
type PurchaseListDTO struct { type PurchaseListDTO struct {
PurchaseRelationDTO PurchaseRelationDTO
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
DueDate *time.Time `json:"due_date"` DueDate *time.Time `json:"due_date"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` RequesterName string `json:"requester_name"`
UpdatedAt time.Time `json:"updated_at"` PoExpedition []string `json:"po_expedition"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"` Products []productDTO.ProductRelationDTO `json:"products"`
Location *locationDTO.LocationRelationDTO `json:"location"`
Area *areaDTO.AreaRelationDTO `json:"area"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
} }
type PurchaseDetailDTO struct { type PurchaseDetailDTO struct {
@@ -146,6 +152,10 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
mapped := userDTO.ToUserRelationDTO(p.CreatedUser) mapped := userDTO.ToUserRelationDTO(p.CreatedUser)
createdUser = &mapped createdUser = &mapped
} }
requesterName := ""
if createdUser != nil {
requesterName = createdUser.Name
}
var latestApproval *approvalDTO.ApprovalRelationDTO var latestApproval *approvalDTO.ApprovalRelationDTO
if p.LatestApproval != nil && p.LatestApproval.Id != 0 { if p.LatestApproval != nil && p.LatestApproval.Id != 0 {
@@ -153,11 +163,53 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
latestApproval = &mapped latestApproval = &mapped
} }
var (
poExpedition []string
location *locationDTO.LocationRelationDTO
area *areaDTO.AreaRelationDTO
)
productMap := make(map[uint]productDTO.ProductRelationDTO)
expeditionRefSet := make(map[string]struct{})
for i := range p.Items {
item := p.Items[i]
if item.Product != nil && item.Product.Id != 0 {
if _, exists := productMap[item.Product.Id]; !exists {
productMap[item.Product.Id] = productDTO.ToProductRelationDTO(*item.Product)
}
}
if item.ExpenseNonstock != nil && item.ExpenseNonstock.Expense != nil {
ref := strings.TrimSpace(item.ExpenseNonstock.Expense.ReferenceNumber)
if ref != "" {
if _, exists := expeditionRefSet[ref]; !exists {
expeditionRefSet[ref] = struct{}{}
poExpedition = append(poExpedition, ref)
}
}
}
if location == nil && item.Warehouse != nil && item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 {
loc := locationDTO.ToLocationRelationDTO(*item.Warehouse.Location)
location = &loc
}
if area == nil && item.Warehouse != nil && item.Warehouse.Area.Id != 0 {
ar := areaDTO.ToAreaRelationDTO(item.Warehouse.Area)
area = &ar
}
}
products := make([]productDTO.ProductRelationDTO, 0, len(productMap))
for _, prod := range productMap {
products = append(products, prod)
}
return PurchaseListDTO{ return PurchaseListDTO{
PurchaseRelationDTO: ToPurchaseRelationDTO(&p), PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
Supplier: supplier, Supplier: supplier,
DueDate: p.DueDate, DueDate: p.DueDate,
CreatedUser: createdUser, CreatedUser: createdUser,
RequesterName: requesterName,
PoExpedition: poExpedition,
Products: products,
Location: location,
Area: area,
CreatedAt: p.CreatedAt, CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt, UpdatedAt: p.UpdatedAt,
LatestApproval: latestApproval, LatestApproval: latestApproval,
@@ -31,9 +31,25 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
return []entity.Recording{}, 0, nil return []entity.Recording{}, 0, nil
} }
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action, a.step_number").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
countQuery := r.db.WithContext(ctx). countQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}). Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID) Joins("JOIN (?) AS la ON la.approvable_id = recordings.id", latestApproval).
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Where("la.step_number = ?", utils.RecordingStepDisetujui).
Where("la.action = ?", string(entity.ApprovalActionApproved))
var total int64 var total int64
if err := countQuery.Count(&total).Error; err != nil { if err := countQuery.Count(&total).Error; err != nil {
@@ -59,7 +75,10 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
dataQuery := r.db.WithContext(ctx). dataQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}). Model(&entity.Recording{}).
Joins("JOIN (?) AS la ON la.approvable_id = recordings.id", latestApproval).
Where("project_flock_kandangs_id = ?", projectFlockKandangID). Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Where("la.step_number = ?", utils.RecordingStepDisetujui).
Where("la.action = ?", string(entity.ApprovalActionApproved)).
Preload("Eggs", func(db *gorm.DB) *gorm.DB { Preload("Eggs", func(db *gorm.DB) *gorm.DB {
return db.Select("recording_eggs.*, f.name AS product_flag_name"). return db.Select("recording_eggs.*, f.name AS product_flag_name").
Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id"). Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id").
@@ -398,6 +398,9 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
if detail != nil && detail.TargetMeanBw != nil { if detail != nil && detail.TargetMeanBw != nil {
weeklyResults[i].StdBw = *detail.TargetMeanBw weeklyResults[i].StdBw = *detail.TargetMeanBw
} }
if detail != nil {
weeklyResults[i].DepStd = valueOrZero(detail.MaxDepletion)
}
} }
} }
@@ -1352,10 +1355,12 @@ func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Loc
poNumber = *purchase.PoNumber poNumber = *purchase.PoNumber
} }
prDate := purchase.CreatedAt.In(loc) startDate := resolveDebtSupplierReceivedDate(purchase, loc)
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
aging := int(endDate.Sub(startDate).Hours() / 24) aging := 0
if !startDate.IsZero() {
aging = int(endDate.Sub(startDate).Hours() / 24)
}
totalPrice := 0.0 totalPrice := 0.0
travelNumber := "-" travelNumber := "-"
@@ -1525,8 +1530,10 @@ func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool {
} }
func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int { func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int {
prDate := purchase.CreatedAt.In(loc) startDate := resolveDebtSupplierReceivedDate(purchase, loc)
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc) if startDate.IsZero() {
return 0
}
stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc) stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
if stopDate.Before(startDate) { if stopDate.Before(startDate) {
return 0 return 0
@@ -1534,6 +1541,23 @@ func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc
return int(stopDate.Sub(startDate).Hours() / 24) return int(stopDate.Sub(startDate).Hours() / 24)
} }
func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Location) time.Time {
earliest := time.Time{}
for _, item := range purchase.Items {
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
continue
}
received := item.ReceivedDate.In(loc)
if earliest.IsZero() || received.Before(earliest) {
earliest = received
}
}
if earliest.IsZero() {
return time.Time{}
}
return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
}
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) { func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
params, filters, err := s.parseHppPerKandangQuery(ctx) params, filters, err := s.parseHppPerKandangQuery(ctx)
if err != nil { if err != nil {
+45 -3
View File
@@ -212,6 +212,31 @@ const (
KandangStatusActive KandangStatus = "ACTIVE" KandangStatusActive KandangStatus = "ACTIVE"
) )
// -------------------------------------------------------------------
// Marketing Type
// -------------------------------------------------------------------
type MarketingType string
const (
MarketingTypeAyam MarketingType = "AYAM"
MarketingTypeTelur MarketingType = "TELUR"
MarketingTypeTrading MarketingType = "TRADING"
MarketingTypeAyamPullet MarketingType = "AYAM_PULLET"
)
// -------------------------------------------------------------------
// Convertion Unit
// -------------------------------------------------------------------
type ConvertionUnit string
const (
ConvertionUnitPeti ConvertionUnit = "PETI"
ConvertionUnitKG ConvertionUnit = "KG"
ConvertionUnitQty ConvertionUnit = "QTY"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// ProjectFlockCategory // ProjectFlockCategory
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -329,9 +354,10 @@ const (
PurchaseStepReceiving approvalutils.ApprovalStep = 4 PurchaseStepReceiving approvalutils.ApprovalStep = 4
PurchaseStepCompleted approvalutils.ApprovalStep = 5 PurchaseStepCompleted approvalutils.ApprovalStep = 5
PurchasePRNumberPrefix = "PR-LTI-" PurchasePRNumberPrefix = "PR-LTI-"
PurchasePONumberPrefix = "PO-LTI-" PurchasePONumberPrefix = "PO-LTI-"
PurchaseNumberPadding = 4 AdjustmentStockNumberPrefix = "ADJ-"
PurchaseNumberPadding = 4
) )
var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{
@@ -609,6 +635,22 @@ func IsValidPaymentParty(v string) bool {
return false return false
} }
func IsValidMarketingType(v string) bool {
switch MarketingType(v) {
case MarketingTypeAyam, MarketingTypeTelur, MarketingTypeTrading, MarketingTypeAyamPullet:
return true
}
return false
}
func IsValidConvertionUnit(v string) bool {
switch ConvertionUnit(v) {
case ConvertionUnitPeti, ConvertionUnitKG, ConvertionUnitQty:
return true
}
return false
}
// example use // example use
// Recording helper // Recording helper