mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
[FEAT/BE] Add filter delivery order, adjust response purchase and fcr growing recording
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user