fix: adjusment module depletion, chickin, recording refactor

This commit is contained in:
Hafizh A. Y
2026-02-28 21:35:07 +07:00
parent 944604adad
commit dd61b66af0
11 changed files with 1906 additions and 1343 deletions
@@ -487,16 +487,12 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
continue
}
asOf := usableRow.SortAt
if req.AsOf != nil && asOf.Before(*req.AsOf) {
asOf = *req.AsOf
}
allocateRes, allocateErr := s.allocateInternal(ctx, tx, AllocateRequest{
FlagGroupCode: req.FlagGroupCode,
ProductWarehouseID: req.ProductWarehouseID,
Usable: usableRow.Ref,
NeedQty: desiredQty,
AsOf: &asOf,
AsOf: nil,
})
if allocateErr != nil {
err = allocateErr
@@ -151,19 +151,29 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
usedExpr := "0::numeric"
pendingExpr := "0::numeric"
availableExpr := baseQtyExpr
extraArgs := make([]any, 0, 1)
extraArgs := make([]any, 0, 2)
whereExtraArgs := make([]any, 0, 1)
if req.Lane == LaneStockable {
if rule.UsedQuantityCol != nil && strings.TrimSpace(*rule.UsedQuantityCol) != "" {
usedCol, _ := mustSafeIdentifier(*rule.UsedQuantityCol)
usedExpr = fmt.Sprintf("COALESCE(src.%s,0)::numeric", usedCol)
} else {
// NOTE:
// usedExpr is referenced twice in the generated SELECT:
// 1) as used_quantity
// 2) inside available_quantity = base - usedExpr
// plus once in stockable WHERE clause via availableExpr > 0.
// We split the args because the WHERE placeholder order appears
// after product/flag filter placeholders in the final SQL.
usedExpr = fmt.Sprintf(
"(SELECT COALESCE(SUM(sa.qty),0)::numeric FROM stock_allocations sa WHERE sa.stockable_type = ? AND sa.stockable_id = src.%s AND sa.status = '%s')",
sourceIDCol,
activeAllocationStatus(),
)
extraArgs = append(extraArgs, rule.LegacyTypeKey)
extraArgs = append(extraArgs, rule.LegacyTypeKey)
whereExtraArgs = append(whereExtraArgs, rule.LegacyTypeKey)
}
availableExpr = fmt.Sprintf("(%s - %s)", baseQtyExpr, usedExpr)
} else {
@@ -179,6 +189,12 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
return "", nil, err
}
functionCodeExpr := "?::text"
functionCodeArgs := []any{rule.FunctionCode}
if rule.SourceTable == "adjustment_stocks" {
functionCodeExpr = "COALESCE(NULLIF(src.function_code,''), ?::text)"
}
whereParts := []string{
fmt.Sprintf("src.%s = ?", productWarehouseCol),
fmt.Sprintf(`EXISTS (
@@ -209,7 +225,7 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
SELECT
?::text AS source_table,
?::text AS legacy_type_key,
?::text AS function_code,
%s AS function_code,
src.%s AS source_id,
src.%s AS product_warehouse_id,
%s AS sort_at,
@@ -221,20 +237,21 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
FROM %s src
%s
WHERE %s
`, sourceIDCol, productWarehouseCol, sortExpr, baseQtyExpr, usedExpr, pendingExpr, availableExpr, sourceTable, joinClause, strings.Join(whereParts, " AND "))
`, functionCodeExpr, sourceIDCol, productWarehouseCol, sortExpr, baseQtyExpr, usedExpr, pendingExpr, availableExpr, sourceTable, joinClause, strings.Join(whereParts, " AND "))
args := []any{
rule.SourceTable,
rule.LegacyTypeKey,
rule.FunctionCode,
trait.SortPriority,
}
args = append(args, functionCodeArgs...)
args = append(args, trait.SortPriority)
args = append(args, extraArgs...)
args = append(args,
req.ProductWarehouseID,
entity.FlagableTypeProduct,
req.FlagGroupCode,
)
args = append(args, whereExtraArgs...)
if req.AsOf != nil {
args = append(args, *req.AsOf)
@@ -0,0 +1,13 @@
BEGIN;
-- Restore CHICKIN route if rollback is required.
-- NOTE: released PROJECT_CHICKIN allocations are not restored by this down migration.
UPDATE fifo_stock_v2_route_rules
SET is_active = TRUE,
updated_at = NOW()
WHERE flag_group_code = 'AYAM'
AND lane = 'USABLE'
AND function_code = 'CHICKIN_OUT'
AND source_table = 'project_chickins';
COMMIT;
@@ -0,0 +1,151 @@
BEGIN;
-- Disable CHICKIN as FIFO USABLE so chick-in acts as business tagging/conversion,
-- not physical stock consumption.
UPDATE fifo_stock_v2_route_rules
SET is_active = FALSE,
updated_at = NOW()
WHERE flag_group_code = 'AYAM'
AND lane = 'USABLE'
AND function_code = 'CHICKIN_OUT'
AND source_table = 'project_chickins'
AND is_active = TRUE;
-- Release existing active allocations created by PROJECT_CHICKIN
-- and return warehouse qty back.
WITH released AS (
UPDATE stock_allocations
SET status = 'RELEASED',
released_at = COALESCE(released_at, NOW()),
updated_at = NOW(),
note = CASE
WHEN COALESCE(note, '') = '' THEN 'fifo_v2_chickin_conversion_release'
ELSE note || '; fifo_v2_chickin_conversion_release'
END
WHERE usable_type = 'PROJECT_CHICKIN'
AND status = 'ACTIVE'
RETURNING product_warehouse_id, qty
),
pw_delta AS (
SELECT product_warehouse_id, COALESCE(SUM(qty), 0) AS qty_delta
FROM released
GROUP BY product_warehouse_id
)
UPDATE product_warehouses pw
SET qty = COALESCE(pw.qty, 0) + d.qty_delta
FROM pw_delta d
WHERE pw.id = d.product_warehouse_id;
-- Resync stockable total_used columns from remaining ACTIVE allocations.
-- purchase_items (PURCHASE_ITEMS)
UPDATE purchase_items pi
SET total_used = COALESCE(a.used, 0)
FROM (
SELECT stockable_id, SUM(qty) AS used
FROM stock_allocations
WHERE status = 'ACTIVE'
AND stockable_type = 'PURCHASE_ITEMS'
GROUP BY stockable_id
) a
WHERE pi.id = a.stockable_id;
UPDATE purchase_items pi
SET total_used = 0
WHERE NOT EXISTS (
SELECT 1
FROM stock_allocations sa
WHERE sa.status = 'ACTIVE'
AND sa.stockable_type = 'PURCHASE_ITEMS'
AND sa.stockable_id = pi.id
);
-- stock_transfer_details (STOCK_TRANSFER_IN)
UPDATE stock_transfer_details std
SET total_used = COALESCE(a.used, 0)
FROM (
SELECT stockable_id, SUM(qty) AS used
FROM stock_allocations
WHERE status = 'ACTIVE'
AND stockable_type = 'STOCK_TRANSFER_IN'
GROUP BY stockable_id
) a
WHERE std.id = a.stockable_id;
UPDATE stock_transfer_details std
SET total_used = 0
WHERE NOT EXISTS (
SELECT 1
FROM stock_allocations sa
WHERE sa.status = 'ACTIVE'
AND sa.stockable_type = 'STOCK_TRANSFER_IN'
AND sa.stockable_id = std.id
);
-- adjustment_stocks (ADJUSTMENT_IN)
UPDATE adjustment_stocks ast
SET total_used = COALESCE(a.used, 0)
FROM (
SELECT stockable_id, SUM(qty) AS used
FROM stock_allocations
WHERE status = 'ACTIVE'
AND stockable_type = 'ADJUSTMENT_IN'
GROUP BY stockable_id
) a
WHERE ast.id = a.stockable_id;
UPDATE adjustment_stocks ast
SET total_used = 0
WHERE NOT EXISTS (
SELECT 1
FROM stock_allocations sa
WHERE sa.status = 'ACTIVE'
AND sa.stockable_type = 'ADJUSTMENT_IN'
AND sa.stockable_id = ast.id
);
-- laying_transfer_targets (TRANSFERTOLAYING_IN)
UPDATE laying_transfer_targets ltt
SET total_used = COALESCE(a.used, 0)
FROM (
SELECT stockable_id, SUM(qty) AS used
FROM stock_allocations
WHERE status = 'ACTIVE'
AND stockable_type = 'TRANSFERTOLAYING_IN'
GROUP BY stockable_id
) a
WHERE ltt.id = a.stockable_id;
UPDATE laying_transfer_targets ltt
SET total_used = 0
WHERE NOT EXISTS (
SELECT 1
FROM stock_allocations sa
WHERE sa.status = 'ACTIVE'
AND sa.stockable_type = 'TRANSFERTOLAYING_IN'
AND sa.stockable_id = ltt.id
);
-- recording_eggs (RECORDING_EGG)
UPDATE recording_eggs re
SET total_used = COALESCE(a.used, 0)
FROM (
SELECT stockable_id, SUM(qty) AS used
FROM stock_allocations
WHERE status = 'ACTIVE'
AND stockable_type = 'RECORDING_EGG'
GROUP BY stockable_id
) a
WHERE re.id = a.stockable_id;
UPDATE recording_eggs re
SET total_used = 0
WHERE NOT EXISTS (
SELECT 1
FROM stock_allocations sa
WHERE sa.status = 'ACTIVE'
AND sa.stockable_type = 'RECORDING_EGG'
AND sa.stockable_id = re.id
);
COMMIT;
@@ -46,6 +46,7 @@ type adjustmentService struct {
const (
adjustmentLaneStockable = "STOCKABLE"
adjustmentLaneUsable = "USABLE"
flagGroupAyam = "AYAM"
)
func NewAdjustmentService(
@@ -129,8 +130,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if functionCode == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype is required")
}
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) {
functionCode = string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut)
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut) {
return nil, fiber.NewError(
fiber.StatusBadRequest,
"RECORDING_DEPLETION_OUT tidak boleh diinput manual. Gunakan RECORDING_DEPLETION_IN, sistem akan otomatis membuat depletion-out AYAM",
)
}
warehouseID, err := s.resolveWarehouseID(c.Context(), req)
@@ -211,6 +215,133 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
}
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) {
if routeMeta.Lane != adjustmentLaneStockable {
return fiber.NewError(fiber.StatusBadRequest, "Transaction subtype depletion in harus lane STOCKABLE")
}
if projectFlockKandangID == nil || *projectFlockKandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id aktif wajib tersedia untuk depletion conversion")
}
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
sourcePW, err := s.resolveAyamSourceProductWarehouse(ctx, tx, warehouseID, *projectFlockKandangID)
if err != nil {
return err
}
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
ctx,
tx,
[]uint{productWarehouse.Id, sourcePW.Id},
); err != nil {
return err
}
sourceRoute, err := s.resolveRouteByFunctionCode(
ctx,
sourcePW.ProductId,
string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut),
)
if err != nil {
return err
}
if sourceRoute.Lane != adjustmentLaneUsable {
return fiber.NewError(fiber.StatusBadRequest, "Route depletion out untuk produk AYAM tidak valid")
}
sourceCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
if err != nil {
return err
}
sourceAdjustment := &entity.AdjustmentStock{
ProductWarehouseId: sourcePW.Id,
TransactionType: transactionType,
FunctionCode: sourceRoute.FunctionCode,
UsageQty: qty,
Price: req.Price,
GrandTotal: grandTotal,
AdjNumber: sourceCode,
}
if err := adjustmentStockRepoTX.CreateOne(ctx, sourceAdjustment, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion source adjustment stock record")
}
destCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
if err != nil {
return err
}
destinationAdjustment := &entity.AdjustmentStock{
ProductWarehouseId: productWarehouse.Id,
TransactionType: transactionType,
FunctionCode: routeMeta.FunctionCode,
TotalQty: qty,
Price: req.Price,
GrandTotal: grandTotal,
AdjNumber: destCode,
}
if err := adjustmentStockRepoTX.CreateOne(ctx, destinationAdjustment, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion destination adjustment stock record")
}
sourceAsOf := sourceAdjustment.CreatedAt
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
FlagGroupCode: sourceRoute.FlagGroupCode,
ProductWarehouseID: sourcePW.Id,
AsOf: &sourceAsOf,
Tx: tx,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-out AYAM via FIFO v2: %v", err))
}
destinationAsOf := destinationAdjustment.CreatedAt
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
FlagGroupCode: routeMeta.FlagGroupCode,
ProductWarehouseID: destinationAdjustment.ProductWarehouseId,
AsOf: &destinationAsOf,
Tx: tx,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-in destination via FIFO v2: %v", err))
}
refreshedSource, err := adjustmentStockRepoTX.GetByID(ctx, sourceAdjustment.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion source adjustment stock")
}
refreshedDestination, err := adjustmentStockRepoTX.GetByID(ctx, destinationAdjustment.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion destination adjustment stock")
}
if err := s.createAdjustmentStockLog(
ctx,
stockLogRepoTX,
refreshedSource.Id,
refreshedSource.ProductWarehouseId,
note,
actorID,
0,
refreshedSource.UsageQty+refreshedSource.PendingQty,
); err != nil {
return err
}
if err := s.createAdjustmentStockLog(
ctx,
stockLogRepoTX,
refreshedDestination.Id,
refreshedDestination.ProductWarehouseId,
note,
actorID,
refreshedDestination.TotalQty,
0,
); err != nil {
return err
}
createdAdjustmentStockId = destinationAdjustment.Id
return nil
}
adjustmentStock := &entity.AdjustmentStock{
ProductWarehouseId: productWarehouse.Id,
TransactionType: transactionType,
@@ -264,29 +395,16 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
decreaseQty = refreshedAdjustment.UsageQty
}
stockLogs, err := stockLogRepoTX.GetByProductWarehouse(ctx, productWarehouse.Id, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
currentStock := 0.0
if len(stockLogs) > 0 {
currentStock = stockLogs[0].Stock
}
newLog := &entity.StockLog{
LoggableType: string(utils.StockLogTypeAdjustment),
LoggableId: adjustmentStock.Id,
Notes: note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: actorID,
Increase: increaseQty,
Decrease: decreaseQty,
Stock: currentStock + increaseQty - decreaseQty,
}
if err := stockLogRepoTX.CreateOne(ctx, newLog, nil); err != nil {
if err := s.createAdjustmentStockLog(
ctx,
stockLogRepoTX,
adjustmentStock.Id,
productWarehouse.Id,
note,
actorID,
increaseQty,
decreaseQty,
); err != nil {
return err
}
@@ -417,6 +535,88 @@ func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context,
return uint(projectFlockKandang.Id), nil
}
func (s *adjustmentService) resolveAyamSourceProductWarehouse(
ctx context.Context,
tx *gorm.DB,
warehouseID uint,
projectFlockKandangID uint,
) (*entity.ProductWarehouse, error) {
if tx == nil {
return nil, fmt.Errorf("transaction is required")
}
if projectFlockKandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id tidak valid untuk depletion conversion")
}
var sourcePW entity.ProductWarehouse
err := tx.WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where(`
EXISTS (
SELECT 1
FROM flags f
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE f.flagable_type = ?
AND f.flagable_id = product_warehouses.product_id
AND fm.flag_group_code = ?
)
`, entity.FlagableTypeProduct, flagGroupAyam).
Order(gorm.Expr("CASE WHEN warehouse_id = ? THEN 0 ELSE 1 END ASC", warehouseID)).
Order("id ASC").
Take(&sourcePW).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Produk sumber AYAM pada project flock kandang yang sama tidak ditemukan")
}
return nil, err
}
return &sourcePW, nil
}
func (s *adjustmentService) createAdjustmentStockLog(
ctx context.Context,
stockLogRepo stockLogsRepo.StockLogRepository,
adjustmentID uint,
productWarehouseID uint,
note string,
actorID uint,
increaseQty float64,
decreaseQty float64,
) error {
if stockLogRepo == nil || adjustmentID == 0 || productWarehouseID == 0 {
return nil
}
if increaseQty == 0 && decreaseQty == 0 {
return nil
}
stockLogs, err := stockLogRepo.GetByProductWarehouse(ctx, productWarehouseID, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
currentStock := 0.0
if len(stockLogs) > 0 {
currentStock = stockLogs[0].Stock
}
newLog := &entity.StockLog{
LoggableType: string(utils.StockLogTypeAdjustment),
LoggableId: adjustmentID,
Notes: note,
ProductWarehouseId: productWarehouseID,
CreatedBy: actorID,
Increase: increaseQty,
Decrease: decreaseQty,
Stock: currentStock + increaseQty - decreaseQty,
}
return stockLogRepo.CreateOne(ctx, newLog, nil)
}
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
if err := s.Validate.Struct(query); err != nil {
return nil, 0, err
@@ -605,59 +605,11 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
if tx == nil {
return errors.New("transaction is required")
}
if s.FifoStockV2Svc == nil {
return errors.New("fifo v2 service is not available")
}
if desiredQty < 0 {
return errors.New("desired quantity must be zero or greater")
}
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0); err != nil {
return err
}
asOf := chickin.ChickInDate
if asOf.IsZero() {
asOf = chickin.CreatedAt
}
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
return err
}
var refreshed entity.ProjectChickin
if err := tx.WithContext(ctx).
Where("id = ?", chickin.Id).
Take(&refreshed).Error; err != nil {
return err
}
if refreshed.UsageQty > 0 {
decreaseLog := &entity.StockLog{
Decrease: refreshed.UsageQty,
LoggableType: string(utils.StockLogTypeChikin),
LoggableId: refreshed.Id,
ProductWarehouseId: refreshed.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d", refreshed.Id),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
decreaseLog.Stock = latestStockLog.Stock
decreaseLog.Stock -= decreaseLog.Decrease
} else {
decreaseLog.Stock -= decreaseLog.Decrease
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil); err != nil {
return err
}
}
return nil
return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0)
}
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
@@ -667,9 +619,6 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
if tx == nil {
return errors.New("transaction is required")
}
if s.FifoStockV2Svc == nil {
return errors.New("fifo v2 service is not available")
}
if err := tx.WithContext(ctx).
Model(&entity.ProjectFlockPopulation{}).
@@ -678,11 +627,7 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
return err
}
asOf := chickin.ChickInDate
if asOf.IsZero() {
asOf = chickin.CreatedAt
}
return reflowChickinScope(ctx, s.FifoStockV2Svc, tx, targetPW.Id, &asOf)
return nil
}
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
@@ -692,53 +637,11 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
if tx == nil {
return errors.New("transaction is required")
}
if s.FifoStockV2Svc == nil {
return errors.New("fifo v2 service is not available")
}
var currentUsage float64
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&currentUsage).Error; err != nil {
return err
}
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
return err
}
asOf := chickin.ChickInDate
if asOf.IsZero() {
asOf = chickin.CreatedAt
}
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
return err
}
if currentUsage > 0 {
increaseLog := &entity.StockLog{
Increase: currentUsage,
LoggableType: string(utils.StockLogTypeChikin),
LoggableId: chickin.Id,
ProductWarehouseId: chickin.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, chickin.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
if len(stockLogs) > 0 {
latestStockLog := stockLogs[0]
increaseLog.Stock = latestStockLog.Stock
increaseLog.Stock += increaseLog.Increase
} else {
increaseLog.Stock += increaseLog.Increase
}
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil); err != nil {
return err
}
}
return nil
}
@@ -25,20 +25,27 @@ type RecordingRepository interface {
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
ListByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, from *time.Time) ([]entity.Recording, error)
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
CreateRecording(tx *gorm.DB, recording *entity.Recording) error
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
CreateStock(tx *gorm.DB, stock *entity.RecordingStock) error
DeleteStocks(tx *gorm.DB, recordingID uint) error
DeleteStocksByIDs(tx *gorm.DB, ids []uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
GetStockByID(tx *gorm.DB, stockID uint) (*entity.RecordingStock, error)
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error
UpdateDepletionQuantities(tx *gorm.DB, depletionID uint, qty, usageQty, pendingQty float64) error
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
DeleteDepletions(tx *gorm.DB, recordingID uint) error
ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error)
GetDepletionByID(tx *gorm.DB, depletionID uint) (*entity.RecordingDepletion, error)
CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error
DeleteEggs(tx *gorm.DB, recordingID uint) error
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
UpdateEggTotalQty(tx *gorm.DB, eggID uint, totalQty float64) error
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
@@ -272,6 +279,18 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda
return nextRecordingDay(days), nil
}
func (r *RecordingRepositoryImpl) CreateRecording(tx *gorm.DB, recording *entity.Recording) error {
if recording == nil {
return nil
}
return tx.Select(
"ProjectFlockKandangId",
"RecordDatetime",
"Day",
"CreatedBy",
).Create(recording).Error
}
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 {
return nil
@@ -279,10 +298,24 @@ func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.Reco
return tx.Create(&stocks).Error
}
func (r *RecordingRepositoryImpl) CreateStock(tx *gorm.DB, stock *entity.RecordingStock) error {
if stock == nil {
return nil
}
return tx.Create(stock).Error
}
func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) error {
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
}
func (r *RecordingRepositoryImpl) DeleteStocksByIDs(tx *gorm.DB, ids []uint) error {
if len(ids) == 0 {
return nil
}
return tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error
}
func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) {
var items []entity.RecordingStock
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
@@ -291,6 +324,18 @@ func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]e
return items, nil
}
func (r *RecordingRepositoryImpl) GetStockByID(tx *gorm.DB, stockID uint) (*entity.RecordingStock, error) {
if stockID == 0 {
return nil, gorm.ErrRecordNotFound
}
var stock entity.RecordingStock
if err := tx.Where("id = ?", stockID).Take(&stock).Error; err != nil {
return nil, err
}
return &stock, nil
}
func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error {
return tx.Model(&entity.RecordingStock{}).
Where("id = ?", stockID).
@@ -306,6 +351,16 @@ func (r *RecordingRepositoryImpl) UpdateDepletionPending(tx *gorm.DB, depletionI
Update("pending_qty", pendingQty).Error
}
func (r *RecordingRepositoryImpl) UpdateDepletionQuantities(tx *gorm.DB, depletionID uint, qty, usageQty, pendingQty float64) error {
return tx.Model(&entity.RecordingDepletion{}).
Where("id = ?", depletionID).
Updates(map[string]any{
"qty": qty,
"usage_qty": usageQty,
"pending_qty": pendingQty,
}).Error
}
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 {
return nil
@@ -325,6 +380,18 @@ func (r *RecordingRepositoryImpl) ListDepletions(tx *gorm.DB, recordingID uint)
return items, nil
}
func (r *RecordingRepositoryImpl) GetDepletionByID(tx *gorm.DB, depletionID uint) (*entity.RecordingDepletion, error) {
if depletionID == 0 {
return nil, gorm.ErrRecordNotFound
}
var depletion entity.RecordingDepletion
if err := tx.Where("id = ?", depletionID).Take(&depletion).Error; err != nil {
return nil, err
}
return &depletion, nil
}
func (r *RecordingRepositoryImpl) CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error {
if len(eggs) == 0 {
return nil
@@ -344,6 +411,12 @@ func (r *RecordingRepositoryImpl) ListEggs(tx *gorm.DB, recordingID uint) ([]ent
return items, nil
}
func (r *RecordingRepositoryImpl) UpdateEggTotalQty(tx *gorm.DB, eggID uint, totalQty float64) error {
return tx.Model(&entity.RecordingEgg{}).
Where("id = ?", eggID).
Update("total_qty", totalQty).Error
}
func (r *RecordingRepositoryImpl) GetRecordingEggByID(
ctx context.Context,
id uint,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -233,7 +233,7 @@ var adjustmentSubtypesByType = map[AdjustmentTransactionType][]string{
}
var hiddenAdjustmentSubtypesForFrontend = map[string]struct{}{
string(AdjustmentTransactionSubtypeRecordingDepletionIn): {},
string(AdjustmentTransactionSubtypeRecordingDepletionOut): {},
}
var adjustmentSubtypeToType = func() map[string]AdjustmentTransactionType {