Merge branch 'fix/be/response-check-closing' into 'development'

[FEAT/BE] fix response check closing

See merge request mbugroup/lti-api!332
This commit is contained in:
Hafizh A. Y.
2026-02-23 06:44:10 +00:00
7 changed files with 229 additions and 68 deletions
@@ -65,7 +65,7 @@ type SapronakCategoryRowDTO struct {
QtyOut float64 `json:"qty_out"` QtyOut float64 `json:"qty_out"`
QtyUsed float64 `json:"qty_used"` QtyUsed float64 `json:"qty_used"`
Description string `json:"description"` Description string `json:"description"`
ProductCategory []string `json:"product_category"` ProductCategory string `json:"product_category"`
UnitPrice float64 `json:"unit_price"` UnitPrice float64 `json:"unit_price"`
TotalAmount float64 `json:"total_amount"` TotalAmount float64 `json:"total_amount"`
Notes string `json:"notes"` Notes string `json:"notes"`
@@ -183,13 +183,13 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
"PULLET": 0, "PULLET": 0,
} }
buildFlagList := func(productID uint, fallback string) []string { buildFlagList := func(productID uint, fallback string) string {
rawFlags := productFlags[productID] rawFlags := productFlags[productID]
if len(rawFlags) == 0 { if len(rawFlags) == 0 {
if fallback == "" { if fallback == "" {
return []string{} return ""
} }
return []string{fallback} return fallback
} }
seen := make(map[string]struct{}, len(rawFlags)) seen := make(map[string]struct{}, len(rawFlags))
ordered := make([]string, 0, len(rawFlags)) ordered := make([]string, 0, len(rawFlags))
@@ -220,7 +220,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
return li < lj return li < lj
}) })
return ordered return strings.Join(ordered, " ")
} }
for _, group := range report.Groups { for _, group := range report.Groups {
@@ -317,6 +317,27 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
} }
// For chicken categories, keep qty_used aligned with qty_in - qty_out.
// Sales are excluded; usage represents remaining after transfers.
adjustChicken := func(cat *SapronakCategoryDTO) {
if cat == nil {
return
}
for i := range cat.Rows {
row := &cat.Rows[i]
remaining := row.QtyIn - row.QtyOut
if remaining < 0 {
remaining = 0
}
row.QtyUsed = remaining
if row.UnitPrice > 0 {
row.TotalAmount = row.QtyUsed * row.UnitPrice
}
}
}
adjustChicken(result.Doc)
adjustChicken(result.Pullet)
buildTotals := func(cat *SapronakCategoryDTO, label string) { buildTotals := func(cat *SapronakCategoryDTO, label string) {
if cat == nil { if cat == nil {
return return
@@ -345,5 +366,22 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
buildTotals(result.Doc, "TOTAL DOC") buildTotals(result.Doc, "TOTAL DOC")
buildTotals(result.Ovk, "TOTAL OVK") buildTotals(result.Ovk, "TOTAL OVK")
buildTotals(result.Pakan, "TOTAL PAKAN") buildTotals(result.Pakan, "TOTAL PAKAN")
// For chicken categories, enforce total qty_used = qty_in - qty_out.
adjustChickenTotal := func(cat *SapronakCategoryDTO) {
if cat == nil {
return
}
remaining := cat.Total.QtyIn - cat.Total.QtyOut
if remaining < 0 {
remaining = 0
}
cat.Total.QtyUsed = remaining
if cat.Total.AvgUnitPrice > 0 {
cat.Total.TotalAmount = cat.Total.AvgUnitPrice * remaining
}
}
adjustChickenTotal(result.Doc)
adjustChickenTotal(result.Pullet)
return result return result
} }
@@ -1029,17 +1029,18 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Joins("LEFT JOIN products p_resolve ON p_resolve.id = COALESCE(pi.product_id, pw_ltt.product_id, pw.product_id)"). Joins("LEFT JOIN product_warehouses pw_pc ON pw_pc.id = pc.product_warehouse_id").
Joins("LEFT JOIN products p_resolve ON p_resolve.id = COALESCE(pi.product_id, pw_ltt.product_id, pw_pc.product_id, pw.product_id)").
Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()). Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Where(` Where(`
(sa.usable_type = ? AND r.project_flock_kandangs_id = ?) (sa.usable_type = ? AND r.project_flock_kandangs_id = ? AND f.name IN ?)
OR OR
(sa.usable_type = ? AND pc_used.project_flock_kandang_id = ?) (sa.usable_type = ? AND pc_used.project_flock_kandang_id = ? AND f.name IN ?)
`, `,
fifo.UsableKeyRecordingStock.String(), projectFlockKandangID, fifo.UsableKeyRecordingStock.String(), projectFlockKandangID, sapronakFlagsUsage,
fifo.UsableKeyProjectChickin.String(), projectFlockKandangID, fifo.UsableKeyProjectChickin.String(), projectFlockKandangID, sapronakFlagsChickin,
) )
query = r.joinSapronakProductFlag(query, "p_resolve"). query = r.joinSapronakProductFlag(query, "p_resolve").
Group(` Group(`
@@ -1447,51 +1448,90 @@ func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.C
return map[uint][]SapronakDetailRow{}, nil return map[uint][]SapronakDetailRow{}, nil
} }
pfpType := fifo.StockableKeyProjectFlockPopulation.String()
query := r.withCtx(ctx). query := r.withCtx(ctx).
Table("stock_allocations AS sa"). Table("stock_allocations AS sa").
Select(` Select(fmt.Sprintf(`
pw.product_id AS product_id, p_resolve.id AS product_id,
p.name AS product_name, p_resolve.name AS product_name,
f.name AS flag, f.name AS flag,
COALESCE( CASE
pi.received_date, WHEN sa.stockable_type = '%s' THEN COALESCE(
st.transfer_date, pi_pc.received_date,
lt.transfer_date, st_pc.transfer_date,
ast.created_at lt_pc.transfer_date,
) AS date, ast_pc.created_at,
COALESCE( pc.chick_in_date
po.po_number, )
st.movement_number, ELSE COALESCE(
lt.transfer_number, pi.received_date,
CONCAT('ADJ-', ast.id), st.transfer_date,
'' lt.transfer_date,
) AS reference, ast.created_at
)
END AS date,
CASE
WHEN sa.stockable_type = '%s' THEN COALESCE(
po_pc.po_number,
st_pc.movement_number,
lt_pc.transfer_number,
CASE WHEN ast_pc.id IS NOT NULL THEN CONCAT('ADJ-', ast_pc.id) END,
CONCAT('CHICKIN-', pc.id),
''
)
ELSE COALESCE(
po.po_number,
st.movement_number,
lt.transfer_number,
CASE WHEN ast.id IS NOT NULL THEN CONCAT('ADJ-', ast.id) END,
''
)
END AS reference,
0 AS qty_in, 0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out, COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(pi.price, p.product_price, 0) AS price CASE
`). WHEN sa.stockable_type = '%s' THEN COALESCE(pi_pc.price, p_resolve.product_price, 0)
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). ELSE COALESCE(pi.price, p_resolve.product_price, 0)
Joins("JOIN products p ON p.id = pw.product_id"). END AS price
`, pfpType, pfpType, pfpType)).
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()). Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
Joins("JOIN product_warehouses pw_sales ON pw_sales.id = mdp.product_warehouse_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id"). Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id"). Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN product_warehouses pw_ltt ON pw_ltt.id = ltt.product_warehouse_id").
Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Joins("LEFT JOIN stock_allocations sa_pc ON sa_pc.usable_type = ? AND sa_pc.usable_id = pc.id", fifo.UsableKeyProjectChickin.String()).
Joins("LEFT JOIN purchase_items pi_pc ON pi_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
Joins("LEFT JOIN purchases po_pc ON po_pc.id = pi_pc.purchase_id").
Joins("LEFT JOIN stock_transfer_details std_pc ON std_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
Joins("LEFT JOIN stock_transfers st_pc ON st_pc.id = std_pc.stock_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt_pc ON ltt_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
Joins("LEFT JOIN laying_transfers lt_pc ON lt_pc.id = ltt_pc.laying_transfer_id").
Joins("LEFT JOIN adjustment_stocks ast_pc ON ast_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
Joins("LEFT JOIN product_warehouses pw_pc ON pw_pc.id = pc.product_warehouse_id").
Joins(fmt.Sprintf("LEFT JOIN products p_resolve ON p_resolve.id = CASE WHEN sa.stockable_type = '%s' THEN pw_pc.product_id ELSE COALESCE(pi.product_id, pw_ltt.product_id, pw.product_id) END", pfpType)).
Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()). Where("sa.stockable_type <> ?", fifo.StockableKeyRecordingEgg.String()).
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). Where("pw_sales.project_flock_kandang_id = ?", projectFlockKandangID).
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Group(` Group(`
pw.product_id, p.name, f.name, p_resolve.id, p_resolve.name, f.name,
pi_pc.received_date, st_pc.transfer_date, lt_pc.transfer_date, ast_pc.created_at, pc.chick_in_date,
pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at,
po_pc.po_number, st_pc.movement_number, lt_pc.transfer_number, ast_pc.id, pc.id,
po.po_number, st.movement_number, lt.transfer_number, ast.id, po.po_number, st.movement_number, lt.transfer_number, ast.id,
pi.price, p.product_price pi_pc.price, pi.price, p_resolve.product_price, sa.stockable_type
`) `)
query = r.joinSapronakProductFlag(query, "p") query = r.joinSapronakProductFlag(query, "p_resolve")
return scanAndGroupDetails(query) return scanAndGroupDetails(query)
} }
@@ -470,6 +470,7 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
// should not be counted yet. Only when category is LAYING we allow // should not be counted yet. Only when category is LAYING we allow
// pullet usage to contribute to qty_used. // pullet usage to contribute to qty_used.
isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying)) isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
hasChickin := len(pfk.Chickins) > 0
if !isLaying { if !isLaying {
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows)) filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
@@ -775,6 +776,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
if hasChickin && (strings.EqualFold(flag, "DOC") || strings.EqualFold(flag, "PULLET") || strings.EqualFold(flag, "LAYER")) {
continue
}
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { if d.Flag == "" {
@@ -794,6 +798,10 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
// For chicken, we don't count sales as sapronak outflow.
if strings.EqualFold(flag, "DOC") || strings.EqualFold(flag, "PULLET") || strings.EqualFold(flag, "LAYER") {
continue
}
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
if d.Flag == "" { if d.Flag == "" {
@@ -4,8 +4,8 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -117,18 +117,30 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
Preload("Products.DeliveryProduct") Preload("Products.DeliveryProduct")
if params.Status != "" { if params.Status != "" {
status := strings.TrimSpace(params.Status)
latestApprovalSubQuery := s.MarketingRepo.DB(). latestApprovalSubQuery := s.MarketingRepo.DB().
WithContext(c.Context()). WithContext(c.Context()).
Table("approvals"). Table("approvals").
Select("DISTINCT ON (approvable_id) approvable_id, step_name"). Select("DISTINCT ON (approvable_id) approvable_id, step_name, action").
Where("approvable_type = ?", utils.ApprovalWorkflowMarketing.String()). Where("approvable_type = ?", utils.ApprovalWorkflowMarketing.String()).
Order("approvable_id, id DESC") Order("approvable_id, id DESC")
db = db.Where(`EXISTS (
SELECT 1 if strings.EqualFold(status, "DITOLAK") {
FROM (?) AS latest_approval db = db.Where(`EXISTS (
WHERE latest_approval.approvable_id = marketings.id SELECT 1
AND LOWER(latest_approval.step_name) = LOWER(?) FROM (?) AS latest_approval
)`, latestApprovalSubQuery, params.Status) WHERE latest_approval.approvable_id = marketings.id
AND latest_approval.action = ?
)`, latestApprovalSubQuery, string(entity.ApprovalActionRejected))
} else {
db = db.Where(`EXISTS (
SELECT 1
FROM (?) AS latest_approval
WHERE latest_approval.approvable_id = marketings.id
AND LOWER(latest_approval.step_name) = LOWER(?)
AND (latest_approval.action IS NULL OR latest_approval.action <> ?)
)`, latestApprovalSubQuery, status, string(entity.ApprovalActionRejected))
}
} }
if params.Search != "" { if params.Search != "" {
@@ -548,11 +560,17 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
if deliveryProduct == nil || deliveryProduct.Id == 0 { if deliveryProduct == nil || deliveryProduct.Id == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found") return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
} }
if deliveryProduct.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product warehouse not found")
}
if deliveryProduct.ProductWarehouseId != marketingProduct.ProductWarehouseId {
return fiber.NewError(fiber.StatusBadRequest, "Delivery product warehouse mismatch with marketing product")
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery, UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id, UsableID: deliveryProduct.Id,
ProductWarehouseID: marketingProduct.ProductWarehouseId, ProductWarehouseID: deliveryProduct.ProductWarehouseId,
Quantity: requestedQty, Quantity: requestedQty,
AllowPending: false, AllowPending: false,
Tx: tx, Tx: tx,
@@ -573,12 +591,12 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
Decrease: result.UsageQuantity, Decrease: result.UsageQuantity,
LoggableType: string(utils.StockLogTypeMarketing), LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id, LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId, ProductWarehouseId: deliveryProduct.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity), Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, deliveryProduct.ProductWarehouseId, 1)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
} }
@@ -152,6 +152,31 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
} }
} }
requestedByWarehouse := make(map[uint]float64)
for _, item := range req.MarketingProducts {
if item.ProductWarehouseId == 0 {
continue
}
requestedByWarehouse[item.ProductWarehouseId] += item.Qty
}
for pwID, requestedQty := range requestedByWarehouse {
productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), pwID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", pwID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock availability")
}
availableQty := productWarehouse.Quantity
if availableQty+1e-6 < requestedQty {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Stok tidak mencukupi untuk gudang %d: diminta %.3f, tersedia %.3f", pwID, requestedQty, availableQty),
)
}
}
soDate, err := utils.ParseDateString(req.Date) soDate, err := utils.ParseDateString(req.Date)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
@@ -308,25 +308,23 @@ func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*Closin
} }
for _, pw := range productWarehouses { for _, pw := range productWarehouses {
if pw.Quantity > 0 { category := ""
category := "" if pw.Product.ProductCategory.Id != 0 {
if pw.Product.ProductCategory.Id != 0 { category = pw.Product.ProductCategory.Name
category = pw.Product.ProductCategory.Name
}
uomName := ""
if pw.Product.Uom.Id != 0 {
uomName = pw.Product.Uom.Name
}
stockRemain = append(stockRemain, StockRemainingDetail{
FlagName: string(flagName),
ProductWarehouseId: pw.Id,
ProductId: pw.ProductId,
ProductName: pw.Product.Name,
ProductCategory: category,
Uom: uomName,
Quantity: pw.Quantity,
})
} }
uomName := ""
if pw.Product.Uom.Id != 0 {
uomName = pw.Product.Uom.Name
}
stockRemain = append(stockRemain, StockRemainingDetail{
FlagName: string(flagName),
ProductWarehouseId: pw.Id,
ProductId: pw.ProductId,
ProductName: pw.Product.Name,
ProductCategory: category,
Uom: uomName,
Quantity: pw.Quantity,
})
} }
} }
} }
@@ -585,7 +583,7 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
} }
} }
if s.ApprovalSvc != nil { if s.ApprovalSvc != nil {
reopenAction := entity.ApprovalActionUpdated reopenAction := entity.ApprovalActionApproved
// Hindari duplikasi jika approval terakhir sudah Disetujui + Updated // Hindari duplikasi jika approval terakhir sudah Disetujui + Updated
latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil) latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
if lerr != nil { if lerr != nil {
@@ -611,6 +609,31 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
return nil, aerr return nil, aerr
} }
} }
// Pastikan approval project flock kembali ke Aktif
latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
if lerr != nil {
return nil, lerr
}
shouldCreatePF := true
if latestPF != nil &&
latestPF.StepNumber == uint16(utils.ProjectFlockStepAktif) &&
latestPF.Action != nil && *latestPF.Action == reopenAction {
shouldCreatePF = false
}
if shouldCreatePF {
if _, aerr := s.ApprovalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
pfk.ProjectFlockId,
utils.ProjectFlockStepAktif,
&reopenAction,
actorID,
nil,
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
return nil, aerr
}
}
} }
default: default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose") return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
@@ -42,6 +42,7 @@ type KandangWithProjectFlockIdDTO struct {
kandangDTO.KandangRelationDTO kandangDTO.KandangRelationDTO
ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
Period int `json:"period"` Period int `json:"period"`
ClosedAt *time.Time `json:"closed_at,omitempty"`
} }
type ProjectFlockDetailDTO struct { type ProjectFlockDetailDTO struct {
@@ -74,20 +75,28 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
for i, kandang := range e.Kandangs { for i, kandang := range e.Kandangs {
var ( var (
pfkId uint pfkId uint
period int period int
closedAt *time.Time
) )
for _, kh := range e.KandangHistory { for _, kh := range e.KandangHistory {
if kh.KandangId == kandang.Id { if kh.KandangId == kandang.Id {
pfkId = kh.Id pfkId = kh.Id
period = kh.Period period = kh.Period
closedAt = kh.ClosedAt
break break
} }
} }
mapped := kandangDTO.ToKandangRelationDTO(kandang)
if closedAt != nil {
// Jangan ubah tabel kandang, hanya override status di response.
mapped.Status = string(utils.KandangStatusNonActive)
}
kandangSummaries[i] = KandangWithProjectFlockIdDTO{ kandangSummaries[i] = KandangWithProjectFlockIdDTO{
KandangRelationDTO: kandangDTO.ToKandangRelationDTO(kandang), KandangRelationDTO: mapped,
ProjectFlockKandangId: pfkId, ProjectFlockKandangId: pfkId,
Period: period, Period: period,
ClosedAt: closedAt,
} }
} }
} }