mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh
This commit is contained in:
@@ -407,7 +407,7 @@ SELECT
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.quantity AS quantity,
|
||||
std.usage_qty AS quantity,
|
||||
u.name AS unit,
|
||||
'Stock Refill' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -456,7 +456,7 @@ SELECT
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
COALESCE(tw.name, '') AS destination,
|
||||
std.quantity AS quantity,
|
||||
std.usage_qty AS quantity,
|
||||
u.name AS unit,
|
||||
'Transfer to other unit' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -927,34 +927,34 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_qty,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_price,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS qty_divisor,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) / NULLIF(COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0), 0) AS average_price`,
|
||||
|
||||
@@ -194,13 +194,17 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
StockLogId: newLog.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) {
|
||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
||||
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",
|
||||
StockableID: newLog.Id,
|
||||
StockableID: adjustmentStock.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
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))
|
||||
}
|
||||
|
||||
// Update stockable tracking fields
|
||||
adjustmentStock.TotalQty = replenishResult.AddedQuantity
|
||||
adjustmentStock.TotalUsed = 0
|
||||
|
||||
} else {
|
||||
// Adjustment DECREASE → Consume stock (Usable)
|
||||
consumeResult, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||
UsableKey: "ADJUSTMENT_OUT",
|
||||
UsableID: newLog.Id,
|
||||
UsableID: adjustmentStock.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
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 {
|
||||
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)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
|
||||
@@ -866,6 +867,14 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
||||
}
|
||||
|
||||
if len(pfkIDs) > 0 {
|
||||
uniformityRepo := uniformityRepository.NewUniformityRepository(s.Repository.DB())
|
||||
if dbTransaction != nil {
|
||||
uniformityRepo = uniformityRepository.NewUniformityRepository(dbTransaction)
|
||||
}
|
||||
if err := uniformityRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
|
||||
}
|
||||
|
||||
pwRepo := s.ProductWarehouseRepo
|
||||
if dbTransaction != nil {
|
||||
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
|
||||
|
||||
@@ -295,16 +295,17 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var rows []struct {
|
||||
UsageQty float64
|
||||
TotalQty float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
if err := tx.
|
||||
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 products ON products.id = product_warehouses.product_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).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return 0, err
|
||||
@@ -312,16 +313,16 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
|
||||
|
||||
var total float64
|
||||
for _, row := range rows {
|
||||
if row.UsageQty <= 0 {
|
||||
if row.TotalQty <= 0 {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(row.UomName) {
|
||||
case "kilogram", "kg", "kilograms", "kilo":
|
||||
total += row.UsageQty * 1000
|
||||
total += row.TotalQty * 1000
|
||||
case "gram", "g", "grams":
|
||||
total += row.UsageQty
|
||||
total += row.TotalQty
|
||||
default:
|
||||
total += row.UsageQty
|
||||
total += row.TotalQty
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
|
||||
@@ -229,7 +229,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := s.Repository.CreateOne(ctx, &createdRecording, func(*gorm.DB) *gorm.DB { return tx }); err != nil {
|
||||
createTx := tx.WithContext(ctx).Select(
|
||||
"ProjectFlockKandangId",
|
||||
"RecordDatetime",
|
||||
"Day",
|
||||
"CreatedBy",
|
||||
)
|
||||
if err := createTx.Create(&createdRecording).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
@@ -241,11 +247,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
}
|
||||
|
||||
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
||||
stockDesired := resetStockQuantitiesForFIFO(mappedStocks, s.FifoSvc != nil)
|
||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil)
|
||||
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -299,9 +307,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
ctx := c.Context()
|
||||
|
||||
var recordingEntity *entity.Recording
|
||||
var updatedRecording *entity.Recording
|
||||
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
recording, err := s.Repository.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.Repository.WithRelations(tx)
|
||||
repoTx := s.Repository.WithTx(tx)
|
||||
recording, err := repoTx.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.Repository.WithRelations(db)
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -316,6 +326,42 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
hasDepletionChanges := req.Depletions != 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 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 {
|
||||
return nil
|
||||
}
|
||||
@@ -347,39 +393,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
|
||||
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 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 {
|
||||
if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||
return err
|
||||
@@ -398,12 +417,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
|
||||
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 {
|
||||
s.Log.Errorf("Failed to clear eggs: %+v", err)
|
||||
return err
|
||||
@@ -421,7 +434,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 {
|
||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||
return err
|
||||
@@ -470,13 +483,31 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := repoTx.GetByID(ctx, recordingEntity.Id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.Repository.WithRelations(db)
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to reload recording %d after update: %+v", recordingEntity.Id, err)
|
||||
return err
|
||||
}
|
||||
updatedRecording = updated
|
||||
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
return nil, transactionErr
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
if updatedRecording == nil {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
if err := s.attachLatestApproval(ctx, updatedRecording); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.attachProductionStandard(ctx, updatedRecording); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updatedRecording, nil
|
||||
}
|
||||
|
||||
func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
|
||||
@@ -654,12 +685,17 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
|
||||
if stock.UsageQty != nil {
|
||||
desired = *stock.UsageQty
|
||||
}
|
||||
var pending float64
|
||||
if stock.PendingQty != nil {
|
||||
pending = *stock.PendingQty
|
||||
}
|
||||
desiredTotal := desired + pending
|
||||
|
||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||
UsableKey: recordingStockUsableKey,
|
||||
UsableID: stock.Id,
|
||||
ProductWarehouseID: stock.ProductWarehouseId,
|
||||
Quantity: desired,
|
||||
Quantity: desiredTotal,
|
||||
AllowPending: true,
|
||||
Tx: tx,
|
||||
})
|
||||
@@ -745,6 +781,288 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context,
|
||||
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 {
|
||||
day := 0
|
||||
if recording.Day != nil {
|
||||
|
||||
@@ -93,6 +93,10 @@ func (u *UniformityController) GetOne(c *fiber.Ctx) error {
|
||||
Uniformity: result.Uniformity,
|
||||
Cv: result.Cv,
|
||||
}
|
||||
document, documentURL, err = u.UniformityService.GetDocumentInfo(c, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
standard, err := u.UniformityService.GetStandard(c, result)
|
||||
|
||||
@@ -74,7 +74,6 @@ type UniformityListDTO struct {
|
||||
MeanDown float64 `json:"mean_down"`
|
||||
StandardMeanWeight *float64 `json:"standard_mean_weight"`
|
||||
StandardUniformity *float64 `json:"standard_uniformity"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CreatedBy uint `json:"created_by"`
|
||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||
}
|
||||
@@ -154,7 +153,6 @@ func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []Unifor
|
||||
UniformQty: item.UniformQty,
|
||||
MeanUp: item.MeanUp,
|
||||
MeanDown: item.MeanDown,
|
||||
CreatedAt: item.CreatedAt,
|
||||
CreatedBy: item.CreatedBy,
|
||||
LatestApproval: latestApproval,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
@@ -8,6 +10,7 @@ import (
|
||||
|
||||
type UniformityRepository interface {
|
||||
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
|
||||
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
|
||||
}
|
||||
|
||||
type UniformityRepositoryImpl struct {
|
||||
@@ -19,3 +22,13 @@ func NewUniformityRepository(db *gorm.DB) UniformityRepository {
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockKandangUniformity](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UniformityRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.DB().WithContext(ctx).
|
||||
Unscoped().
|
||||
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Delete(&entity.ProjectFlockKandangUniformity{}).Error
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ type UniformityService interface {
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
|
||||
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
|
||||
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
|
||||
GetDocumentInfo(ctx *fiber.Ctx, uniformityID uint) (*entity.Document, string, error)
|
||||
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error)
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent
|
||||
if params.Week != 0 {
|
||||
db = db.Where("week = ?", params.Week)
|
||||
}
|
||||
return db.Order("uniform_date DESC").Order("created_at DESC")
|
||||
return db.Order("uniform_date DESC").Order("id DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -592,28 +593,19 @@ func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (Uniform
|
||||
return computeUniformity(rows)
|
||||
}
|
||||
|
||||
func (s uniformityService) GetDocumentInfo(c *fiber.Ctx, uniformityID uint) (*entity.Document, string, error) {
|
||||
return s.fetchUniformityDocument(c.Context(), uniformityID, true)
|
||||
}
|
||||
|
||||
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error) {
|
||||
if s.DocumentSvc == nil {
|
||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
||||
}
|
||||
|
||||
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID))
|
||||
document, url, err := s.fetchUniformityDocument(c.Context(), uniformityID, false)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
}
|
||||
if len(documents) == 0 {
|
||||
if document == nil || url == "" {
|
||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||
}
|
||||
|
||||
document := documents[0]
|
||||
url, err := s.DocumentSvc.PresignURL(c.Context(), document, 15*time.Minute)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
}
|
||||
if url == "" {
|
||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
@@ -638,7 +630,35 @@ func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniform
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
}
|
||||
|
||||
return calculation, &document, url, nil
|
||||
return calculation, document, url, nil
|
||||
}
|
||||
|
||||
func (s uniformityService) fetchUniformityDocument(ctx context.Context, uniformityID uint, allowMissing bool) (*entity.Document, string, error) {
|
||||
if s.DocumentSvc == nil {
|
||||
return nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
||||
}
|
||||
|
||||
documents, err := s.DocumentSvc.ListByTarget(ctx, "UNIFORMITY", uint64(uniformityID))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if len(documents) == 0 {
|
||||
if allowMissing {
|
||||
return nil, "", nil
|
||||
}
|
||||
return nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||
}
|
||||
|
||||
document := documents[0]
|
||||
url, err := s.DocumentSvc.PresignURL(ctx, document, 15*time.Minute)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if url == "" {
|
||||
return nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
|
||||
}
|
||||
|
||||
return &document, url, nil
|
||||
}
|
||||
|
||||
func (s *uniformityService) createUniformityApproval(
|
||||
|
||||
@@ -180,7 +180,10 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
|
||||
req.Items = []validation.ReceivePurchaseItemRequest{singleItem}
|
||||
}
|
||||
}
|
||||
req.TravelDocuments = form.File["documents"]
|
||||
req.TravelDocuments = form.File["travel_documents"]
|
||||
if len(req.TravelDocuments) == 0 {
|
||||
req.TravelDocuments = form.File["documents"]
|
||||
}
|
||||
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -999,6 +999,22 @@ func (s *purchaseService) uploadTravelDocument(
|
||||
return "", errors.New("document service not available")
|
||||
}
|
||||
|
||||
documents, err := s.DocumentSvc.ListByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(itemID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(documents) > 0 {
|
||||
var ids []uint
|
||||
for _, doc := range documents {
|
||||
if doc.Type == string(utils.DocumentTypePurchaseTravel) {
|
||||
ids = append(ids, doc.Id)
|
||||
}
|
||||
}
|
||||
if err := s.DocumentSvc.DeleteDocuments(ctx, ids, true); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
documentFiles := []commonSvc.DocumentFile{{
|
||||
File: file,
|
||||
Type: string(utils.DocumentTypePurchaseTravel),
|
||||
@@ -1015,7 +1031,7 @@ func (s *purchaseService) uploadTravelDocument(
|
||||
if len(results) == 0 {
|
||||
return "", errors.New("upload result is empty")
|
||||
}
|
||||
return results[0].URL, nil
|
||||
return results[0].Document.Path, nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
|
||||
@@ -1499,10 +1515,56 @@ func (s *purchaseService) loadPurchase(
|
||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
s.applyTravelDocumentURLs(ctx, purchase)
|
||||
|
||||
return purchase, nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) applyTravelDocumentURLs(ctx context.Context, purchase *entity.Purchase) {
|
||||
if purchase == nil || s.DocumentSvc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range purchase.Items {
|
||||
item := &purchase.Items[i]
|
||||
documents, err := s.DocumentSvc.ListByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(item.Id))
|
||||
if err != nil {
|
||||
s.Log.Warnf("Unable to load travel documents for purchase item %d: %+v", item.Id, err)
|
||||
} else {
|
||||
var targetDoc *entity.Document
|
||||
for j := len(documents) - 1; j >= 0; j-- {
|
||||
if documents[j].Type == string(utils.DocumentTypePurchaseTravel) {
|
||||
targetDoc = &documents[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetDoc != nil {
|
||||
url, err := s.DocumentSvc.PresignURL(ctx, *targetDoc, 15*time.Minute)
|
||||
if err != nil {
|
||||
s.Log.Warnf("Unable to presign travel document for purchase item %d: %+v", item.Id, err)
|
||||
} else if url != "" {
|
||||
item.TravelNumberDocs = &url
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path := item.TravelNumberDocs
|
||||
if path == nil || strings.TrimSpace(*path) == "" {
|
||||
continue
|
||||
}
|
||||
url, err := commonSvc.ResolveDocumentURL(ctx, s.DocumentSvc, *path, 15*time.Minute)
|
||||
if err != nil {
|
||||
s.Log.Warnf("Unable to presign travel document for purchase item %d: %+v", item.Id, err)
|
||||
continue
|
||||
}
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
item.TravelNumberDocs = &url
|
||||
}
|
||||
}
|
||||
|
||||
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
|
||||
seen := make(map[uint]struct{})
|
||||
ids := make([]uint, 0)
|
||||
|
||||
@@ -171,6 +171,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
utils.Log.Warnf("token refresh response status %d", resp.StatusCode)
|
||||
if resp.StatusCode == fiber.StatusTooManyRequests {
|
||||
return fiber.NewError(fiber.StatusTooManyRequests, "Too many attempts, please slow down")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ func (r *UserRepositoryImpl) GetByIdUser(
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*entity.User, error) {
|
||||
return r.BaseRepositoryImpl.First(ctx, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("id_user = ?", idUser)
|
||||
return db.Where("id_user::bigint = ?::bigint", idUser)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func (r *UserRepositoryImpl) UpsertByIdUser(ctx context.Context, user *entity.Us
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) SoftDeleteByIdUser(ctx context.Context, idUser int64) error {
|
||||
query := r.DB().WithContext(ctx).Where("id_user = ?", idUser)
|
||||
query := r.DB().WithContext(ctx).Where("id_user::bigint = ?::bigint", idUser)
|
||||
result := query.Delete(&entity.User{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
|
||||
Reference in New Issue
Block a user