mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat(BE): fixing fifo system recording
This commit is contained in:
@@ -192,6 +192,16 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
if req.Quantity < 0 {
|
if req.Quantity < 0 {
|
||||||
return nil, errors.New("quantity must be zero or greater")
|
return nil, errors.New("quantity must be zero or greater")
|
||||||
}
|
}
|
||||||
|
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"usable_key": req.UsableKey.String(),
|
||||||
|
"usable_id": req.UsableID,
|
||||||
|
"requested_quantity": req.Quantity,
|
||||||
|
"allow_pending": req.AllowPending,
|
||||||
|
"product_warehouse_id": req.ProductWarehouseID,
|
||||||
|
}).Debug("fifo consume request")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
cfg, ok := fifo.Usable(req.UsableKey)
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -220,6 +230,19 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
currentPending := ctxRow.PendingQty
|
currentPending := ctxRow.PendingQty
|
||||||
currentTotal := currentUsage + currentPending
|
currentTotal := currentUsage + currentPending
|
||||||
delta := req.Quantity - currentTotal
|
delta := req.Quantity - currentTotal
|
||||||
|
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"usable_key": req.UsableKey.String(),
|
||||||
|
"usable_id": req.UsableID,
|
||||||
|
"product_warehouse_id": productWarehouseID,
|
||||||
|
"current_usage_qty": currentUsage,
|
||||||
|
"current_pending_qty": currentPending,
|
||||||
|
"current_total_qty": currentTotal,
|
||||||
|
"requested_quantity": req.Quantity,
|
||||||
|
"calculated_delta": delta,
|
||||||
|
"input_warehouse_match": req.ProductWarehouseID == 0 || req.ProductWarehouseID == productWarehouseID,
|
||||||
|
}).Debug("fifo consume context")
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
usageDelta float64
|
usageDelta float64
|
||||||
@@ -285,6 +308,20 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
result.ReleasedQuantity = releasedAmount
|
result.ReleasedQuantity = releasedAmount
|
||||||
result.UsageQuantity = currentUsage + usageDelta
|
result.UsageQuantity = currentUsage + usageDelta
|
||||||
result.PendingQuantity = currentPending + pendingDelta
|
result.PendingQuantity = currentPending + pendingDelta
|
||||||
|
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"usable_key": req.UsableKey.String(),
|
||||||
|
"usable_id": req.UsableID,
|
||||||
|
"product_warehouse_id": productWarehouseID,
|
||||||
|
"usage_delta": usageDelta,
|
||||||
|
"pending_delta": pendingDelta,
|
||||||
|
"released_quantity": releasedAmount,
|
||||||
|
"added_allocations": len(addedAlloc),
|
||||||
|
"final_usage_qty": result.UsageQuantity,
|
||||||
|
"final_pending_qty": result.PendingQuantity,
|
||||||
|
"final_requested_qty": result.RequestedQuantity,
|
||||||
|
}).Debug("fifo consume result")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -299,6 +336,13 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
|||||||
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||||
return errors.New("usable key and id are required")
|
return errors.New("usable key and id are required")
|
||||||
}
|
}
|
||||||
|
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"usable_key": req.UsableKey.String(),
|
||||||
|
"usable_id": req.UsableID,
|
||||||
|
"reason": req.Reason,
|
||||||
|
}).Debug("fifo release request")
|
||||||
|
}
|
||||||
|
|
||||||
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
cfg, ok := fifo.Usable(req.UsableKey)
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
@@ -310,6 +354,16 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"usable_key": req.UsableKey.String(),
|
||||||
|
"usable_id": req.UsableID,
|
||||||
|
"product_warehouse_id": ctxRow.ProductWarehouseID,
|
||||||
|
"current_usage_qty": ctxRow.UsageQty,
|
||||||
|
"current_pending_qty": ctxRow.PendingQty,
|
||||||
|
"current_total_qty": ctxRow.UsageQty + ctxRow.PendingQty,
|
||||||
|
}).Debug("fifo release context")
|
||||||
|
}
|
||||||
|
|
||||||
var usageDelta, pendingDelta float64
|
var usageDelta, pendingDelta float64
|
||||||
if ctxRow.UsageQty > 0 {
|
if ctxRow.UsageQty > 0 {
|
||||||
@@ -326,6 +380,15 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"usable_key": req.UsableKey.String(),
|
||||||
|
"usable_id": req.UsableID,
|
||||||
|
"usage_delta": usageDelta,
|
||||||
|
"pending_delta": pendingDelta,
|
||||||
|
}).Debug("fifo release applied")
|
||||||
|
}
|
||||||
|
|
||||||
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
|
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
|
||||||
return s.txOrDB(tx, db)
|
return s.txOrDB(tx, db)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -194,13 +194,17 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
StockLogId: newLog.Id,
|
StockLogId: newLog.Id,
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
}
|
}
|
||||||
|
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
||||||
|
}
|
||||||
|
|
||||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
// Adjustment INCREASE → Replenish stock (Stockable)
|
||||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||||
replenishResult, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
||||||
StockableKey: "ADJUSTMENT_IN",
|
StockableKey: "ADJUSTMENT_IN",
|
||||||
StockableID: newLog.Id,
|
StockableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
Note: ¬e,
|
Note: ¬e,
|
||||||
@@ -210,15 +214,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stockable tracking fields
|
|
||||||
adjustmentStock.TotalQty = replenishResult.AddedQuantity
|
|
||||||
adjustmentStock.TotalUsed = 0
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Adjustment DECREASE → Consume stock (Usable)
|
// Adjustment DECREASE → Consume stock (Usable)
|
||||||
consumeResult, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||||
UsableKey: "ADJUSTMENT_OUT",
|
UsableKey: "ADJUSTMENT_OUT",
|
||||||
UsableID: newLog.Id,
|
UsableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
AllowPending: false, // Don't allow pending for adjustment
|
AllowPending: false, // Don't allow pending for adjustment
|
||||||
@@ -227,16 +227,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update usable tracking fields
|
|
||||||
adjustmentStock.UsageQty = consumeResult.UsageQuantity
|
|
||||||
adjustmentStock.PendingQty = consumeResult.PendingQuantity
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save AdjustmentStock record
|
|
||||||
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
|
||||||
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||||
|
|||||||
@@ -295,16 +295,17 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
|
|||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
var rows []struct {
|
var rows []struct {
|
||||||
UsageQty float64
|
TotalQty float64
|
||||||
UomName string
|
UomName string
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.
|
if err := tx.
|
||||||
Table("recording_stocks").
|
Table("recording_stocks").
|
||||||
Select("COALESCE(recording_stocks.usage_qty, 0) AS usage_qty, LOWER(uoms.name) AS uom_name").
|
Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
|
||||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||||
Where("recording_stocks.recording_id = ?", recordingID).
|
Where("recording_stocks.recording_id = ?", recordingID).
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -312,16 +313,16 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
|
|||||||
|
|
||||||
var total float64
|
var total float64
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
if row.UsageQty <= 0 {
|
if row.TotalQty <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(row.UomName) {
|
switch strings.TrimSpace(row.UomName) {
|
||||||
case "kilogram", "kg", "kilograms", "kilo":
|
case "kilogram", "kg", "kilograms", "kilo":
|
||||||
total += row.UsageQty * 1000
|
total += row.TotalQty * 1000
|
||||||
case "gram", "g", "grams":
|
case "gram", "g", "grams":
|
||||||
total += row.UsageQty
|
total += row.TotalQty
|
||||||
default:
|
default:
|
||||||
total += row.UsageQty
|
total += row.TotalQty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return total, nil
|
return total, nil
|
||||||
|
|||||||
@@ -247,11 +247,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
||||||
|
stockDesired := resetStockQuantitiesForFIFO(mappedStocks, s.FifoSvc != nil)
|
||||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
||||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil)
|
||||||
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -324,6 +326,49 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
hasDepletionChanges := req.Depletions != nil
|
hasDepletionChanges := req.Depletions != nil
|
||||||
hasEggChanges := req.Eggs != nil
|
hasEggChanges := req.Eggs != nil
|
||||||
|
|
||||||
|
var existingStocks []entity.RecordingStock
|
||||||
|
if hasStockChanges {
|
||||||
|
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.Log.WithFields(logrus.Fields{
|
||||||
|
"recording_id": recordingEntity.Id,
|
||||||
|
"existing": summarizeExistingStocks(existingStocks),
|
||||||
|
"incoming": summarizeIncomingStocks(req.Stocks),
|
||||||
|
}).Debug("recording update stock comparison")
|
||||||
|
}
|
||||||
|
if stocksMatch(existingStocks, req.Stocks) {
|
||||||
|
hasStockChanges = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDepletions []entity.RecordingDepletion
|
||||||
|
if hasDepletionChanges {
|
||||||
|
existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if depletionsMatch(existingDepletions, req.Depletions) {
|
||||||
|
hasDepletionChanges = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingEggs []entity.RecordingEgg
|
||||||
|
if hasEggChanges {
|
||||||
|
existingEggs, err = s.Repository.ListEggs(tx, recordingEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if eggsMatch(existingEggs, req.Eggs) {
|
||||||
|
hasEggChanges = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -355,39 +400,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges {
|
if hasStockChanges {
|
||||||
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks); err != nil {
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.releaseRecordingStocks(ctx, tx, existingStocks); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil {
|
|
||||||
s.Log.Errorf("Failed to clear stocks: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedStocks := recordingutil.MapStocks(recordingEntity.Id, req.Stocks)
|
|
||||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
|
||||||
s.Log.Errorf("Failed to update stocks: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasDepletionChanges {
|
if hasDepletionChanges {
|
||||||
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -406,12 +424,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasEggChanges {
|
if hasEggChanges {
|
||||||
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear eggs: %+v", err)
|
s.Log.Errorf("Failed to clear eggs: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -429,7 +441,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges || hasDepletionChanges {
|
if hasStockChanges || hasDepletionChanges || hasEggChanges {
|
||||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -680,12 +692,27 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
if stock.UsageQty != nil {
|
if stock.UsageQty != nil {
|
||||||
desired = *stock.UsageQty
|
desired = *stock.UsageQty
|
||||||
}
|
}
|
||||||
|
var pending float64
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
desiredTotal := desired + pending
|
||||||
|
|
||||||
|
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.Log.WithFields(logrus.Fields{
|
||||||
|
"recording_stock_id": stock.Id,
|
||||||
|
"product_warehouse_id": stock.ProductWarehouseId,
|
||||||
|
"desired_usage_qty": desired,
|
||||||
|
"desired_pending_qty": pending,
|
||||||
|
"desired_total_qty": desiredTotal,
|
||||||
|
}).Debug("recording fifo consume start")
|
||||||
|
}
|
||||||
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||||
UsableKey: recordingStockUsableKey,
|
UsableKey: recordingStockUsableKey,
|
||||||
UsableID: stock.Id,
|
UsableID: stock.Id,
|
||||||
ProductWarehouseID: stock.ProductWarehouseId,
|
ProductWarehouseID: stock.ProductWarehouseId,
|
||||||
Quantity: desired,
|
Quantity: desiredTotal,
|
||||||
AllowPending: true,
|
AllowPending: true,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
@@ -694,6 +721,17 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.Log.WithFields(logrus.Fields{
|
||||||
|
"recording_stock_id": stock.Id,
|
||||||
|
"product_warehouse_id": stock.ProductWarehouseId,
|
||||||
|
"result_usage_qty": result.UsageQuantity,
|
||||||
|
"result_pending_qty": result.PendingQuantity,
|
||||||
|
"released_qty": result.ReleasedQuantity,
|
||||||
|
"added_allocations": len(result.AddedAllocations),
|
||||||
|
}).Debug("recording fifo consume result")
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -716,6 +754,23 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var usage float64
|
||||||
|
var pending float64
|
||||||
|
if stock.UsageQty != nil {
|
||||||
|
usage = *stock.UsageQty
|
||||||
|
}
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
|
||||||
|
s.Log.WithFields(logrus.Fields{
|
||||||
|
"recording_stock_id": stock.Id,
|
||||||
|
"product_warehouse_id": stock.ProductWarehouseId,
|
||||||
|
"current_usage_qty": usage,
|
||||||
|
"current_pending_qty": pending,
|
||||||
|
}).Debug("recording fifo release start")
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||||
UsableKey: recordingStockUsableKey,
|
UsableKey: recordingStockUsableKey,
|
||||||
UsableID: stock.Id,
|
UsableID: stock.Id,
|
||||||
@@ -771,6 +826,288 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context,
|
|||||||
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type desiredStock struct {
|
||||||
|
Usage float64
|
||||||
|
Pending float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock {
|
||||||
|
desired := make([]desiredStock, len(stocks))
|
||||||
|
for i := range stocks {
|
||||||
|
if stocks[i].UsageQty != nil {
|
||||||
|
desired[i].Usage = *stocks[i].UsageQty
|
||||||
|
}
|
||||||
|
if stocks[i].PendingQty != nil {
|
||||||
|
desired[i].Pending = *stocks[i].PendingQty
|
||||||
|
}
|
||||||
|
if !enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
zero := 0.0
|
||||||
|
stocks[i].UsageQty = &zero
|
||||||
|
stocks[i].PendingQty = &zero
|
||||||
|
}
|
||||||
|
return desired
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyStockDesiredQuantities(stocks []entity.RecordingStock, desired []desiredStock, enabled bool) {
|
||||||
|
if !enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range stocks {
|
||||||
|
if i >= len(desired) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
usage := desired[i].Usage
|
||||||
|
pending := desired[i].Pending
|
||||||
|
stocks[i].UsageQty = &usage
|
||||||
|
stocks[i].PendingQty = &pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) syncRecordingStocks(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
recordingID uint,
|
||||||
|
existing []entity.RecordingStock,
|
||||||
|
incoming []validation.Stock,
|
||||||
|
) error {
|
||||||
|
if s.FifoSvc == nil {
|
||||||
|
if err := s.Repository.DeleteStocks(tx, recordingID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mapped := recordingutil.MapStocks(recordingID, incoming)
|
||||||
|
return s.Repository.CreateStocks(tx, mapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingByWarehouse := make(map[uint][]entity.RecordingStock)
|
||||||
|
for _, stock := range existing {
|
||||||
|
existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock)
|
||||||
|
}
|
||||||
|
|
||||||
|
stocksToConsume := make([]entity.RecordingStock, 0, len(incoming))
|
||||||
|
for _, item := range incoming {
|
||||||
|
list := existingByWarehouse[item.ProductWarehouseId]
|
||||||
|
var stock entity.RecordingStock
|
||||||
|
if len(list) > 0 {
|
||||||
|
stock = list[0]
|
||||||
|
existingByWarehouse[item.ProductWarehouseId] = list[1:]
|
||||||
|
} else {
|
||||||
|
zero := 0.0
|
||||||
|
stock = entity.RecordingStock{
|
||||||
|
RecordingId: recordingID,
|
||||||
|
ProductWarehouseId: item.ProductWarehouseId,
|
||||||
|
UsageQty: &zero,
|
||||||
|
PendingQty: &zero,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&stock).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
desired := item.Qty
|
||||||
|
stock.UsageQty = &desired
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
pending := *item.PendingQty
|
||||||
|
stock.PendingQty = &pending
|
||||||
|
}
|
||||||
|
stocksToConsume = append(stocksToConsume, stock)
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftovers []entity.RecordingStock
|
||||||
|
for _, list := range existingByWarehouse {
|
||||||
|
leftovers = append(leftovers, list...)
|
||||||
|
}
|
||||||
|
if len(leftovers) > 0 {
|
||||||
|
if err := s.releaseRecordingStocks(ctx, tx, leftovers); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ids := make([]uint, 0, len(leftovers))
|
||||||
|
for _, stock := range leftovers {
|
||||||
|
if stock.Id != 0 {
|
||||||
|
ids = append(ids, stock.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
if err := tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stocksToConsume) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.consumeRecordingStocks(ctx, tx, stocksToConsume)
|
||||||
|
}
|
||||||
|
|
||||||
|
type eggTotals struct {
|
||||||
|
Qty int
|
||||||
|
Weight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type stockTotals struct {
|
||||||
|
Usage float64
|
||||||
|
Pending float64
|
||||||
|
Total float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals {
|
||||||
|
totals := make(map[uint]stockTotals)
|
||||||
|
for _, stock := range stocks {
|
||||||
|
var usage float64
|
||||||
|
var pending float64
|
||||||
|
if stock.UsageQty != nil {
|
||||||
|
usage = *stock.UsageQty
|
||||||
|
}
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
current := totals[stock.ProductWarehouseId]
|
||||||
|
current.Usage += usage
|
||||||
|
current.Pending += pending
|
||||||
|
current.Total += usage + pending
|
||||||
|
totals[stock.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
return totals
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals {
|
||||||
|
totals := make(map[uint]stockTotals)
|
||||||
|
for _, stock := range stocks {
|
||||||
|
var pending float64
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
current := totals[stock.ProductWarehouseId]
|
||||||
|
current.Usage += stock.Qty
|
||||||
|
current.Pending += pending
|
||||||
|
current.Total += stock.Qty + pending
|
||||||
|
totals[stock.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
return totals
|
||||||
|
}
|
||||||
|
|
||||||
|
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
|
||||||
|
hasPending := false
|
||||||
|
for _, item := range incoming {
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
hasPending = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUsage := make(map[uint]float64)
|
||||||
|
existingTotal := make(map[uint]float64)
|
||||||
|
for _, stock := range existing {
|
||||||
|
var usage float64
|
||||||
|
var pending float64
|
||||||
|
if stock.UsageQty != nil {
|
||||||
|
usage = *stock.UsageQty
|
||||||
|
}
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
existingUsage[stock.ProductWarehouseId] += usage
|
||||||
|
existingTotal[stock.ProductWarehouseId] += usage + pending
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingUsage := make(map[uint]float64)
|
||||||
|
incomingTotal := make(map[uint]float64)
|
||||||
|
for _, item := range incoming {
|
||||||
|
var pending float64
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
pending = *item.PendingQty
|
||||||
|
}
|
||||||
|
incomingUsage[item.ProductWarehouseId] += item.Qty
|
||||||
|
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPending {
|
||||||
|
return floatMapsMatch(existingTotal, incomingTotal)
|
||||||
|
}
|
||||||
|
return floatMapsMatch(existingUsage, incomingUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func depletionsMatch(existing []entity.RecordingDepletion, incoming []validation.Depletion) bool {
|
||||||
|
existingTotals := make(map[uint]float64)
|
||||||
|
for _, dep := range existing {
|
||||||
|
existingTotals[dep.ProductWarehouseId] += dep.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingTotals := make(map[uint]float64)
|
||||||
|
for _, dep := range incoming {
|
||||||
|
incomingTotals[dep.ProductWarehouseId] += dep.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
return floatMapsMatch(existingTotals, incomingTotals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
|
||||||
|
existingTotals := make(map[uint]eggTotals)
|
||||||
|
for _, egg := range existing {
|
||||||
|
weight := 0.0
|
||||||
|
if egg.Weight != nil {
|
||||||
|
weight = *egg.Weight
|
||||||
|
}
|
||||||
|
current := existingTotals[egg.ProductWarehouseId]
|
||||||
|
current.Qty += egg.Qty
|
||||||
|
current.Weight += float64(egg.Qty) * weight
|
||||||
|
existingTotals[egg.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingTotals := make(map[uint]eggTotals)
|
||||||
|
for _, egg := range incoming {
|
||||||
|
weight := 0.0
|
||||||
|
if egg.Weight != nil {
|
||||||
|
weight = *egg.Weight
|
||||||
|
}
|
||||||
|
current := incomingTotals[egg.ProductWarehouseId]
|
||||||
|
current.Qty += egg.Qty
|
||||||
|
current.Weight += float64(egg.Qty) * weight
|
||||||
|
incomingTotals[egg.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(existingTotals) != len(incomingTotals) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, existingTotal := range existingTotals {
|
||||||
|
incomingTotal, ok := incomingTotals[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if existingTotal.Qty != incomingTotal.Qty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !floatNearlyEqual(existingTotal.Weight, incomingTotal.Weight) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatMapsMatch(a, b map[uint]float64) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for key, value := range a {
|
||||||
|
other, ok := b[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !floatNearlyEqual(value, other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatNearlyEqual(a, b float64) bool {
|
||||||
|
return math.Abs(a-b) <= 0.000001
|
||||||
|
}
|
||||||
|
|
||||||
func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
|
func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
|
||||||
day := 0
|
day := 0
|
||||||
if recording.Day != nil {
|
if recording.Day != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user