mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
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:
@@ -41,6 +41,7 @@ type ProductionData struct {
|
||||
TotalWeightProduced float64
|
||||
TotalEggWeightKg float64
|
||||
TotalWeightSold float64
|
||||
TotalBirdSold float64
|
||||
TotalSalesAmount float64
|
||||
}
|
||||
|
||||
@@ -283,6 +284,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo
|
||||
continue
|
||||
}
|
||||
data.TotalWeightSold += delivery.TotalWeight
|
||||
data.TotalBirdSold += delivery.UsageQty
|
||||
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 {
|
||||
|
||||
totalPopulationIn := production.TotalPopulationIn
|
||||
totalWeightProduced := production.TotalWeightProduced
|
||||
totalEggWeightKg := production.TotalEggWeightKg
|
||||
totalSalesAmount := production.TotalSalesAmount
|
||||
totalWeightSold := production.TotalWeightSold
|
||||
totalBirdSold := production.TotalBirdSold
|
||||
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
|
||||
|
||||
weightForSales := totalWeightSold
|
||||
weightForCalculation := totalWeightProduced
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
weightForSales = totalWeightSold
|
||||
weightForCalculation = totalEggWeightKg
|
||||
}
|
||||
isLaying := projectFlock.Category == string(utils.ProjectFlockCategoryLaying)
|
||||
|
||||
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||
if totalPopulationIn > 0 {
|
||||
rpPerBird = amount / totalPopulationIn
|
||||
}
|
||||
if weightForSales > 0 {
|
||||
rpPerKg = amount / weightForSales
|
||||
// Fungsi untuk sales: LAYING = populasi aktual, GROWING = ekor terjual
|
||||
calculateSalesMetrics := 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
|
||||
}
|
||||
|
||||
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
|
||||
|
||||
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||
// Fungsi untuk cost: per ekor = populasi aktual, per kg = LAYING telur produksi / GROWING ayam produksi
|
||||
calculateCostMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||
if actualPopulation > 0 {
|
||||
rpPerBird = amount / actualPopulation
|
||||
}
|
||||
if weightForCalculation > 0 {
|
||||
rpPerKg = amount / weightForCalculation
|
||||
if isLaying {
|
||||
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
|
||||
}
|
||||
|
||||
plItems := []dto.ProfitLossItem{}
|
||||
|
||||
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
|
||||
salesRpPerBird, salesRpPerKg := calculateSalesMetrics(totalSalesAmount)
|
||||
salesLabel := "Penjualan Ayam"
|
||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
if isLaying {
|
||||
salesLabel = "Penjualan Telur"
|
||||
}
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
@@ -435,23 +468,23 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
|
||||
))
|
||||
|
||||
totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost
|
||||
_, sapronakRpPerKg := calculateMetrics(totalSapronakAmount)
|
||||
sapronakRpPerBird := 0.0
|
||||
sapronakRpPerKg := 0.0
|
||||
for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} {
|
||||
rpPerBird, _ := calculateMetrics(amount)
|
||||
rpPerBird, rpPerKg := calculateCostMetrics(amount)
|
||||
sapronakRpPerBird += rpPerBird
|
||||
sapronakRpPerKg += rpPerKg
|
||||
}
|
||||
sapronakLabel := "Pengeluaran Sapronak"
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
string(dto.PLCodeSapronak),
|
||||
sapronakLabel,
|
||||
"Pengeluaran Sapronak",
|
||||
"purchase",
|
||||
sapronakRpPerBird,
|
||||
sapronakRpPerKg,
|
||||
totalSapronakAmount,
|
||||
))
|
||||
|
||||
overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational)
|
||||
overheadRpPerBird, overheadRpPerKg := calculateOverheadMetrics(costs.RealizationOperational)
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
string(dto.PLCodeOverhead),
|
||||
"Overhead",
|
||||
@@ -461,7 +494,7 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
|
||||
costs.RealizationOperational,
|
||||
))
|
||||
|
||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost)
|
||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateOverheadMetrics(costs.ExpeditionCost)
|
||||
plItems = append(plItems, dto.ToProfitLossItem(
|
||||
string(dto.PLCodeEkspedisi),
|
||||
"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")
|
||||
}
|
||||
|
||||
afterQuantity := productWarehouse.Quantity
|
||||
newLog := &entity.StockLog{
|
||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||
LoggableId: 0,
|
||||
@@ -183,14 +182,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||
afterQuantity += req.Quantity
|
||||
newLog.Increase = req.Quantity
|
||||
newLog.Stock += newLog.Increase
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
afterQuantity -= req.Quantity
|
||||
newLog.Decrease = req.Quantity
|
||||
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
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -64,7 +64,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(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)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
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"
|
||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
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,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
|
||||
}
|
||||
|
||||
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||
|
||||
totalConsumed := 0.0
|
||||
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 {
|
||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||
}
|
||||
|
||||
if actorID > 0 && totalConsumed > 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)
|
||||
}
|
||||
|
||||
if actorID > 0 && result.UsageQuantity > 0 {
|
||||
decreaseLog := &entity.StockLog{
|
||||
Decrease: totalConsumed,
|
||||
Decrease: result.UsageQuantity,
|
||||
LoggableType: string(utils.StockLogTypeMarketing),
|
||||
LoggableId: deliveryProduct.Id,
|
||||
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
||||
CreatedBy: actorID,
|
||||
Notes: notes,
|
||||
Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
|
||||
}
|
||||
|
||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||
}
|
||||
if len(stockLogs) > 0 {
|
||||
latestStockLog := stockLogs[0]
|
||||
decreaseLog.Stock = latestStockLog.Stock
|
||||
decreaseLog.Stock -= decreaseLog.Decrease
|
||||
decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease
|
||||
} else {
|
||||
decreaseLog.Stock -= decreaseLog.Decrease
|
||||
}
|
||||
@@ -610,6 +564,10 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if actorID > 0 && currentUsage > 0 {
|
||||
increaseLog := &entity.StockLog{
|
||||
Increase: currentUsage,
|
||||
@@ -617,7 +575,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
||||
LoggableId: deliveryProduct.Id,
|
||||
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
||||
CreatedBy: actorID,
|
||||
Notes: "",
|
||||
Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage),
|
||||
}
|
||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
||||
if err != nil {
|
||||
@@ -625,8 +583,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
||||
}
|
||||
if len(stockLogs) > 0 {
|
||||
latestStockLog := stockLogs[0]
|
||||
increaseLog.Stock = latestStockLog.Stock
|
||||
increaseLog.Stock += increaseLog.Increase
|
||||
increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
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/gofiber/fiber/v2"
|
||||
@@ -41,11 +42,12 @@ type salesOrdersService struct {
|
||||
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||
UserRepo userRepo.UserRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
FifoSvc commonSvc.FifoService
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
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 {
|
||||
return &salesOrdersService{
|
||||
Log: utils.Log,
|
||||
@@ -55,6 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
UserRepo: userRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
FifoSvc: fifoSvc,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
}
|
||||
@@ -230,14 +233,14 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
}
|
||||
|
||||
if len(req.MarketingProducts) > 0 {
|
||||
for _, item := range req.MarketingProducts {
|
||||
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
for _, item := range req.MarketingProducts {
|
||||
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,6 +336,32 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
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{
|
||||
"product_warehouse_id": rp.ProductWarehouseId,
|
||||
"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")
|
||||
}
|
||||
|
||||
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
mdp := &entity.MarketingDeliveryProduct{
|
||||
MarketingProductId: old.Id,
|
||||
UnitPrice: 0,
|
||||
TotalWeight: 0,
|
||||
AvgWeight: 0,
|
||||
TotalPrice: 0,
|
||||
DeliveryDate: nil,
|
||||
VehicleNumber: rp.VehicleNumber,
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
|
||||
if deliveryProduct.Id == 0 {
|
||||
mdp := &entity.MarketingDeliveryProduct{
|
||||
MarketingProductId: old.Id,
|
||||
UnitPrice: 0,
|
||||
TotalWeight: 0,
|
||||
AvgWeight: 0,
|
||||
TotalPrice: 0,
|
||||
DeliveryDate: nil,
|
||||
VehicleNumber: rp.VehicleNumber,
|
||||
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")
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
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 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
|
||||
if deliveryProduct.DeliveryDate != nil {
|
||||
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 {
|
||||
@@ -459,6 +491,19 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||
|
||||
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 {
|
||||
if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("marketing_product_id = ?", product.Id).Unscoped()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"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 {
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction)
|
||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||
targetRepoTx := repository.NewLayingTransferTargetRepository(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")
|
||||
}
|
||||
|
||||
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{
|
||||
ProductWarehouseId: *source.ProductWarehouseId,
|
||||
CreatedBy: actorID,
|
||||
@@ -937,36 +959,6 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi
|
||||
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) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user