mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
fixing report-counting-sapronak
This commit is contained in:
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-4
@@ -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 {
|
||||||
|
|||||||
+1
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user