Merge branch 'dev/teguh' into 'development'

FIX/BE: fix closing keuangan kalkulasi penjualan, marketing fifo, transfer laying fifo stock alocation

See merge request mbugroup/lti-api!298
This commit is contained in:
Hafizh A. Y.
2026-02-03 02:34:33 +00:00
6 changed files with 206 additions and 161 deletions
@@ -41,6 +41,7 @@ type ProductionData struct {
TotalWeightProduced float64 TotalWeightProduced float64
TotalEggWeightKg float64 TotalEggWeightKg float64
TotalWeightSold float64 TotalWeightSold float64
TotalBirdSold float64
TotalSalesAmount float64 TotalSalesAmount float64
} }
@@ -283,6 +284,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo
continue continue
} }
data.TotalWeightSold += delivery.TotalWeight data.TotalWeightSold += delivery.TotalWeight
data.TotalBirdSold += delivery.UsageQty
data.TotalSalesAmount += delivery.TotalPrice data.TotalSalesAmount += delivery.TotalPrice
} }
@@ -383,46 +385,77 @@ func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *enti
func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection { func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection {
totalPopulationIn := production.TotalPopulationIn
totalWeightProduced := production.TotalWeightProduced totalWeightProduced := production.TotalWeightProduced
totalEggWeightKg := production.TotalEggWeightKg totalEggWeightKg := production.TotalEggWeightKg
totalSalesAmount := production.TotalSalesAmount totalSalesAmount := production.TotalSalesAmount
totalWeightSold := production.TotalWeightSold totalWeightSold := production.TotalWeightSold
totalBirdSold := production.TotalBirdSold
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
weightForSales := totalWeightSold isLaying := projectFlock.Category == string(utils.ProjectFlockCategoryLaying)
weightForCalculation := totalWeightProduced
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
weightForSales = totalWeightSold
weightForCalculation = totalEggWeightKg
}
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { // Fungsi untuk sales: LAYING = populasi aktual, GROWING = ekor terjual
if totalPopulationIn > 0 { calculateSalesMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
rpPerBird = amount / totalPopulationIn if isLaying {
} if actualPopulation > 0 {
if weightForSales > 0 { rpPerBird = amount / actualPopulation
rpPerKg = amount / weightForSales }
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
} else {
if totalBirdSold > 0 {
rpPerBird = amount / totalBirdSold
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
} }
return return
} }
actualPopulation := production.TotalPopulationIn - production.TotalDepletion // Fungsi untuk cost: per ekor = populasi aktual, per kg = LAYING telur produksi / GROWING ayam produksi
calculateCostMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 { if actualPopulation > 0 {
rpPerBird = amount / actualPopulation rpPerBird = amount / actualPopulation
} }
if weightForCalculation > 0 { if isLaying {
rpPerKg = amount / weightForCalculation if totalEggWeightKg > 0 {
rpPerKg = amount / totalEggWeightKg
}
} else {
if totalWeightProduced > 0 {
rpPerKg = amount / totalWeightProduced
}
}
return
}
// Fungsi untuk overhead/ekspedisi: LAYING = populasi aktual, GROWING = ekor terjual
calculateOverheadMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if isLaying {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
} else {
if totalBirdSold > 0 {
rpPerBird = amount / totalBirdSold
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
} }
return return
} }
plItems := []dto.ProfitLossItem{} plItems := []dto.ProfitLossItem{}
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount) salesRpPerBird, salesRpPerKg := calculateSalesMetrics(totalSalesAmount)
salesLabel := "Penjualan Ayam" salesLabel := "Penjualan Ayam"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { if isLaying {
salesLabel = "Penjualan Telur" salesLabel = "Penjualan Telur"
} }
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
@@ -435,23 +468,23 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
)) ))
totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost
_, sapronakRpPerKg := calculateMetrics(totalSapronakAmount)
sapronakRpPerBird := 0.0 sapronakRpPerBird := 0.0
sapronakRpPerKg := 0.0
for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} { for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} {
rpPerBird, _ := calculateMetrics(amount) rpPerBird, rpPerKg := calculateCostMetrics(amount)
sapronakRpPerBird += rpPerBird sapronakRpPerBird += rpPerBird
sapronakRpPerKg += rpPerKg
} }
sapronakLabel := "Pengeluaran Sapronak"
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSapronak), string(dto.PLCodeSapronak),
sapronakLabel, "Pengeluaran Sapronak",
"purchase", "purchase",
sapronakRpPerBird, sapronakRpPerBird,
sapronakRpPerKg, sapronakRpPerKg,
totalSapronakAmount, totalSapronakAmount,
)) ))
overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational) overheadRpPerBird, overheadRpPerKg := calculateOverheadMetrics(costs.RealizationOperational)
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeOverhead), string(dto.PLCodeOverhead),
"Overhead", "Overhead",
@@ -461,7 +494,7 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
costs.RealizationOperational, costs.RealizationOperational,
)) ))
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost) ekspedisiRpPerBird, ekspedisiRpPerKg := calculateOverheadMetrics(costs.ExpeditionCost)
plItems = append(plItems, dto.ToProfitLossItem( plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeEkspedisi), string(dto.PLCodeEkspedisi),
"Ekspedisi", "Ekspedisi",
@@ -160,7 +160,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
} }
afterQuantity := productWarehouse.Quantity
newLog := &entity.StockLog{ newLog := &entity.StockLog{
LoggableType: string(utils.StockLogTypeAdjustment), LoggableType: string(utils.StockLogTypeAdjustment),
LoggableId: 0, LoggableId: 0,
@@ -183,14 +182,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
if transactionType == string(utils.StockLogTransactionTypeIncrease) { if transactionType == string(utils.StockLogTransactionTypeIncrease) {
afterQuantity += req.Quantity
newLog.Increase = req.Quantity newLog.Increase = req.Quantity
newLog.Stock += newLog.Increase newLog.Stock += newLog.Increase
} else { } else {
if productWarehouse.Quantity < req.Quantity { if productWarehouse.Quantity < req.Quantity {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity))
} }
afterQuantity -= req.Quantity
newLog.Decrease = req.Quantity newLog.Decrease = req.Quantity
newLog.Stock -= newLog.Decrease newLog.Stock -= newLog.Decrease
} }
@@ -243,12 +240,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
} }
} }
productWarehouse.Quantity = afterQuantity
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return err
}
createdAdjustmentStockId = adjustmentStock.Id createdAdjustmentStockId = adjustmentStock.Id
return nil return nil
}) })
+1 -1
View File
@@ -64,7 +64,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate) salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate) deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -10,7 +10,6 @@ import (
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware" m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
@@ -502,78 +501,33 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
Tx: tx, Tx: tx,
}) })
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
totalConsumed := 0.0 if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil {
var fifoConsumed float64
var directConsumed float64
if result != nil && result.UsageQuantity > 0 {
fifoConsumed = result.UsageQuantity
totalConsumed = result.UsageQuantity
}
if err != nil || (totalConsumed < requestedQty) {
remainder := requestedQty - totalConsumed
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
if err2 != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock")
}
if pw == nil || pw.Quantity < remainder {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. FIFO: %.2f, Direct Available: %.2f, Total Needed: %.2f", func() float64 {
if pw != nil {
return pw.Quantity
} else {
return 0
}
}(), remainder, requestedQty))
}
if err := pwRepo.AdjustQuantities(ctx, map[uint]float64{
marketingProduct.ProductWarehouseId: -remainder,
}, func(db *gorm.DB) *gorm.DB {
return tx
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to adjust product warehouse quantity")
}
directConsumed = remainder
totalConsumed += remainder
}
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, totalConsumed, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
} }
if actorID > 0 && totalConsumed > 0 { if actorID > 0 && result.UsageQuantity > 0 {
notes := ""
if fifoConsumed > 0 && directConsumed > 0 {
notes = fmt.Sprintf("Partial FIFO (%.2f) + Direct (%.2f)", fifoConsumed, directConsumed)
} else if fifoConsumed > 0 {
notes = fmt.Sprintf("FIFO stock only (%.2f)", fifoConsumed)
} else if directConsumed > 0 {
notes = fmt.Sprintf("Direct stock only (%.2f)", directConsumed)
}
decreaseLog := &entity.StockLog{ decreaseLog := &entity.StockLog{
Decrease: totalConsumed, Decrease: result.UsageQuantity,
LoggableType: string(utils.StockLogTypeMarketing), LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id, LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId, ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
Notes: notes, Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
} }
if len(stockLogs) > 0 { if len(stockLogs) > 0 {
latestStockLog := stockLogs[0] latestStockLog := stockLogs[0]
decreaseLog.Stock = latestStockLog.Stock decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease
decreaseLog.Stock -= decreaseLog.Decrease
} else { } else {
decreaseLog.Stock -= decreaseLog.Decrease decreaseLog.Stock -= decreaseLog.Decrease
} }
@@ -610,6 +564,10 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
return err return err
} }
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
return err
}
if actorID > 0 && currentUsage > 0 { if actorID > 0 && currentUsage > 0 {
increaseLog := &entity.StockLog{ increaseLog := &entity.StockLog{
Increase: currentUsage, Increase: currentUsage,
@@ -617,7 +575,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
LoggableId: deliveryProduct.Id, LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId, ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
Notes: "", Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage),
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
if err != nil { if err != nil {
@@ -625,8 +583,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
} }
if len(stockLogs) > 0 { if len(stockLogs) > 0 {
latestStockLog := stockLogs[0] latestStockLog := stockLogs[0]
increaseLog.Stock = latestStockLog.Stock increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase
increaseLog.Stock += increaseLog.Increase
} else { } else {
increaseLog.Stock += increaseLog.Increase increaseLog.Stock += increaseLog.Increase
} }
@@ -634,9 +591,5 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil) s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil)
} }
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
return err
}
return nil return nil
} }
@@ -19,6 +19,7 @@ import (
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -41,11 +42,12 @@ type salesOrdersService struct {
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
UserRepo userRepo.UserRepository UserRepo userRepo.UserRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
WarehouseRepo warehouseRepo.WarehouseRepository WarehouseRepo warehouseRepo.WarehouseRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
} }
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository, func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService { projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{ return &salesOrdersService{
Log: utils.Log, Log: utils.Log,
@@ -55,6 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
UserRepo: userRepo, UserRepo: userRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
} }
@@ -230,14 +233,14 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
if len(req.MarketingProducts) > 0 { if len(req.MarketingProducts) > 0 {
for _, item := range req.MarketingProducts { for _, item := range req.MarketingProducts {
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err return nil, err
} }
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
} }
} }
@@ -333,6 +336,32 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
totalPrice = totalWeight * rp.UnitPrice totalPrice = totalWeight * rp.UnitPrice
} }
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
}
if err == nil && deliveryProduct.Id != 0 {
oldQty := old.Qty
newQty := rp.Qty
qtyDiff := newQty - oldQty
if qtyDiff < 0 {
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
} else if qtyDiff > 0 {
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: rp.ProductWarehouseId,
Quantity: qtyDiff,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err))
}
}
}
updateBody := map[string]any{ updateBody := map[string]any{
"product_warehouse_id": rp.ProductWarehouseId, "product_warehouse_id": rp.ProductWarehouseId,
"qty": rp.Qty, "qty": rp.Qty,
@@ -345,25 +374,20 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
} }
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { if deliveryProduct.Id == 0 {
if errors.Is(err, gorm.ErrRecordNotFound) { mdp := &entity.MarketingDeliveryProduct{
MarketingProductId: old.Id,
mdp := &entity.MarketingDeliveryProduct{ UnitPrice: 0,
MarketingProductId: old.Id, TotalWeight: 0,
UnitPrice: 0, AvgWeight: 0,
TotalWeight: 0, TotalPrice: 0,
AvgWeight: 0, DeliveryDate: nil,
TotalPrice: 0, VehicleNumber: rp.VehicleNumber,
DeliveryDate: nil, UsageQty: 0,
VehicleNumber: rp.VehicleNumber, PendingQty: 0,
UsageQty: 0, }
PendingQty: 0, if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
} return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
}
} else {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
} }
} }
} else { } else {
@@ -380,10 +404,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
} }
if err == nil { if err == nil && deliveryProduct.Id != 0 {
if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 { if deliveryProduct.DeliveryDate != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id))
}
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err))
} }
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
@@ -459,6 +491,19 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
if len(marketing.Products) > 0 { if len(marketing.Products) > 0 {
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
if err == nil && len(deliveryProducts) > 0 {
for _, dp := range deliveryProducts {
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: dp.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err))
}
}
}
for _, product := range marketing.Products { for _, product := range marketing.Products {
if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("marketing_product_id = ?", product.Id).Unscoped() return db.Where("marketing_product_id = ?", product.Id).Unscoped()
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"strings" "strings"
"time" "time"
@@ -743,7 +744,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction) repoTx := s.Repository.WithTx(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction) stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)
@@ -817,6 +818,27 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty") return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
} }
targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare)
for i, target := range targets {
roundedQty := math.Round(targetShares[i])
if roundedQty <= 0 {
continue
}
mappingAllocation := &entity.StockAllocation{
StockableType: fifo.UsableKeyTransferToLayingOut.String(),
StockableId: source.Id,
UsableType: fifo.StockableKeyTransferToLayingIn.String(),
UsableId: target.Id,
ProductWarehouseId: *source.ProductWarehouseId,
Qty: roundedQty,
Status: entity.StockAllocationStatusActive,
}
if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target")
}
}
stockLogDecrease := &entity.StockLog{ stockLogDecrease := &entity.StockLog{
ProductWarehouseId: *source.ProductWarehouseId, ProductWarehouseId: *source.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
@@ -937,36 +959,6 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi
return err return err
} }
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil && existing != nil {
if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"qty": gorm.Expr("qty + ?", quantity)}, nil); err != nil {
return nil, err
}
return existing, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
newWarehouse := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
ProjectFlockKandangId: projectFlockKandangId,
Quantity: quantity,
}
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
return nil, err
}
return newWarehouse, nil
}
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) { func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
@@ -1060,3 +1052,34 @@ func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFl
return kandangMaxTargetQty, nil return kandangMaxTargetQty, nil
} }
func distributeProportionalWithRounding(targets []entity.LayingTransferTarget, totalTargetQty, sourceShare float64) []float64 {
if len(targets) == 0 {
return []float64{}
}
targetShares := make([]float64, len(targets))
totalRounded := 0.0
for i, target := range targets {
targetShares[i] = (target.TotalQty / totalTargetQty) * sourceShare
totalRounded += math.Round(targetShares[i])
}
diff := sourceShare - totalRounded
if diff != 0 {
maxIdx := 0
maxDecimal := 0.0
for i, share := range targetShares {
decimal := share - math.Round(share)
if decimal > maxDecimal {
maxDecimal = decimal
maxIdx = i
}
}
targetShares[maxIdx] += diff
}
return targetShares
}