mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 06:45:43 +00:00
base example counting sapronak
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||
projectFlockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -17,19 +23,26 @@ import (
|
||||
type ClosingService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||
GetSapronakReport(ctx *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error)
|
||||
}
|
||||
|
||||
type closingService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
ProjectFlockKandangRepo projectFlockRepo.ProjectFlockKandangRepository
|
||||
}
|
||||
|
||||
func NewClosingService(repo repository.ClosingRepository, validate *validator.Validate) ClosingService {
|
||||
func NewClosingService(
|
||||
repo repository.ClosingRepository,
|
||||
projectFlockKandangRepo projectFlockRepo.ProjectFlockKandangRepository,
|
||||
validate *validator.Validate,
|
||||
) ClosingService {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,3 +83,362 @@ func (s closingService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, err
|
||||
}
|
||||
return closing, nil
|
||||
}
|
||||
|
||||
var sapronakFlags = []string{
|
||||
string(utils.FlagDOC),
|
||||
string(utils.FlagPakan),
|
||||
string(utils.FlagOVK),
|
||||
}
|
||||
|
||||
type sapronakIncomingRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Qty float64
|
||||
Value float64
|
||||
DefaultPrice float64
|
||||
}
|
||||
|
||||
type sapronakUsageRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Qty float64
|
||||
DefaultPrice float64
|
||||
}
|
||||
|
||||
func (s closingService) GetSapronakReport(c *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfks, err := s.loadProjectFlockKandangs(c, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pfks) == 0 {
|
||||
return []dto.SapronakReportDTO{}, nil
|
||||
}
|
||||
|
||||
startMap, err := s.mapStartDates(c.Context(), pfks)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to prepare start dates for sapronak report: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare sapronak report")
|
||||
}
|
||||
statusMap, nextStartMap := s.computeStatusAndNextStart(pfks, startMap)
|
||||
|
||||
filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
|
||||
if filterStatus == "" {
|
||||
filterStatus = "all"
|
||||
}
|
||||
|
||||
results := make([]dto.SapronakReportDTO, 0, len(pfks))
|
||||
for _, pfk := range pfks {
|
||||
status := statusMap[pfk.Id]
|
||||
if status == "" {
|
||||
status = "closing"
|
||||
}
|
||||
|
||||
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
|
||||
continue
|
||||
}
|
||||
|
||||
start := startMap[pfk.Id]
|
||||
var startPtr *time.Time
|
||||
if !start.IsZero() {
|
||||
startCopy := start
|
||||
startPtr = &startCopy
|
||||
}
|
||||
|
||||
var endPtr *time.Time
|
||||
if end, ok := nextStartMap[pfk.Id]; ok {
|
||||
endCopy := end
|
||||
endPtr = &endCopy
|
||||
}
|
||||
|
||||
items, totalIncoming, totalUsage, err := s.buildSapronakItems(c.Context(), pfk, startPtr, endPtr)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||
}
|
||||
|
||||
results = append(results, dto.SapronakReportDTO{
|
||||
ProjectFlockKandangID: pfk.Id,
|
||||
ProjectFlockID: pfk.ProjectFlockId,
|
||||
ProjectName: pfk.ProjectFlock.FlockName,
|
||||
KandangID: pfk.KandangId,
|
||||
KandangName: pfk.Kandang.Name,
|
||||
Period: pfk.Period,
|
||||
Status: status,
|
||||
StartDate: startPtr,
|
||||
EndDate: endPtr,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].KandangID == results[j].KandangID {
|
||||
if results[i].Period == results[j].Period {
|
||||
return results[i].ProjectFlockKandangID < results[j].ProjectFlockKandangID
|
||||
}
|
||||
return results[i].Period < results[j].Period
|
||||
}
|
||||
return results[i].KandangID < results[j].KandangID
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s closingService) loadProjectFlockKandangs(c *fiber.Ctx, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||
db := s.ProjectFlockKandangRepo.DB().
|
||||
WithContext(c.Context()).
|
||||
Preload("ProjectFlock").
|
||||
Preload("Kandang")
|
||||
|
||||
if params != nil {
|
||||
if params.ProjectFlockID > 0 {
|
||||
db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID)
|
||||
}
|
||||
if params.KandangID > 0 {
|
||||
db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID)
|
||||
}
|
||||
}
|
||||
|
||||
var pfks []entity.ProjectFlockKandang
|
||||
if err := db.Find(&pfks).Error; err != nil {
|
||||
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
|
||||
}
|
||||
|
||||
return pfks, nil
|
||||
}
|
||||
|
||||
func (s closingService) mapStartDates(ctx context.Context, pfks []entity.ProjectFlockKandang) (map[uint]time.Time, error) {
|
||||
result := make(map[uint]time.Time, len(pfks))
|
||||
if len(pfks) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
ids := make([]uint, len(pfks))
|
||||
for i, pfk := range pfks {
|
||||
ids[i] = pfk.Id
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
|
||||
StartDate *time.Time `gorm:"column:start_date"`
|
||||
}
|
||||
|
||||
if err := s.ProjectFlockKandangRepo.DB().
|
||||
WithContext(ctx).
|
||||
Table("project_chickins").
|
||||
Select("project_flock_kandang_id, MIN(chick_in_date) AS start_date").
|
||||
Where("project_flock_kandang_id IN ?", ids).
|
||||
Group("project_flock_kandang_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if row.StartDate != nil {
|
||||
result[row.ProjectFlockKandangID] = row.StartDate.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
for _, pfk := range pfks {
|
||||
if _, exists := result[pfk.Id]; !exists {
|
||||
result[pfk.Id] = pfk.CreatedAt.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s closingService) computeStatusAndNextStart(pfks []entity.ProjectFlockKandang, startMap map[uint]time.Time) (map[uint]string, map[uint]time.Time) {
|
||||
statusMap := make(map[uint]string, len(pfks))
|
||||
nextStartMap := make(map[uint]time.Time, len(pfks))
|
||||
|
||||
if len(pfks) == 0 {
|
||||
return statusMap, nextStartMap
|
||||
}
|
||||
|
||||
grouped := make(map[uint][]entity.ProjectFlockKandang)
|
||||
for _, pfk := range pfks {
|
||||
grouped[pfk.KandangId] = append(grouped[pfk.KandangId], pfk)
|
||||
}
|
||||
|
||||
for _, list := range grouped {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Period == list[j].Period {
|
||||
return startMap[list[i].Id].Before(startMap[list[j].Id])
|
||||
}
|
||||
return list[i].Period < list[j].Period
|
||||
})
|
||||
|
||||
for idx, item := range list {
|
||||
if idx < len(list)-1 {
|
||||
next := list[idx+1]
|
||||
if start, ok := startMap[next.Id]; ok {
|
||||
nextStartMap[item.Id] = start
|
||||
}
|
||||
statusMap[item.Id] = "closing"
|
||||
continue
|
||||
}
|
||||
statusMap[item.Id] = "active"
|
||||
}
|
||||
}
|
||||
|
||||
return statusMap, nextStartMap
|
||||
}
|
||||
|
||||
func (s closingService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time) ([]dto.SapronakItemDTO, float64, float64, error) {
|
||||
incoming, err := s.fetchIncomingSapronak(ctx, pfk.KandangId, start, end)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
usage, err := s.fetchUsageSapronak(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
|
||||
|
||||
for _, row := range incoming {
|
||||
avgPrice := row.DefaultPrice
|
||||
if row.Qty > 0 && row.Value > 0 {
|
||||
avgPrice = row.Value / row.Qty
|
||||
}
|
||||
|
||||
itemMap[row.ProductID] = dto.SapronakItemDTO{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
IncomingQty: row.Qty,
|
||||
IncomingValue: row.Value,
|
||||
RemainingQty: row.Qty,
|
||||
AveragePrice: avgPrice,
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range usage {
|
||||
existing := itemMap[row.ProductID]
|
||||
price := existing.AveragePrice
|
||||
if price == 0 {
|
||||
price = row.DefaultPrice
|
||||
}
|
||||
|
||||
usageValue := row.Qty * price
|
||||
|
||||
existing.ProductID = row.ProductID
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = row.ProductName
|
||||
}
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = row.Flag
|
||||
}
|
||||
existing.AveragePrice = price
|
||||
existing.UsageQty += row.Qty
|
||||
existing.UsageValue += usageValue
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
|
||||
itemMap[row.ProductID] = existing
|
||||
}
|
||||
|
||||
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||
var totalIncoming, totalUsage float64
|
||||
for _, item := range itemMap {
|
||||
totalIncoming += item.IncomingValue
|
||||
totalUsage += item.UsageValue
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].Flag == items[j].Flag {
|
||||
return strings.ToLower(items[i].ProductName) < strings.ToLower(items[j].ProductName)
|
||||
}
|
||||
return items[i].Flag < items[j].Flag
|
||||
})
|
||||
|
||||
return items, totalIncoming, totalUsage, nil
|
||||
}
|
||||
|
||||
func (s closingService) fetchIncomingSapronak(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint]sapronakIncomingRow, error) {
|
||||
rows := make([]sapronakIncomingRow, 0)
|
||||
|
||||
db := s.Repository.DB().
|
||||
WithContext(ctx).
|
||||
Table("purchase_items AS pi").
|
||||
Select(`
|
||||
pi.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(SUM(pi.total_qty), 0) AS qty,
|
||||
COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
|
||||
COALESCE(p.product_price, 0) AS default_price
|
||||
`).
|
||||
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
|
||||
Joins("JOIN products p ON p.id = pi.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlags).
|
||||
Where("pi.received_date IS NOT NULL")
|
||||
|
||||
if start != nil {
|
||||
db = db.Where("pi.received_date >= ?", *start)
|
||||
}
|
||||
if end != nil {
|
||||
db = db.Where("pi.received_date < ?", *end)
|
||||
}
|
||||
|
||||
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint]sapronakIncomingRow, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.ProductID] = row
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s closingService) fetchUsageSapronak(ctx context.Context, pfkID uint) (map[uint]sapronakUsageRow, error) {
|
||||
rows := make([]sapronakUsageRow, 0)
|
||||
|
||||
if pfkID == 0 {
|
||||
return map[uint]sapronakUsageRow{}, nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().
|
||||
WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(SUM(rs.usage_qty), 0) AS qty,
|
||||
COALESCE(p.product_price, 0) AS default_price
|
||||
`).
|
||||
Joins("JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("r.project_flock_kandangs_id = ?", pfkID).
|
||||
Where("f.name IN ?", sapronakFlags)
|
||||
|
||||
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint]sapronakUsageRow, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.ProductID] = row
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user