|
|
|
@@ -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
|
|
|
|
|
}
|
|
|
|
|