fixing report-counting-sapronak

This commit is contained in:
ragilap
2025-12-16 10:44:19 +07:00
parent f60564d673
commit cf7b3418a5
12 changed files with 593 additions and 589 deletions
@@ -14,16 +14,14 @@ import (
) )
type ClosingController struct { type ClosingController struct {
ClosingService service.ClosingService ClosingService service.ClosingService
SapronakService service.SapronakService SapronakService service.SapronakService
SapronakFormatter service.SapronakFormatter
} }
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, sapronakFormatter service.SapronakFormatter) *ClosingController { func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
return &ClosingController{ return &ClosingController{
ClosingService: closingService, ClosingService: closingService,
SapronakService: sapronakService, SapronakService: sapronakService,
SapronakFormatter: sapronakFormatter,
} }
} }
@@ -207,7 +205,7 @@ func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
return err return err
} }
payload := u.SapronakFormatter.ProjectPayload(result, flag) payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.Success{ JSON(response.Success{
@@ -237,7 +235,7 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
return err return err
} }
payload := u.SapronakFormatter.KandangPayload(result, flag) payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.Success{ JSON(response.Success{
+145 -4
View File
@@ -1,6 +1,9 @@
package dto package dto
import "time" import (
"strings"
"time"
)
type SapronakDetailDTO struct { type SapronakDetailDTO struct {
ProductID uint `json:"product_id"` ProductID uint `json:"product_id"`
@@ -82,9 +85,10 @@ type SapronakCategoryDTO struct {
} }
type SapronakProjectAggregatedDTO struct { type SapronakProjectAggregatedDTO struct {
Doc *SapronakCategoryDTO `json:"doc,omitempty"` Doc *SapronakCategoryDTO `json:"doc,omitempty"`
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"` Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"` Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
} }
type ClosingSapronakItemDTO struct { type ClosingSapronakItemDTO struct {
@@ -109,3 +113,140 @@ type ClosingSapronakDTO struct {
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"` IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"` OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
} }
// === Mapper Functions for Aggregated Sapronak Response ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
result := SapronakProjectAggregatedDTO{}
if len(reports) == 0 {
return result
}
rep := reports[0]
return ToSapronakProjectAggregatedFromReport(&rep, flag)
}
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
result := SapronakProjectAggregatedDTO{}
if report == nil {
report = &SapronakReportDTO{}
}
filter := strings.ToUpper(strings.TrimSpace(flag))
byFlag := map[string]**SapronakCategoryDTO{}
if filter == "" || filter == "DOC" {
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["DOC"] = &result.Doc
}
if filter == "" || filter == "OVK" {
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["OVK"] = &result.Ovk
}
if filter == "" || filter == "PAKAN" {
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PAKAN"] = &result.Pakan
}
if filter == "" || filter == "PULLET" {
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PULLET"] = &result.Pullet
}
formatDate := func(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("02-Jan-2006")
}
for _, group := range report.Groups {
flagKey := strings.ToUpper(group.Flag)
ptr := byFlag[flagKey]
if ptr == nil || *ptr == nil {
continue
}
target := *ptr
rowIndexByProduct := make(map[string]int)
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
if idx, ok := rowIndexByProduct[productKey]; ok {
return &target.Rows[idx]
}
target.Rows = append(target.Rows, base)
idx := len(target.Rows) - 1
rowIndexByProduct[productKey] = idx
return &target.Rows[idx]
}
for idx, item := range group.Items {
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
baseRow := SapronakCategoryRowDTO{
ID: idx + 1,
Date: formatDate(item.Tanggal),
ReferenceNumber: item.NoReferensi,
Description: item.ProductName,
ProductCategory: item.ProductName,
UnitPrice: item.Harga,
Notes: "-",
}
row := getOrCreateRow(productKey, baseRow)
switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai
case "pemakaian", "adjustment keluar":
row.QtyUsed += item.QtyKeluar
case "mutasi keluar":
row.QtyOut += item.QtyKeluar
default:
row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai
}
if row.QtyIn > 0 {
row.UnitPrice = row.TotalAmount / row.QtyIn
}
}
for i := range target.Rows {
target.Rows[i].ID = i + 1
}
}
buildTotals := func(cat *SapronakCategoryDTO, label string) {
if cat == nil {
return
}
var qtyIn, qtyOut, qtyUsed, total float64
for _, r := range cat.Rows {
qtyIn += r.QtyIn
qtyOut += r.QtyOut
qtyUsed += r.QtyUsed
total += r.TotalAmount
}
avg := 0.0
if qtyIn > 0 {
avg = total / qtyIn
}
cat.Total = SapronakCategoryTotalDTO{
Label: label,
QtyIn: qtyIn,
QtyOut: qtyOut,
QtyUsed: qtyUsed,
AvgUnitPrice: avg,
TotalAmount: total,
}
}
buildTotals(result.Doc, "TOTAL DOC")
buildTotals(result.Ovk, "TOTAL OVK")
buildTotals(result.Pakan, "TOTAL PAKAN")
buildTotals(result.Pullet, "TOTAL PULLET")
return result
}
+2 -1
View File
@@ -24,6 +24,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
closingRepo := rClosing.NewClosingRepository(db) closingRepo := rClosing.NewClosingRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db) projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
marketingRepo := rMarketings.NewMarketingRepository(db) marketingRepo := rMarketings.NewMarketingRepository(db)
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db) marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
@@ -33,7 +34,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService, sapronakService) ClosingRoutes(router, userService, closingService, sapronakService)
@@ -16,14 +16,14 @@ import (
type ClosingRepository interface { type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
MapSapronakStartDates(ctx context.Context, pfkIDs []uint) (map[uint]time.Time, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
} }
type ClosingRepositoryImpl struct { type ClosingRepositoryImpl struct {
@@ -260,183 +260,159 @@ type SapronakDetailRow struct {
Price float64 Price float64
} }
func (r *ClosingRepositoryImpl) ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
db := r.DB().
WithContext(ctx).
Preload("ProjectFlock").
Preload("Kandang")
if params != nil { func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
if params.ProjectFlockID > 0 {
db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID) func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
} for _, j := range joins {
if params.KandangID > 0 { if strings.TrimSpace(j) != "" {
db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID) db = db.Joins(j)
}
if params.ProjectFlockKandangID > 0 {
db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
} }
} }
return db
var pfks []entity.ProjectFlockKandang
if err := db.Find(&pfks).Error; err != nil {
return nil, err
}
return pfks, nil
} }
func (r *ClosingRepositoryImpl) MapSapronakStartDates(ctx context.Context, pfkIDs []uint) (map[uint]time.Time, error) { func sapronakFlags(flags ...utils.FlagType) []string {
result := make(map[uint]time.Time, len(pfkIDs)) out := make([]string, len(flags))
if len(pfkIDs) == 0 { for i, f := range flags {
return result, nil out[i] = string(f)
} }
return out
}
var rows []struct { var (
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"` sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
StartDate *time.Time `gorm:"column:start_date"` sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
} sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
)
if err := r.DB().
WithContext(ctx).
Table("project_chickins").
Select("project_flock_kandang_id, MIN(chick_in_date) AS start_date").
Where("project_flock_kandang_id IN ?", pfkIDs).
Group("project_flock_kandang_id").
Scan(&rows).Error; err != nil {
return nil, err
}
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
m := make(map[uint][]SapronakDetailRow)
for _, row := range rows { for _, row := range rows {
if row.StartDate != nil { m[row.ProductID] = append(m[row.ProductID], row)
result[row.ProjectFlockKandangID] = row.StartDate.UTC()
}
} }
return m
return result, nil
} }
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) { func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
rows := make([]SapronakIncomingRow, 0)
db := r.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 ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)}).
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
}
return rows, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) {
rows := make([]SapronakUsageRow, 0)
if pfkID == 0 {
return rows, nil
}
db := r.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 ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
if start != nil {
db = db.Where("r.record_datetime >= ?", *start)
}
if end != nil {
db = db.Where("r.record_datetime < ?", *end)
}
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
rows := make([]SapronakDetailRow, 0) rows := make([]SapronakDetailRow, 0)
db := r.DB().
WithContext(ctx).
Table("purchase_items AS pi").
Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
pi.received_date AS date,
COALESCE(po.po_number, '') AS reference,
COALESCE(pi.total_qty,0) AS qty_in,
0 AS qty_out,
COALESCE(pi.price,0) AS 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 ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)}).
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.Scan(&rows).Error; err != nil { if err := db.Scan(&rows).Error; err != nil {
return nil, err return nil, err
} }
return groupSapronakDetails(rows), nil
result := make(map[uint][]SapronakDetailRow)
for _, row := range rows {
result[row.ProductID] = append(result[row.ProductID], row)
}
return result, nil
} }
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) { // =========================
rows := make([]SapronakDetailRow, 0) // Usage (summary + details)
// =========================
db := r.DB(). func (r *ClosingRepositoryImpl) usageQuery(
WithContext(ctx). ctx context.Context,
Table("recording_stocks AS rs"). table string,
Select(` pwJoinCond string,
joins []string,
where string,
args ...any,
) *gorm.DB {
db := r.withCtx(ctx).Table(table).Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(SUM(usage_qty), 0) AS qty,
COALESCE(p.product_price, 0) AS default_price
`)
db = applyJoins(db, joins...)
return db.
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
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(where, args...)
}
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
ctx context.Context,
table string,
pwJoinCond string,
joins []string,
where string,
args ...any,
) ([]SapronakUsageRow, error) {
rows := make([]SapronakUsageRow, 0)
db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) detailQuery(
ctx context.Context,
table string,
pwJoinCond string,
joins []string,
selectSQL string,
where string,
args ...any,
) *gorm.DB {
db := r.withCtx(ctx).
Table(table).
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
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)
db = applyJoins(db, joins...)
return db.Select(selectSQL).Where(where, args...)
}
func (r *ClosingRepositoryImpl) fetchSapronakDetails(
ctx context.Context,
table string,
pwJoinCond string,
joins []string,
selectSQL string,
where string,
args ...any,
) (map[uint][]SapronakDetailRow, error) {
return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
}
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
if pfkID == 0 {
return nil, nil
}
return r.fetchSapronakUsage(
ctx,
"recording_stocks rs",
"pw.id = rs.product_warehouse_id",
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
"r.project_flock_kandangs_id = ? AND f.name IN ?",
pfkID,
sapronakFlagsUsage,
)
}
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
if pfkID == 0 {
return []SapronakUsageRow{}, nil
}
return r.fetchSapronakUsage(
ctx,
"project_chickins pc",
"pw.id = pc.product_warehouse_id",
nil,
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
pfkID,
sapronakFlagsChickin,
)
}
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
return r.fetchSapronakDetails(
ctx,
"recording_stocks rs",
"pw.id = rs.product_warehouse_id",
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
`
pw.product_id AS product_id, pw.product_id AS product_id,
p.name AS product_name, p.name AS product_name,
f.name AS flag, f.name AS flag,
@@ -445,184 +421,180 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, p
0 AS qty_in, 0 AS qty_in,
COALESCE(rs.usage_qty,0) AS qty_out, COALESCE(rs.usage_qty,0) AS qty_out,
COALESCE(p.product_price,0) AS price COALESCE(p.product_price,0) AS price
`,
"r.project_flock_kandangs_id = ? AND f.name IN ?",
pfkID,
sapronakFlagsUsage,
)
}
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
return r.fetchSapronakDetails(
ctx,
"project_chickins pc",
"pw.id = pc.product_warehouse_id",
nil,
`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
pc.chick_in_date AS date,
CAST(pc.id AS TEXT) AS reference,
0 AS qty_in,
COALESCE(pc.usage_qty,0) AS qty_out,
COALESCE(p.product_price,0) AS price
`,
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
pfkID,
sapronakFlagsChickin,
)
}
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
return r.withCtx(ctx).
Table("purchase_items AS pi").
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 ?", sapronakFlagsAll).
Where("pi.received_date IS NOT NULL")
}
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
rows := make([]SapronakIncomingRow, 0)
db := r.incomingPurchaseBase(ctx, kandangID).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
`)
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
return scanAndGroupDetails(
r.incomingPurchaseBase(ctx, kandangID).Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
pi.received_date AS date,
COALESCE(po.po_number, '') AS reference,
COALESCE(pi.total_qty,0) AS qty_in,
0 AS qty_out,
COALESCE(pi.price,0) AS price
`),
)
}
type stockLogSapronakRow struct {
ID uint `gorm:"column:id"`
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
Flag string `gorm:"column:flag"`
CreatedAt *time.Time `gorm:"column:created_at"`
Increase float64 `gorm:"column:increase"`
Decrease float64 `gorm:"column:decrease"`
Price float64 `gorm:"column:price"`
MovementNumber string `gorm:"column:movement_number"`
}
func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
rows := make([]stockLogSapronakRow, 0)
movementSelect := "'' AS movement_number"
joins := []string{}
if withMovement {
movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
}
db := r.withCtx(ctx).
Table("stock_logs sl").
Select(`
sl.id AS id,
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
sl.created_at AS created_at,
COALESCE(sl.increase,0) AS increase,
COALESCE(sl.decrease,0) AS decrease,
COALESCE(p.product_price,0) AS price,
` + movementSelect + `
`). `).
Joins("JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"). Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_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). Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("r.project_flock_kandangs_id = ?", pfkID). Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
Where("f.name IN ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
if start != nil { db = applyJoins(db, joins...)
db = db.Where("r.record_datetime >= ?", *start)
}
if end != nil {
db = db.Where("r.record_datetime < ?", *end)
}
if err := db.Scan(&rows).Error; err != nil { if err := db.
Where("sl.loggable_type = ?", logType).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Scan(&rows).Error; err != nil {
return nil, err return nil, err
} }
result := make(map[uint][]SapronakDetailRow) return rows, nil
for _, row := range rows {
result[row.ProductID] = append(result[row.ProductID], row)
}
return result, nil
} }
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
incoming := make(map[uint][]SapronakDetailRow) in := make(map[uint][]SapronakDetailRow)
outgoing := make(map[uint][]SapronakDetailRow) out := make(map[uint][]SapronakDetailRow)
rows := make([]struct {
ID uint
ProductID uint
ProductName string
Flag string
CreatedAt *time.Time
Increase float64
Decrease float64
Price float64
}, 0)
db := r.DB().
WithContext(ctx).
Table("stock_logs sl").
Select(`
sl.id AS id,
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
sl.created_at AS created_at,
COALESCE(sl.increase,0) AS increase,
COALESCE(sl.decrease,0) AS decrease,
COALESCE(p.product_price,0) AS price
`).
Joins("JOIN product_warehouses pw ON pw.id = sl.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).
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("sl.loggable_type = ?", entity.LogTypeAdjustment).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
if start != nil {
db = db.Where("sl.created_at >= ?", *start)
}
if end != nil {
db = db.Where("sl.created_at < ?", *end)
}
if err := db.Scan(&rows).Error; err != nil {
return nil, nil, err
}
for _, row := range rows { for _, row := range rows {
ref := fmt.Sprintf("ADJ-%d", row.ID) base := SapronakDetailRow{
ProductID: row.ProductID,
ProductName: row.ProductName,
Flag: row.Flag,
Date: row.CreatedAt,
Reference: refFn(row),
Price: row.Price,
}
if row.Increase > 0 { if row.Increase > 0 {
incoming[row.ProductID] = append(incoming[row.ProductID], SapronakDetailRow{ d := base
ProductID: row.ProductID, d.QtyIn = row.Increase
ProductName: row.ProductName, in[row.ProductID] = append(in[row.ProductID], d)
Flag: row.Flag,
Date: row.CreatedAt,
Reference: ref,
QtyIn: row.Increase,
QtyOut: 0,
Price: row.Price,
})
} }
if row.Decrease > 0 { if row.Decrease > 0 {
outgoing[row.ProductID] = append(outgoing[row.ProductID], SapronakDetailRow{ d := base
ProductID: row.ProductID, d.QtyOut = row.Decrease
ProductName: row.ProductName, out[row.ProductID] = append(out[row.ProductID], d)
Flag: row.Flag,
Date: row.CreatedAt,
Reference: ref,
QtyIn: 0,
QtyOut: row.Decrease,
Price: row.Price,
})
} }
} }
return incoming, outgoing, nil return in, out
} }
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
incoming := make(map[uint][]SapronakDetailRow) rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
outgoing := make(map[uint][]SapronakDetailRow) if err != nil {
rows := make([]struct {
ID uint
ProductID uint
ProductName string
Flag string
CreatedAt *time.Time
Increase float64
Decrease float64
Price float64
}, 0)
db := r.DB().
WithContext(ctx).
Table("stock_logs sl").
Select(`
sl.id AS id,
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
sl.created_at AS created_at,
COALESCE(sl.increase,0) AS increase,
COALESCE(sl.decrease,0) AS decrease,
COALESCE(p.product_price,0) AS price
`).
Joins("JOIN product_warehouses pw ON pw.id = sl.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).
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("sl.loggable_type = ?", entity.LogTypeTransfer).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
if start != nil {
db = db.Where("sl.created_at >= ?", *start)
}
if end != nil {
db = db.Where("sl.created_at < ?", *end)
}
if err := db.Scan(&rows).Error; err != nil {
return nil, nil, err return nil, nil, err
} }
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
for _, row := range rows { return in, out, nil
ref := fmt.Sprintf("TRF-%d", row.ID) }
if row.Increase > 0 {
incoming[row.ProductID] = append(incoming[row.ProductID], SapronakDetailRow{ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
ProductID: row.ProductID, rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
ProductName: row.ProductName, if err != nil {
Flag: row.Flag, return nil, nil, err
Date: row.CreatedAt, }
Reference: ref, in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
QtyIn: row.Increase, if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
QtyOut: 0, return ref
Price: row.Price, }
}) return fmt.Sprintf("TRF-%d", row.ID)
} })
if row.Decrease > 0 { return in, out, nil
outgoing[row.ProductID] = append(outgoing[row.ProductID], SapronakDetailRow{
ProductID: row.ProductID,
ProductName: row.ProductName,
Flag: row.Flag,
Date: row.CreatedAt,
Reference: ref,
QtyIn: 0,
QtyOut: row.Decrease,
Price: row.Price,
})
}
}
return incoming, outgoing, nil
} }
+1 -2
View File
@@ -10,8 +10,7 @@ import (
) )
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) { func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
formatter := closing.NewSapronakFormatter() ctrl := controller.NewClosingController(s, sapronakSvc)
ctrl := controller.NewClosingController(s, sapronakSvc, formatter)
route := v1.Group("/closings") route := v1.Group("/closings")
@@ -13,36 +13,35 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
type SapronakService interface { type SapronakService interface {
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
GetSapronakReport(ctx *fiber.Ctx, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error)
} }
type sapronakService struct { type sapronakService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.ClosingRepository Repository repository.ClosingRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
} }
func NewSapronakService(repo repository.ClosingRepository, validate *validator.Validate) SapronakService { func NewSapronakService(
repo repository.ClosingRepository,
pfkRepo projectflockRepository.ProjectFlockKandangRepository,
validate *validator.Validate,
) SapronakService {
return &sapronakService{ return &sapronakService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
ProjectFlockKandangRepo: pfkRepo,
} }
} }
func (s sapronakService) GetSapronakReport(c *fiber.Ctx, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, err
}
return s.computeSapronakReports(c.Context(), params)
}
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) { func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required") return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
@@ -96,13 +95,6 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
return []dto.SapronakReportDTO{}, nil return []dto.SapronakReportDTO{}, nil
} }
startMap, err := s.mapStartDates(ctx, 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)) filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
if filterStatus == "" { if filterStatus == "" {
filterStatus = "all" filterStatus = "all"
@@ -110,29 +102,17 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
results := make([]dto.SapronakReportDTO, 0, len(pfks)) results := make([]dto.SapronakReportDTO, 0, len(pfks))
for _, pfk := range pfks { for _, pfk := range pfks {
status := statusMap[pfk.Id] status := "closing"
if status == "" { if pfk.ClosedAt == nil {
status = "closing" status = "active"
} }
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") { if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
continue continue
} }
start := startMap[pfk.Id] // We no longer filter by date for closing sapronak report; pass nil pointers.
var startPtr *time.Time items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
if !start.IsZero() {
startCopy := start
startPtr = &startCopy
}
var endPtr *time.Time
if end, ok := nextStartMap[pfk.Id]; ok {
endCopy := end
endPtr = &endCopy
}
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, startPtr, endPtr, params.Flag)
if err != nil { if err != nil {
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err) 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") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
@@ -146,8 +126,8 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
KandangName: pfk.Kandang.Name, KandangName: pfk.Kandang.Name,
Period: pfk.Period, Period: pfk.Period,
Status: status, Status: status,
StartDate: startPtr, StartDate: nil,
EndDate: endPtr, EndDate: nil,
TotalIncomingValue: totalIncoming, TotalIncomingValue: totalIncoming,
TotalUsageValue: totalUsage, TotalUsageValue: totalUsage,
Items: items, Items: items,
@@ -159,41 +139,31 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
} }
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) { func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
pfks, err := s.Repository.ListProjectFlockKandangsForSapronak(ctx, params) db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
if err != nil { Preload("ProjectFlock").
Preload("Kandang").
Preload("Chickins")
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)
}
if params.ProjectFlockKandangID > 0 {
db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
}
}
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) 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 nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
} }
return pfks, nil return pfks, nil
} }
func (s sapronakService) 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
}
startDates, err := s.Repository.MapSapronakStartDates(ctx, ids)
if err != nil {
return nil, err
}
for _, pfk := range pfks {
if start, ok := startDates[pfk.Id]; ok {
result[pfk.Id] = start
continue
}
result[pfk.Id] = pfk.CreatedAt.UTC()
}
return result, nil
}
func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO { func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
if len(reports) == 0 { if len(reports) == 0 {
return dto.SapronakReportDTO{} return dto.SapronakReportDTO{}
@@ -202,7 +172,6 @@ func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO,
var ( var (
totalIncoming float64 totalIncoming float64
totalUsage float64 totalUsage float64
earliestStart *time.Time
projectName = reports[0].ProjectName projectName = reports[0].ProjectName
) )
@@ -220,11 +189,6 @@ func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO,
for _, r := range reports { for _, r := range reports {
totalIncoming += r.TotalIncomingValue totalIncoming += r.TotalIncomingValue
totalUsage += r.TotalUsageValue totalUsage += r.TotalUsageValue
if r.StartDate != nil {
if earliestStart == nil || r.StartDate.Before(*earliestStart) {
earliestStart = r.StartDate
}
}
for _, it := range r.Items { for _, it := range r.Items {
cur := itemMap[it.ProductID] cur := itemMap[it.ProductID]
@@ -274,7 +238,7 @@ func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO,
ProjectFlockID: projectID, ProjectFlockID: projectID,
ProjectName: projectName, ProjectName: projectName,
Status: "combined", Status: "combined",
StartDate: earliestStart, StartDate: nil,
TotalIncomingValue: totalIncoming, TotalIncomingValue: totalIncoming,
TotalUsageValue: totalUsage, TotalUsageValue: totalUsage,
Items: items, Items: items,
@@ -282,36 +246,6 @@ func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO,
} }
} }
func (s sapronakService) 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 {
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 mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) { func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) {
incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows)) incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows))
for _, row := range incomingRows { for _, row := range incomingRows {
@@ -330,6 +264,7 @@ type sapronakDetailMaps struct {
AdjIncoming map[uint][]dto.SapronakDetailDTO AdjIncoming map[uint][]dto.SapronakDetailDTO
AdjOutgoing map[uint][]dto.SapronakDetailDTO AdjOutgoing map[uint][]dto.SapronakDetailDTO
TransferIn map[uint][]dto.SapronakDetailDTO TransferIn map[uint][]dto.SapronakDetailDTO
TransferOut map[uint][]dto.SapronakDetailDTO
} }
func buildSapronakDetails( func buildSapronakDetails(
@@ -338,6 +273,7 @@ func buildSapronakDetails(
adjIncomingRows map[uint][]repository.SapronakDetailRow, adjIncomingRows map[uint][]repository.SapronakDetailRow,
adjOutgoingRows map[uint][]repository.SapronakDetailRow, adjOutgoingRows map[uint][]repository.SapronakDetailRow,
transferInRows map[uint][]repository.SapronakDetailRow, transferInRows map[uint][]repository.SapronakDetailRow,
transferOutRows map[uint][]repository.SapronakDetailRow,
) sapronakDetailMaps { ) sapronakDetailMaps {
result := sapronakDetailMaps{ result := sapronakDetailMaps{
Incoming: make(map[uint][]dto.SapronakDetailDTO), Incoming: make(map[uint][]dto.SapronakDetailDTO),
@@ -345,6 +281,7 @@ func buildSapronakDetails(
AdjIncoming: make(map[uint][]dto.SapronakDetailDTO), AdjIncoming: make(map[uint][]dto.SapronakDetailDTO),
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO), AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
TransferIn: make(map[uint][]dto.SapronakDetailDTO), TransferIn: make(map[uint][]dto.SapronakDetailDTO),
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
} }
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) { addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
@@ -376,32 +313,43 @@ func buildSapronakDetails(
addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true) addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false) addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true) addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
return result return result
} }
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) { func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId, start, end) // For sapronak closing report we intentionally ignore date range
// and aggregate all historical transactions for the kandang/project.
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId, start, end) incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id, start, end) usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id, start, end) chickinUsageRows, err := s.Repository.FetchSapronakChickinUsage(ctx, pfk.Id)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId, start, end) usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
transIncomingRows, _, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId, start, end) chickinUsageDetailsRows, err := s.Repository.FetchSapronakChickinUsageDetails(ctx, pfk.Id)
if err != nil {
return nil, nil, 0, 0, err
}
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
if err != nil {
return nil, nil, 0, 0, err
}
transIncomingRows, transOutgoingRows, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
@@ -414,16 +362,50 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
return strings.ToUpper(f) == filterFlag return strings.ToUpper(f) == filterFlag
} }
incoming, usage := mapIncomingUsage(incomingRows, usageRows) // For project flocks with category GROWING, pullet usage from chickin
// should not be counted yet. Only when category is LAYING we allow
// pullet usage to contribute to qty_used.
isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
if !isLaying {
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
for _, row := range chickinUsageRows {
if strings.ToUpper(row.Flag) == "DOC" {
filteredUsage = append(filteredUsage, row)
}
}
chickinUsageRows = filteredUsage
filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
for pid, rows := range chickinUsageDetailsRows {
for _, d := range rows {
if strings.ToUpper(d.Flag) == "DOC" {
filteredDetail[pid] = append(filteredDetail[pid], d)
}
}
}
chickinUsageDetailsRows = filteredDetail
}
allUsageRows := append(usageRows, chickinUsageRows...)
incoming, usage := mapIncomingUsage(incomingRows, allUsageRows)
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage)) itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
groupMap := make(map[string]*dto.SapronakGroupDTO) groupMap := make(map[string]*dto.SapronakGroupDTO)
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows) for pid, rows := range chickinUsageDetailsRows {
if len(rows) == 0 {
continue
}
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
}
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
incomingDetails := detailMaps.Incoming incomingDetails := detailMaps.Incoming
usageDetails := detailMaps.Usage usageDetails := detailMaps.Usage
adjIncoming := detailMaps.AdjIncoming adjIncoming := detailMaps.AdjIncoming
adjOutgoing := detailMaps.AdjOutgoing adjOutgoing := detailMaps.AdjOutgoing
transIncoming := detailMaps.TransferIn transIncoming := detailMaps.TransferIn
transOutgoing := detailMaps.TransferOut
ensureGroup := func(flag string) *dto.SapronakGroupDTO { ensureGroup := func(flag string) *dto.SapronakGroupDTO {
if g, ok := groupMap[flag]; ok { if g, ok := groupMap[flag]; ok {
@@ -670,6 +652,26 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
} }
for productID, details := range transOutgoing {
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
d.Flag = flag
d.ProductName = name
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
}
}
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap)) groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
for _, g := range groupMap { for _, g := range groupMap {
groups = append(groups, *g) groups = append(groups, *g)
@@ -1,125 +0,0 @@
package service
import (
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
)
type SapronakFormatter interface {
ProjectPayload(reports []dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO
KandangPayload(report *dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO
}
type sapronakFormatter struct{}
func NewSapronakFormatter() SapronakFormatter {
return &sapronakFormatter{}
}
func (f *sapronakFormatter) ProjectPayload(reports []dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO {
result := dto.SapronakProjectAggregatedDTO{}
if len(reports) == 0 {
return result
}
rep := reports[0]
return f.mapFromReport(&rep, flag)
}
func (f *sapronakFormatter) KandangPayload(report *dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO {
return f.mapFromReport(report, flag)
}
func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO {
result := dto.SapronakProjectAggregatedDTO{}
if report == nil {
report = &dto.SapronakReportDTO{}
}
filter := strings.ToUpper(strings.TrimSpace(flag))
byFlag := map[string]**dto.SapronakCategoryDTO{}
if filter == "" || filter == "DOC" {
result.Doc = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0)}
byFlag["DOC"] = &result.Doc
}
if filter == "" || filter == "OVK" {
result.Ovk = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0),}
byFlag["OVK"] = &result.Ovk
}
if filter == "" || filter == "PAKAN" {
result.Pakan = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0),}
byFlag["PAKAN"] = &result.Pakan
}
formatDate := func(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("02-Jan-2006")
}
for _, group := range report.Groups {
flag := strings.ToUpper(group.Flag)
ptr := byFlag[flag]
if ptr == nil || *ptr == nil {
continue
}
target := *ptr
for idx, item := range group.Items {
qtyUsed := item.QtyKeluar
if qtyUsed == 0 {
qtyUsed = item.QtyMasuk
}
target.Rows = append(target.Rows, dto.SapronakCategoryRowDTO{
ID: idx + 1,
Date: formatDate(item.Tanggal),
ReferenceNumber: item.NoReferensi,
QtyIn: item.QtyMasuk,
QtyOut: item.QtyKeluar,
QtyUsed: qtyUsed,
Description: item.ProductName,
ProductCategory: item.ProductName,
UnitPrice: item.Harga,
TotalAmount: item.Nilai,
Notes: "-",
})
}
}
buildTotals := func(cat *dto.SapronakCategoryDTO, label string) {
if cat == nil {
return
}
var qtyIn, qtyOut, qtyUsed, total float64
for _, r := range cat.Rows {
qtyIn += r.QtyIn
qtyOut += r.QtyOut
qtyUsed += r.QtyUsed
total += r.TotalAmount
}
avg := 0.0
if qtyIn > 0 {
avg = total / qtyIn
}
cat.Total = dto.SapronakCategoryTotalDTO{
Label: label,
QtyIn: qtyIn,
QtyOut: qtyOut,
QtyUsed: qtyUsed,
AvgUnitPrice: avg,
TotalAmount: total,
}
}
buildTotals(result.Doc, "TOTAL DOC")
buildTotals(result.Ovk, "TOTAL OVK")
buildTotals(result.Pakan, "TOTAL PAKAN")
return result
}
@@ -5,5 +5,5 @@ type CountSapronakQuery struct {
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"` KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
Status string `query:"status" validate:"omitempty,oneof=active closing all"` Status string `query:"status" validate:"omitempty,oneof=active closing all"`
Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN doc ovk pakan"` Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN PULLET doc ovk pakan pullet"`
} }
@@ -27,7 +27,7 @@ type ProductWarehouseRepository interface {
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint) (uint, error) EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
@@ -199,10 +199,21 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
ctx context.Context, ctx context.Context,
productID uint, productID uint,
warehouseID uint, warehouseID uint,
projectFlockKandangID *uint,
createdBy uint, createdBy uint,
) (uint, error) { ) (uint, error) {
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil { if err == nil {
// Backfill project_flock_kandang_id when it's missing and caller provides one.
if projectFlockKandangID != nil && (record.ProjectFlockKandangId == nil || *record.ProjectFlockKandangId == 0) {
if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("id = ?", record.Id).
Update("project_flock_kandang_id", *projectFlockKandangID).Error; err != nil {
return 0, err
}
record.ProjectFlockKandangId = projectFlockKandangID
}
return record.Id, nil return record.Id, nil
} }
if !errors.Is(err, gorm.ErrRecordNotFound) { if !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -210,9 +221,10 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
} }
entity := &entity.ProductWarehouse{ entity := &entity.ProductWarehouse{
ProductId: productID, ProductId: productID,
WarehouseId: warehouseID, WarehouseId: warehouseID,
Quantity: 0, ProjectFlockKandangId: projectFlockKandangID,
Quantity: 0,
// CreatedBy: uint(createdBy), // CreatedBy: uint(createdBy),
} }
// if entity.CreatedBy == 0 { // if entity.CreatedBy == 0 {
@@ -28,6 +28,7 @@ type ProjectFlockKandangRepository interface {
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
} }
@@ -189,9 +189,6 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
if upd.VehicleNumber != nil { if upd.VehicleNumber != nil {
data["vehicle_number"] = upd.VehicleNumber data["vehicle_number"] = upd.VehicleNumber
} }
if upd.ReceivedQty != nil {
data["total_qty"] = upd.ReceivedQty
}
if upd.WarehouseID != nil && *upd.WarehouseID != 0 { if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
data["warehouse_id"] = upd.WarehouseID data["warehouse_id"] = upd.WarehouseID
} }
@@ -814,7 +814,13 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
// Always ensure PW when qty > 0 so stockable has target. // Always ensure PW when qty > 0 so stockable has target.
if prep.receivedQty > 0 { if prep.receivedQty > 0 {
pwID, err := pwRepoTx.EnsureProductWarehouse(c.Context(), uint(item.ProductId), prep.warehouseID, purchase.CreatedBy) pwID, err := pwRepoTx.EnsureProductWarehouse(
c.Context(),
uint(item.ProductId),
prep.warehouseID,
item.ProjectFlockKandangId,
purchase.CreatedBy,
)
if err != nil { if err != nil {
return err return err
} }