[FEAT/BE] Add filter delivery order, adjust response purchase and fcr growing recording

This commit is contained in:
ragilap
2026-02-06 14:13:05 +07:00
parent 77ec805931
commit f74b6476de
6 changed files with 274 additions and 32 deletions
+42 -20
View File
@@ -26,6 +26,7 @@ type FifoService interface {
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error
ResolvePending(ctx context.Context, req PendingResolveRequest) ([]PendingResolution, error)
}
type fifoService struct {
@@ -111,6 +112,11 @@ type PendingResolution struct {
Quantity float64
}
type PendingResolveRequest struct {
ProductWarehouseID uint
Tx *gorm.DB
}
type StockReplenishResult struct {
AddedQuantity float64
PendingResolved []PendingResolution
@@ -227,6 +233,23 @@ func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest)
return result, nil
}
func (s *fifoService) ResolvePending(ctx context.Context, req PendingResolveRequest) ([]PendingResolution, error) {
if req.ProductWarehouseID == 0 {
return nil, errors.New("product warehouse id is required")
}
var resolved []PendingResolution
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
var err error
resolved, err = s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID)
return err
})
if err != nil {
return nil, err
}
return resolved, nil
}
func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) {
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return nil, errors.New("usable key and id are required")
@@ -849,8 +872,8 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p
if cfg.Columns.CreatedAt == cfg.Columns.ID {
var rows []struct {
ID uint
Pending float64
CreatedAt int64
Pending float64 `gorm:"column:pending_qty"`
CreatedAt int64 `gorm:"column:created_at"`
}
query := tx.Table(cfg.Table).
@@ -867,27 +890,26 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p
query = query.Order(order)
}
if err := query.Find(&rows).Error; err != nil {
return nil, err
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if row.Pending <= 0 {
continue
}
for _, row := range rows {
if row.Pending <= 0 {
continue
}
candidates = append(candidates, pendingCandidate{
UsableKey: key,
Config: cfg,
UsableID: row.ID,
Pending: row.Pending,
CreatedAt: time.Unix(0, row.CreatedAt),
})
}
} else {
candidates = append(candidates, pendingCandidate{
UsableKey: key,
Config: cfg,
UsableID: row.ID,
Pending: row.Pending,
CreatedAt: time.Unix(0, row.CreatedAt),
})
}
} else {
var rows []struct {
ID uint
Pending float64
CreatedAt time.Time
Pending float64 `gorm:"column:pending_qty"`
CreatedAt time.Time `gorm:"column:created_at"`
}
query := tx.Table(cfg.Table).
@@ -4,6 +4,7 @@ import (
"fmt"
"math"
"sort"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -17,6 +18,7 @@ import (
type MarketingRelationDTO struct {
Id uint `json:"id"`
SoNumber string `json:"so_number"`
DoNumber *string `json:"do_number"`
SoDate time.Time `json:"so_date"`
Notes string `json:"notes,omitempty"`
}
@@ -95,9 +97,16 @@ type DeliveryMarketingProductDTO struct {
}
func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
var doNumber *string
if doNumbers := collectDoNumbers(marketing); len(doNumbers) > 0 {
value := doNumbers[0]
doNumber = &value
}
return MarketingRelationDTO{
Id: marketing.Id,
SoNumber: marketing.SoNumber,
DoNumber: doNumber,
SoDate: marketing.SoDate,
Notes: marketing.Notes,
}
@@ -182,7 +191,6 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType)
}
}
return MarketingListDTO{
MarketingRelationDTO: ToMarketingRelationDTO(marketing),
Customer: customer,
@@ -239,7 +247,6 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
latestApproval = mapped
}
return MarketingDetailDTO{
MarketingRelationDTO: ToMarketingRelationDTO(marketing),
SoDocs: marketing.SoDocs,
@@ -346,11 +353,46 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
}
func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string {
dateStr := ""
if deliveryDate != nil {
dateStr = deliveryDate.Format("20060102")
numberPrefix := soNumber
if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(soNumber)), "SO-") {
numberPrefix = "DO-" + soNumber[3:]
}
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId)
return numberPrefix
}
func collectDoNumbers(marketing *entity.Marketing) []string {
if marketing == nil || len(marketing.Products) == 0 {
return nil
}
seen := make(map[string]struct{})
for _, product := range marketing.Products {
if product.DeliveryProduct == nil || product.DeliveryProduct.DeliveryDate == nil {
continue
}
warehouseID := product.ProductWarehouse.WarehouseId
if warehouseID == 0 && product.ProductWarehouse.Warehouse.Id != 0 {
warehouseID = product.ProductWarehouse.Warehouse.Id
}
if warehouseID == 0 {
continue
}
doNumber := GenerateDeliveryOrderNumber(marketing.SoNumber, product.DeliveryProduct.DeliveryDate, warehouseID)
if doNumber != "" {
seen[doNumber] = struct{}{}
}
}
if len(seen) == 0 {
return nil
}
result := make([]string, 0, len(seen))
for value := range seen {
result = append(result, value)
}
sort.Strings(result)
return result
}
func getVehicleNumber(e entity.MarketingProduct) string {
@@ -40,6 +40,7 @@ type RecordingRepository interface {
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
GetUniformityMeanBwByWeek(tx *gorm.DB, projectFlockKandangId uint, week int) (float64, bool, error)
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error)
@@ -331,6 +332,33 @@ func (r *RecordingRepositoryImpl) GetCumulativeDepletionByProjectFlockKandangUnt
return total, err
}
func (r *RecordingRepositoryImpl) GetUniformityMeanBwByWeek(tx *gorm.DB, projectFlockKandangId uint, week int) (float64, bool, error) {
if projectFlockKandangId == 0 || week <= 0 {
return 0, false, nil
}
var row struct {
ID uint
MeanUp float64
}
if err := tx.
Table("project_flock_kandang_uniformity").
Select("id, mean_up").
Where("project_flock_kandang_id = ?", projectFlockKandangId).
Where("week = ?", week).
Order("id DESC").
Limit(1).
Scan(&row).Error; err != nil {
return 0, false, err
}
if row.ID == 0 {
return 0, false, nil
}
meanBw := row.MeanUp / 1.10
return meanBw, true, nil
}
func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
if currentDay <= 1 {
return nil, nil
@@ -1178,13 +1178,40 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
}
var fcrValue float64
if usageInGrams > 0 && totalEggWeightGrams > 0 {
fcrValue = usageInGrams / totalEggWeightGrams
isGrowing := false
if s.ProjectFlockKandangRepo != nil {
if pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, recording.ProjectFlockKandangId); err == nil {
if strings.EqualFold(pfk.ProjectFlock.Category, string(utils.ProjectFlockCategoryGrowing)) {
isGrowing = true
}
}
}
if isGrowing {
week := 0
if recording.Day != nil && *recording.Day > 0 {
week = (*recording.Day-1)/7 + 1
}
if week > 0 && s.Repository != nil {
meanBw, ok, err := s.Repository.GetUniformityMeanBwByWeek(tx, recording.ProjectFlockKandangId, week)
if err != nil {
return fmt.Errorf("getUniformityMeanBwByWeek: %w", err)
}
if ok && meanBw > 0 && feedIntake > 0 {
fcrValue = feedIntake / meanBw
}
}
updates["fcr_value"] = fcrValue
recording.FcrValue = &fcrValue
} else {
updates["fcr_value"] = gorm.Expr("NULL")
recording.FcrValue = nil
if usageInGrams > 0 && totalEggWeightGrams > 0 {
fcrValue = usageInGrams / totalEggWeightGrams
updates["fcr_value"] = fcrValue
recording.FcrValue = &fcrValue
} else {
updates["fcr_value"] = gorm.Expr("NULL")
recording.FcrValue = nil
}
}
if usageInGrams > 0 && totalChick > 0 {
@@ -465,11 +465,16 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
); err != nil {
return err
}
if strings.EqualFold(category, string(utils.ProjectFlockCategoryGrowing)) {
if err := s.updateGrowingFcrForWeek(tx, createBody.ProjectFlockKandangId, createBody.Week, calculation.MeanUp); err != nil {
return err
}
}
return nil
}); err != nil {
s.Log.Errorf("Failed to create uniformity: %+v", err)
return nil, err
}
}
if s.DocumentSvc != nil {
actorIDCopy := actorID
@@ -633,6 +638,9 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
return nil, err
}
if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil {
return nil, err
}
return s.GetOne(c, id)
}
@@ -694,6 +702,10 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
}
}
if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil {
return nil, err
}
return s.GetOne(c, id)
}
@@ -724,7 +736,48 @@ func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
type uniformityContext struct {
ID uint
Week int
ProjectFlockKandangId uint
Category string
}
var ctxRow uniformityContext
if err := s.Repository.DB().WithContext(c.Context()).
Table("project_flock_kandang_uniformity u").
Select("u.id, u.week, u.project_flock_kandang_id, pf.category").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Where("u.id = ?", id).
Scan(&ctxRow).Error; err != nil {
return err
}
if ctxRow.ID == 0 {
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
return err
}
if strings.EqualFold(ctxRow.Category, string(utils.ProjectFlockCategoryGrowing)) {
startDay := (ctxRow.Week-1)*7 + 1
endDay := ctxRow.Week * 7
if ctxRow.Week > 0 {
if err := tx.Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", ctxRow.ProjectFlockKandangId).
Where("day BETWEEN ? AND ?", startDay, endDay).
Update("fcr_value", 0).Error; err != nil {
return err
}
}
}
return nil
}); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
@@ -734,6 +787,58 @@ func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
return nil
}
func (s *uniformityService) updateGrowingFcrFromUniformity(ctx context.Context, uniformityID uint) error {
if uniformityID == 0 {
return nil
}
type uniformityRow struct {
ID uint
Week int
MeanUp float64
ProjectFlockKandangId uint
Category string
}
var row uniformityRow
if err := s.Repository.DB().WithContext(ctx).
Table("project_flock_kandang_uniformity u").
Select("u.id, u.week, u.mean_up, u.project_flock_kandang_id, pf.category").
Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Where("u.id = ?", uniformityID).
Scan(&row).Error; err != nil {
return err
}
if row.ID == 0 {
return nil
}
if !strings.EqualFold(row.Category, string(utils.ProjectFlockCategoryGrowing)) {
return nil
}
return s.updateGrowingFcrForWeek(s.Repository.DB().WithContext(ctx), row.ProjectFlockKandangId, row.Week, row.MeanUp)
}
func (s *uniformityService) updateGrowingFcrForWeek(tx *gorm.DB, projectFlockKandangID uint, week int, meanUp float64) error {
if tx == nil || projectFlockKandangID == 0 || week <= 0 {
return nil
}
startDay := (week-1)*7 + 1
endDay := week * 7
meanBw := meanUp / 1.10
if meanBw <= 0 {
return tx.Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Where("day BETWEEN ? AND ?", startDay, endDay).
Update("fcr_value", 0).Error
}
return tx.Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Where("day BETWEEN ? AND ?", startDay, endDay).
Update("fcr_value", gorm.Expr("CASE WHEN feed_intake IS NULL OR feed_intake = 0 THEN 0 ELSE feed_intake / ? END", meanBw)).Error
}
func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
@@ -912,6 +912,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
pwID uint
qty float64
}, 0, len(prepared))
resolvePendingIDs := make(map[uint]struct{})
logEntries := make([]struct {
itemID uint
pwID uint
@@ -952,6 +953,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
pwID uint
qty float64
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
resolvePendingIDs[*newPWID] = struct{}{}
} else {
deltas[*newPWID] += deltaQty
totalQtyDeltas[item.Id] += deltaQty
@@ -964,11 +966,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
qty float64
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
affected[*newPWID] = struct{}{}
resolvePendingIDs[*newPWID] = struct{}{}
} else {
deltas[*newPWID] += deltaQty // negative
affected[*newPWID] = struct{}{}
totalQtyDeltas[item.Id] += deltaQty
}
case newPWID != nil:
resolvePendingIDs[*newPWID] = struct{}{}
}
dateCopy := prep.receivedDate
@@ -1066,6 +1071,19 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err
}
}
for pwID := range resolvePendingIDs {
if pwID == 0 {
continue
}
resolved, err := s.FifoSvc.ResolvePending(c.Context(), commonSvc.PendingResolveRequest{
ProductWarehouseID: pwID,
Tx: tx,
})
if err != nil {
return err
}
s.Log.Infof("ResolvePending purchase=%d pw=%d resolved=%d", purchase.Id, pwID, len(resolved))
}
}
if len(logEntries) > 0 {