package repository import ( "context" "fmt" "strings" "time" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) type ClosingRepository interface { repository.BaseRepository[entity.ProjectFlock] GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) FetchSapronakChickinUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) } type ClosingRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.ProjectFlock] } func NewClosingRepository(db *gorm.DB) ClosingRepository { return &ClosingRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db), } } type SapronakRow struct { Id uint64 `gorm:"column:id"` SortDate time.Time `gorm:"column:sort_date"` DateText string `gorm:"column:date_text"` ReferenceNumber string `gorm:"column:reference_number"` TransactionType string `gorm:"column:transaction_type"` ProductName string `gorm:"column:product_name"` ProductCategory string `gorm:"column:product_category"` ProductSubCategory string `gorm:"column:product_sub_category"` SourceWarehouse string `gorm:"column:source_warehouse"` DestinationWarehouse string `gorm:"column:destination_warehouse"` Destination string `gorm:"column:destination"` Quantity float64 `gorm:"column:quantity"` UnitID uint `gorm:"column:unit_id"` Unit string `gorm:"column:unit"` Notes string `gorm:"column:notes"` } type SapronakSummaryRow struct { Category string `gorm:"column:category"` TotalQty int64 `gorm:"column:total_qty"` UomID uint `gorm:"column:uom_id"` UomName string `gorm:"column:uom_name"` } type ExpeditionHPPRow struct { SupplierName string `gorm:"column:supplier_name"` TotalAmount float64 `gorm:"column:total_amount"` } type SapronakQueryParams struct { Type string WarehouseIDs []uint ProjectFlockKandangIDs []uint Limit int Offset int Search string StartDate *time.Time EndDate *time.Time } func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) { db := r.DB().WithContext(ctx) var ( unionParts []string args []any ) switch params.Type { case validation.SapronakTypeIncoming: if len(params.WarehouseIDs) == 0 { return []SapronakRow{}, 0, nil } unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL) args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs) case validation.SapronakTypeOutgoing: if len(params.WarehouseIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL) args = append(args, params.WarehouseIDs, params.WarehouseIDs) } if len(params.ProjectFlockKandangIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) args = append(args, params.ProjectFlockKandangIDs) } if len(unionParts) == 0 { return []SapronakRow{}, 0, nil } default: return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type) } unionSQL := strings.Join(unionParts, " UNION ALL ") search := strings.TrimSpace(params.Search) searchClause := "" var searchArgs []any if search != "" { searchClause = ` WHERE ( reference_number ILIKE ? OR product_name ILIKE ? OR product_category ILIKE ? OR source_warehouse ILIKE ? OR destination_warehouse ILIKE ? OR CAST(quantity AS TEXT) ILIKE ? OR unit ILIKE ? OR notes ILIKE ? OR transaction_type ILIKE ? )` like := "%" + search + "%" searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like) } var totalResults int64 dateClause := "" var dateArgs []any if params.StartDate != nil { dateClause += " AND sort_date::date >= ?" dateArgs = append(dateArgs, params.StartDate) } if params.EndDate != nil { dateClause += " AND sort_date::date <= ?" dateArgs = append(dateArgs, params.EndDate) } whereClause := searchClause if dateClause != "" { if whereClause == "" { whereClause = " WHERE " + strings.TrimPrefix(dateClause, " AND ") } else { whereClause += dateClause } } countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, whereClause) countArgs := append(append(append([]any{}, args...), searchArgs...), dateArgs...) if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil { return nil, 0, err } dataArgs := append(append(append([]any{}, args...), searchArgs...), dateArgs...) dataArgs = append(dataArgs, params.Limit, params.Offset) dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, whereClause) var rows []SapronakRow if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil { return nil, 0, err } return rows, totalResults, nil } func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) { db := r.DB().WithContext(ctx) var ( unionParts []string args []any ) switch params.Type { case validation.SapronakTypeIncoming: if len(params.WarehouseIDs) == 0 { return []SapronakSummaryRow{}, nil } unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL) args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs) case validation.SapronakTypeOutgoing: if len(params.WarehouseIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL) args = append(args, params.WarehouseIDs, params.WarehouseIDs) } if len(params.ProjectFlockKandangIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) args = append(args, params.ProjectFlockKandangIDs) } if len(unionParts) == 0 { return []SapronakSummaryRow{}, nil } default: return nil, fmt.Errorf("invalid sapronak type: %s", params.Type) } unionSQL := strings.Join(unionParts, " UNION ALL ") search := strings.TrimSpace(params.Search) searchClause := "" var searchArgs []any if search != "" { searchClause = ` WHERE ( reference_number ILIKE ? OR product_name ILIKE ? OR product_category ILIKE ? OR source_warehouse ILIKE ? OR destination_warehouse ILIKE ? OR CAST(quantity AS TEXT) ILIKE ? OR unit ILIKE ? OR notes ILIKE ? OR transaction_type ILIKE ? )` like := "%" + search + "%" searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like) } dateClause := "" var dateArgs []any if params.StartDate != nil { dateClause += " AND sort_date::date >= ?" dateArgs = append(dateArgs, params.StartDate) } if params.EndDate != nil { dateClause += " AND sort_date::date <= ?" dateArgs = append(dateArgs, params.EndDate) } whereClause := searchClause if dateClause != "" { if whereClause == "" { whereClause = " WHERE " + strings.TrimPrefix(dateClause, " AND ") } else { whereClause += dateClause } } querySQL := fmt.Sprintf(` SELECT product_category AS category, CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty, unit_id AS uom_id, unit AS uom_name FROM (%s) AS combined%s GROUP BY product_category, unit_id, unit ORDER BY product_category ASC, unit ASC `, unionSQL, whereClause) queryArgs := append(append(append([]any{}, args...), searchArgs...), dateArgs...) var rows []SapronakSummaryRow if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { if len(projectFlockKandangIDs) == 0 { return 0, 0, nil } var purchaseAgg struct { TotalIn float64 `gorm:"column:total_in"` } err := r.DB().WithContext(ctx). Table("purchase_items pi"). Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'"). Where("f.name = ?", "PAKAN"). Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs). Select("COALESCE(SUM(pi.total_qty), 0) AS total_in"). Scan(&purchaseAgg).Error if err != nil { return 0, 0, err } var usageAgg struct { TotalUsed float64 `gorm:"column:total_used"` } err = r.DB().WithContext(ctx). Table("recording_stocks rs"). Joins("JOIN recordings rec ON rec.id = rs.recording_id"). Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). Joins("JOIN products prod ON prod.id = pw.product_id"). Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). Where("rec.project_flock_kandangs_id IN ?", projectFlockKandangIDs). Where("f.name = ?", "PAKAN"). Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used"). Scan(&usageAgg).Error if err != nil { return 0, 0, err } return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil } func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { if len(projectFlockKandangIDs) == 0 { return 0, nil } var total float64 if err := r.DB().WithContext(ctx). Model(&entity.ProjectChickin{}). Where("project_flock_kandang_id IN ?", projectFlockKandangIDs). Select("COALESCE(SUM(usage_qty), 0)"). Scan(&total).Error; err != nil { return 0, err } return total, nil } func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { if len(projectFlockKandangIDs) == 0 { return 0, nil } var agg struct { Total float64 `gorm:"column:total_culling"` } err := r.DB().WithContext(ctx). Table("recording_depletions rd"). Joins("JOIN recordings rec ON rec.id = rd.recording_id"). Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id"). Joins("JOIN products prod ON prod.id = pw.product_id"). Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). Where("COALESCE(rd.source_project_flock_kandang_id, rec.project_flock_kandangs_id) IN ?", projectFlockKandangIDs). Where("f.name = ?", utils.FlagAyamCulling). Select("COALESCE(SUM(rd.qty), 0) AS total_culling"). Scan(&agg).Error if err != nil { return 0, err } return agg.Total, nil } func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) { if len(projectFlockKandangIDs) == 0 { return 0, 0, 0, nil } return r.sumMarketingAttributedByProjectFlockKandangIDs(ctx, projectFlockKandangIDs, nil) } func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) { if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { return 0, 0, 0, nil } return r.sumMarketingAttributedByProjectFlockKandangIDs(ctx, projectFlockKandangIDs, flagNames) } func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) { if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { return 0, nil } var agg struct { TotalQty float64 `gorm:"column:total_qty"` } err := r.DB().WithContext(ctx). Table("recording_eggs re"). Joins("JOIN recordings rec ON rec.id = re.recording_id"). Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id"). Joins("JOIN products prod ON prod.id = pw.product_id"). Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). Where("COALESCE(re.project_flock_kandang_id, rec.project_flock_kandangs_id) IN ?", projectFlockKandangIDs). Where("f.name IN ?", flagNames). Select("COALESCE(SUM(re.qty), 0) AS total_qty"). Scan(&agg).Error if err != nil { return 0, err } return agg.TotalQty, nil } func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) { db := r.DB().WithContext(ctx) if projectFlockID == 0 { return nil, fmt.Errorf("invalid project flock id") } query := db. Table("expense_realizations AS er"). Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id"). Joins("JOIN expenses e ON e.id = ens.expense_id"). Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id"). Joins("JOIN nonstocks n ON n.id = ens.nonstock_id"). Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock). Joins("JOIN suppliers s ON s.id = e.supplier_id"). Where("pfk.project_flock_id = ?", projectFlockID). Where("e.category = ?", "BOP"). Where("e.realization_date IS NOT NULL"). Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))) if projectFlockKandangID != nil && *projectFlockKandangID != 0 { query = query.Where("pfk.id = ?", *projectFlockKandangID) } var rows []ExpeditionHPPRow err := query. Select( "e.supplier_id AS supplier_id, " + "s.name AS supplier_name, " + "SUM(er.qty * er.price) AS total_amount", ). Group("e.supplier_id, s.name"). Scan(&rows).Error if err != nil { return nil, err } return rows, nil } const ( sapronakIncomingPurchasesSQL = ` SELECT CAST(pi.id AS BIGINT) AS id, COALESCE(pi.received_date, '1970-01-01') AS sort_date, COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text, COALESCE(p.po_number, '') AS reference_number, 'Pembelian' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_category, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_sub_category, '-' AS source_warehouse, w.name AS destination_warehouse, '' AS destination, pi.total_qty AS quantity, u.id AS unit_id, u.name AS unit, COALESCE(p.notes, '') AS notes FROM purchase_items pi JOIN purchases p ON p.id = pi.purchase_id JOIN products prod ON prod.id = pi.product_id JOIN uoms u ON u.id = prod.uom_id JOIN warehouses w ON w.id = pi.warehouse_id WHERE pi.warehouse_id IN ? ` sapronakIncomingTransfersSQL = ` SELECT CAST(st.id AS BIGINT) AS id, st.transfer_date AS sort_date, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, st.movement_number AS reference_number, 'Mutasi' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_category, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_sub_category, COALESCE(fw.name, '') AS source_warehouse, COALESCE(tw.name, '') AS destination_warehouse, '' AS destination, std.usage_qty AS quantity, u.id AS unit_id, u.name AS unit, st.reason AS notes FROM stock_transfer_details std JOIN stock_transfers st ON st.id = std.stock_transfer_id LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id JOIN products prod ON prod.id = std.product_id JOIN uoms u ON u.id = prod.uom_id WHERE st.to_warehouse_id IN ? ` sapronakIncomingAdjustmentsSQL = ` SELECT CAST(ast.id AS BIGINT) AS id, ast.created_at AS sort_date, COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text, COALESCE(ast.adj_number, '') AS reference_number, 'Adjustment stock' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_category, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_sub_category, '-' AS source_warehouse, COALESCE(w.name, '') AS destination_warehouse, '' AS destination, COALESCE(ast.total_qty, 0) AS quantity, u.id AS unit_id, u.name AS unit, '-' AS notes FROM adjustment_stocks ast JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id JOIN products prod ON prod.id = pw.product_id JOIN uoms u ON u.id = prod.uom_id WHERE pw.warehouse_id IN ? AND COALESCE(ast.total_qty, 0) <> 0 ` sapronakOutgoingTransfersSQL = ` SELECT CAST(st.id AS BIGINT) AS id, st.transfer_date AS sort_date, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, st.movement_number AS reference_number, 'Mutasi' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_category, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_sub_category, COALESCE(fw.name, '') AS source_warehouse, COALESCE(tw.name, '') AS destination_warehouse, '' AS destination, std.usage_qty AS quantity, u.id AS unit_id, u.name AS unit, st.reason AS notes FROM stock_transfer_details std JOIN stock_transfers st ON st.id = std.stock_transfer_id LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id JOIN products prod ON prod.id = std.product_id JOIN uoms u ON u.id = prod.uom_id WHERE st.from_warehouse_id IN ? ` sapronakOutgoingAdjustmentsSQL = ` SELECT CAST(ast.id AS BIGINT) AS id, ast.created_at AS sort_date, COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text, COALESCE(ast.adj_number, '') AS reference_number, 'Adjustment stock' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_category, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_sub_category, COALESCE(w.name, '') AS source_warehouse, '-' AS destination_warehouse, '' AS destination, COALESCE(ast.usage_qty, 0) AS quantity, u.id AS unit_id, u.name AS unit, '-' AS notes FROM adjustment_stocks ast JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id JOIN products prod ON prod.id = pw.product_id JOIN uoms u ON u.id = prod.uom_id WHERE pw.warehouse_id IN ? AND COALESCE(ast.usage_qty, 0) <> 0 AND EXISTS ( SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = 'products' AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') ) ` sapronakOutgoingMarketingsSQL = ` SELECT CAST(mp.id AS BIGINT) AS id, m.so_date AS sort_date, TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text, m.so_number AS reference_number, 'Penjualan' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_category, COALESCE(( SELECT string_agg( f.name, ' ' ORDER BY CASE WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 ELSE 1 END, f.name ) FROM flags f WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id ), '') AS product_sub_category, w.name AS source_warehouse, COALESCE(c.name, '') AS destination_warehouse, '' AS destination, mp.qty AS quantity, u.id AS unit_id, u.name AS unit, m.notes AS notes FROM marketing_products mp JOIN marketings m ON m.id = mp.marketing_id LEFT JOIN customers c ON c.id = m.customer_id JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id JOIN products prod ON prod.id = pw.product_id JOIN uoms u ON u.id = prod.uom_id JOIN warehouses w ON w.id = pw.warehouse_id WHERE pw.project_flock_kandang_id IN ? AND EXISTS ( SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = 'products' AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') ) ` ) type SapronakIncomingRow struct { ProductID uint ProductName string Flag string Qty float64 Value float64 DefaultPrice float64 } type SapronakUsageRow struct { ProductID uint ProductName string Flag string Qty float64 DefaultPrice float64 } type SapronakDetailRow struct { ProductID uint ProductName string Flag string Date *time.Time Reference string QtyIn float64 QtyOut float64 Price float64 } func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) } func (r *ClosingRepositoryImpl) sumMarketingAttributedByProjectFlockKandangIDs( ctx context.Context, projectFlockKandangIDs []uint, flagNames []string, ) (float64, float64, float64, error) { var agg struct { TotalWeight float64 `gorm:"column:total_weight"` TotalQty float64 `gorm:"column:total_qty"` TotalPrice float64 `gorm:"column:total_price"` } query := r.withCtx(ctx). Table("(?) AS mda", repository.MarketingDeliveryAttributionRowsQuery(r.withCtx(ctx))). Joins("JOIN marketing_delivery_products mdp ON mdp.id = mda.marketing_delivery_product_id"). Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). Joins("JOIN products prod ON prod.id = pw.product_id"). Where("mda.project_flock_kandang_id IN ?", projectFlockKandangIDs). Where("mdp.delivery_date IS NOT NULL") if len(flagNames) > 0 { query = query. Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("f.name IN ?", flagNames) } err := query. Select(` COALESCE(SUM(CASE WHEN COALESCE(mdp.usage_qty, 0) > 0 THEN mdp.total_weight * (mda.allocated_qty / mdp.usage_qty) ELSE 0 END), 0) AS total_weight, COALESCE(SUM(mda.allocated_qty), 0) AS total_qty, COALESCE(SUM(CASE WHEN COALESCE(mdp.usage_qty, 0) > 0 THEN mdp.total_price * (mda.allocated_qty / mdp.usage_qty) ELSE 0 END), 0) AS total_price `). Scan(&agg).Error if err != nil { return 0, 0, 0, err } return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil } func applyDateRange(db *gorm.DB, column string, start, end *time.Time) *gorm.DB { if start != nil { db = db.Where(column+"::date >= ?", start) } if end != nil { db = db.Where(column+"::date <= ?", end) } return db } func applyJoins(db *gorm.DB, joins ...string) *gorm.DB { for _, j := range joins { if strings.TrimSpace(j) != "" { db = db.Joins(j) } } return db } func sapronakFlags(flags ...utils.FlagType) []string { out := make([]string, len(flags)) for i, f := range flags { out[i] = string(f) } return out } var ( sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet) sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK) sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet) ) func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB { subquery := r.DB(). Table("flags"). Select("DISTINCT ON (flagable_id) flagable_id, name"). Where("flagable_type = ?", entity.FlagableTypeProduct). Where("name IN ?", sapronakFlagsAll). Order(fmt.Sprintf( "flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 ELSE 5 END", utils.FlagDOC, utils.FlagPullet, utils.FlagPakan, utils.FlagOVK, )) return db.Joins("JOIN (?) f ON f.flagable_id = "+productAlias+".id", subquery) } func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow { m := make(map[uint][]SapronakDetailRow) for _, row := range rows { m[row.ProductID] = append(m[row.ProductID], row) } return m } func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) { rows := make([]SapronakDetailRow, 0) if err := db.Scan(&rows).Error; err != nil { return nil, err } return groupSapronakDetails(rows), nil } // ========================= // Usage (summary + details) // ========================= func (r *ClosingRepositoryImpl) usageQuery( ctx context.Context, table string, 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...) db = db. Joins("JOIN product_warehouses pw ON "+pwJoinCond). Joins("JOIN products p ON p.id = pw.product_id"). Where(where, args...) db = r.joinSapronakProductFlag(db, "p") return db } 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 scanUsage(db *gorm.DB) ([]SapronakUsageRow, error) { rows := make([]SapronakUsageRow, 0) 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") db = applyJoins(db, joins...) db = r.joinSapronakProductFlag(db, "p") 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, start, end *time.Time) ([]SapronakUsageRow, error) { if pfkID == 0 { return nil, nil } db := r.usageQuery( 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, ) db = applyDateRange(db, "r.record_datetime", start, end) return scanUsage(db) } func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) { if pfkID == 0 { return []SapronakUsageRow{}, nil } db := r.usageQuery( 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, ) db = applyDateRange(db, "pc.chick_in_date", start, end) return scanUsage(db) } func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) { db := r.detailQuery( 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, p.name AS product_name, f.name AS flag, r.record_datetime AS date, CAST(r.id AS TEXT) AS reference, 0 AS qty_in, COALESCE(rs.usage_qty,0) AS qty_out, COALESCE(p.product_price,0) AS price `, "r.project_flock_kandangs_id = ? AND f.name IN ?", pfkID, sapronakFlagsUsage, ) db = applyDateRange(db, "r.record_datetime", start, end) return scanAndGroupDetails(db) } func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) { db := r.detailQuery( 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, ) db = applyDateRange(db, "pc.chick_in_date", start, end) return scanAndGroupDetails(db) } func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) { if projectFlockKandangID == 0 { return map[uint][]SapronakDetailRow{}, nil } dateExpr := "COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pc.chick_in_date, r.record_datetime)" query := r.withCtx(ctx). Table("stock_allocations AS sa"). Select(` p_resolve.id AS product_id, p_resolve.name AS product_name, f.name AS flag, COALESCE( pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pc.chick_in_date, r.record_datetime ) AS date, COALESCE( po.po_number, st.movement_number, lt.transfer_number, CONCAT('ADJ-', ast.id), CONCAT('CHICKIN-', pc.id), CAST(r.id AS TEXT), '' ) AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, COALESCE(pi.price, p_resolve.product_price, 0) AS price `). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()). Joins("LEFT JOIN recordings r ON r.id = rs.recording_id"). Joins("LEFT JOIN project_chickins pc_used ON pc_used.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()). Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id"). Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id"). Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). Joins("LEFT JOIN product_warehouses pw_ltt ON pw_ltt.id = ltt.product_warehouse_id"). Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Joins("LEFT JOIN product_warehouses pw_pc ON pw_pc.id = pc.product_warehouse_id"). Joins("LEFT JOIN products p_resolve ON p_resolve.id = COALESCE(pi.product_id, pw_ltt.product_id, pw_pc.product_id, pw.product_id)"). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume). Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()). Where("f.name IN ?", sapronakFlagsAll). Where(` (sa.usable_type = ? AND r.project_flock_kandangs_id = ? AND f.name IN ?) OR (sa.usable_type = ? AND pc_used.project_flock_kandang_id = ? AND f.name IN ?) `, fifo.UsableKeyRecordingStock.String(), projectFlockKandangID, sapronakFlagsUsage, fifo.UsableKeyProjectChickin.String(), projectFlockKandangID, sapronakFlagsChickin, ) query = r.joinSapronakProductFlag(query, "p_resolve"). Group(` p_resolve.id, p_resolve.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pc.chick_in_date, r.record_datetime, po.po_number, st.movement_number, lt.transfer_number, ast.id, pc.id, r.id, pi.price, p_resolve.product_price `) query = applyDateRange(query, dateExpr, start, end) return scanAndGroupDetails(query) } func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint, start, end *time.Time) *gorm.DB { db := 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 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") db = applyDateRange(db, "pi.received_date", start, end) return r.joinSapronakProductFlag(db, "p") } func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) { rows := make([]SapronakIncomingRow, 0) db := r.incomingPurchaseBase(ctx, kandangID, start, end).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, start, end *time.Time) (map[uint][]SapronakDetailRow, error) { return scanAndGroupDetails( r.incomingPurchaseBase(ctx, kandangID, start, end).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 product_warehouses pw ON pw.id = sl.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id") db = applyJoins(db, joins...) db = r.joinSapronakProductFlag(db, "p") 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 rows, nil } func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) { in := make(map[uint][]SapronakDetailRow) out := make(map[uint][]SapronakDetailRow) for _, row := range rows { base := SapronakDetailRow{ ProductID: row.ProductID, ProductName: row.ProductName, Flag: row.Flag, Date: row.CreatedAt, Reference: refFn(row), Price: row.Price, } if row.Increase > 0 { d := base d.QtyIn = row.Increase in[row.ProductID] = append(in[row.ProductID], d) } if row.Decrease > 0 { d := base d.QtyOut = row.Decrease out[row.ProductID] = append(out[row.ProductID], d) } } return in, out } func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { poByWarehouse := r.DB(). Table("purchase_items pi"). Select("DISTINCT ON (pi.product_warehouse_id) pi.product_warehouse_id, po.po_number, pi.received_date"). Joins("JOIN purchases po ON po.id = pi.purchase_id"). Where("pi.received_date IS NOT NULL"). Order("pi.product_warehouse_id, pi.received_date ASC") incomingQuery := r.withCtx(ctx). Table("adjustment_stocks AS ast"). Select(` pw.product_id AS product_id, p.name AS product_name, f.name AS flag, ast.created_at AS date, CONCAT('ADJ-', ast.id) AS reference, COALESCE(ast.total_qty, 0) AS qty_in, 0 AS qty_out, COALESCE(p.product_price, 0) AS price `). Joins("JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). Where("w.kandang_id = ?", kandangID). Where("f.name IN ?", sapronakFlagsAll). Where("COALESCE(ast.total_qty, 0) > 0") incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p") incomingQuery = applyDateRange(incomingQuery, "ast.created_at", start, end) incoming, err := scanAndGroupDetails(incomingQuery) if err != nil { return nil, nil, err } dateExpr := "COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at)" outgoingQuery := r.withCtx(ctx). Table("stock_allocations AS sa"). Select(` pw.product_id AS product_id, p.name AS product_name, f.name AS flag, COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date, COALESCE(po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, COALESCE(p.product_price, 0) AS price `). Joins("JOIN adjustment_stocks ast ON ast.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyAdjustmentOut.String()). Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id"). Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id"). Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). Joins("LEFT JOIN adjustment_stocks ast_in ON ast_in.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Joins("LEFT JOIN (?) pfp_po ON pfp_po.product_warehouse_id = pfp.product_warehouse_id", poByWarehouse). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume). Where("w.kandang_id = ?", kandangID). Where("f.name IN ?", sapronakFlagsAll). Where("f.name NOT IN ?", sapronakFlags(utils.FlagDOC, utils.FlagPullet)). Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price") outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") outgoingQuery = applyDateRange(outgoingQuery, dateExpr, start, end) outgoing, err := scanAndGroupDetails(outgoingQuery) if err != nil { return nil, nil, err } return incoming, outgoing, nil } func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { incomingQuery := r.withCtx(ctx). Table("stock_transfer_details AS std"). Select(` std.product_id AS product_id, p.name AS product_name, f.name AS flag, st.transfer_date::timestamp AS date, COALESCE(st.movement_number, '') AS reference, COALESCE(std.total_qty, 0) AS qty_in, 0 AS qty_out, COALESCE(p.product_price, 0) AS price `). Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). Joins("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = std.product_id"). Where("w.kandang_id = ?", kandangID). Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll) incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p") incomingQuery = applyDateRange(incomingQuery, "st.transfer_date", start, end) incoming, err := scanAndGroupDetails(incomingQuery) if err != nil { return nil, nil, err } incomingLayingQuery := r.withCtx(ctx). Table("laying_transfer_targets AS ltt"). Select(` pw.product_id AS product_id, p.name AS product_name, f.name AS flag, lt.transfer_date::timestamp AS date, COALESCE(lt.transfer_number, '') AS reference, COALESCE(ltt.total_qty, 0) AS qty_in, 0 AS qty_out, COALESCE(p.product_price, 0) AS price `). Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lt.source_product_warehouse_id"). Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). Where("w.kandang_id = ?", kandangID). Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll) incomingLayingQuery = r.joinSapronakProductFlag(incomingLayingQuery, "p") incomingLayingQuery = applyDateRange(incomingLayingQuery, "lt.transfer_date", start, end) incomingLaying, err := scanAndGroupDetails(incomingLayingQuery) if err != nil { return nil, nil, err } for pid, rows := range incomingLaying { incoming[pid] = append(incoming[pid], rows...) } outgoingQuery := r.withCtx(ctx). Table("stock_allocations AS sa"). Select(` std.product_id AS product_id, p.name AS product_name, f.name AS flag, st.transfer_date::timestamp AS date, COALESCE(st.movement_number, '') AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, COALESCE(p.product_price, 0) AS price `). Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()). Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id"). Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id"). Joins("JOIN products p ON p.id = std.product_id"). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume). Where("w.kandang_id = ?", kandangID). Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll). Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price") outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") outgoingQuery = applyDateRange(outgoingQuery, "st.transfer_date", start, end) outgoing, err := scanAndGroupDetails(outgoingQuery) if err != nil { return nil, nil, err } outgoingLayingQuery := r.withCtx(ctx). Table("stock_allocations AS sa"). Select(` pw.product_id AS product_id, p.name AS product_name, f.name AS flag, lt.transfer_date::timestamp AS date, COALESCE(lt.transfer_number, '') AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, COALESCE(p.product_price, 0) AS price `). Joins("JOIN laying_transfers lt ON lt.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()). Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id"). Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id"). Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume). Where("w.kandang_id = ?", kandangID). Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll). Group("lt.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price") outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p") outgoingLayingQuery = applyDateRange(outgoingLayingQuery, "lt.transfer_date", start, end) outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery) if err != nil { return nil, nil, err } for pid, rows := range outgoingLaying { outgoing[pid] = append(outgoing[pid], rows...) } return incoming, outgoing, nil } func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) { attributedProjectFlockKandangExpr := ` COALESCE( pc.project_flock_kandang_id, pi.project_flock_kandang_id, source_pw.project_flock_kandang_id, ltt.target_project_flock_kandang_id, pw.project_flock_kandang_id ) ` query := r.withCtx(ctx). Table("stock_allocations AS sa"). Select(` pw.product_id AS product_id, p.name AS product_name, f.name AS flag, COALESCE(mdp.delivery_date, mdp.created_at) AS date, COALESCE(m.so_number, '') AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, COALESCE(mdp.unit_price, mp.unit_price, 0) AS price `). Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()). Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). Joins("JOIN marketings m ON m.id = mp.marketing_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("LEFT JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id"). Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume). Where(attributedProjectFlockKandangExpr+" = ?", projectFlockKandangID). Where("f.name IN ?", sapronakFlagsAll). Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") query = r.joinSapronakProductFlag(query, "p") query = applyDateRange(query, "COALESCE(mdp.delivery_date, mdp.created_at)", start, end) sales, err := scanAndGroupDetails(query) if err != nil { return nil, err } nonFifoQuery := r.withCtx(ctx). Table("marketing_delivery_products AS mdp"). Select(` pw.product_id AS product_id, p.name AS product_name, f.name AS flag, COALESCE(mdp.delivery_date, mdp.created_at) AS date, COALESCE(m.so_number, '') AS reference, 0 AS qty_in, COALESCE(mdp.usage_qty, 0) AS qty_out, COALESCE(mdp.unit_price, mp.unit_price, 0) AS price `). Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). Joins("JOIN marketings m ON m.id = mp.marketing_id"). Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). Joins("LEFT JOIN stock_allocations sa ON sa.usable_id = mdp.id AND sa.usable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyMarketingDelivery.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Where("mdp.usage_qty > 0"). Where("sa.id IS NULL"). Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). Where("f.name IN ?", sapronakFlagsAll). Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") nonFifoQuery = r.joinSapronakProductFlag(nonFifoQuery, "p") nonFifoQuery = applyDateRange(nonFifoQuery, "COALESCE(mdp.delivery_date, mdp.created_at)", start, end) nonFifoSales, err := scanAndGroupDetails(nonFifoQuery) if err != nil { return nil, err } for pid, rows := range nonFifoSales { sales[pid] = append(sales[pid], rows...) } return sales, nil } func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) { if projectFlockKandangID == 0 { return map[uint][]SapronakDetailRow{}, nil } pfpType := fifo.StockableKeyProjectFlockPopulation.String() dateExpr := fmt.Sprintf(` CASE WHEN sa.stockable_type = '%s' THEN COALESCE( pi_pc.received_date, st_pc.transfer_date, lt_pc.transfer_date, ast_pc.created_at, pc.chick_in_date ) ELSE COALESCE( pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at ) END `, pfpType) attributedProjectFlockKandangExpr := ` COALESCE( pc.project_flock_kandang_id, pi.project_flock_kandang_id, source_pw.project_flock_kandang_id, ltt.target_project_flock_kandang_id, pw_sales.project_flock_kandang_id ) ` query := r.withCtx(ctx). Table("stock_allocations AS sa"). Select(fmt.Sprintf(` p_resolve.id AS product_id, p_resolve.name AS product_name, f.name AS flag, CASE WHEN sa.stockable_type = '%s' THEN COALESCE( pi_pc.received_date, st_pc.transfer_date, lt_pc.transfer_date, ast_pc.created_at, pc.chick_in_date ) ELSE COALESCE( pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at ) END AS date, CASE WHEN sa.stockable_type = '%s' THEN COALESCE( po_pc.po_number, st_pc.movement_number, lt_pc.transfer_number, CASE WHEN ast_pc.id IS NOT NULL THEN CONCAT('ADJ-', ast_pc.id) END, CONCAT('CHICKIN-', pc.id), '' ) ELSE COALESCE( po.po_number, st.movement_number, lt.transfer_number, CASE WHEN ast.id IS NOT NULL THEN CONCAT('ADJ-', ast.id) END, '' ) END AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, CASE WHEN sa.stockable_type = '%s' THEN COALESCE(pi_pc.price, p_resolve.product_price, 0) ELSE COALESCE(pi.price, p_resolve.product_price, 0) END AS price `, pfpType, pfpType, pfpType)). Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()). Joins("JOIN product_warehouses pw_sales ON pw_sales.id = mdp.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id"). Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id"). Joins("LEFT JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id"). Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). Joins("LEFT JOIN product_warehouses pw_ltt ON pw_ltt.id = ltt.product_warehouse_id"). Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Joins("LEFT JOIN stock_allocations sa_pc ON sa_pc.usable_type = ? AND sa_pc.usable_id = pc.id", fifo.UsableKeyProjectChickin.String()). Joins("LEFT JOIN purchase_items pi_pc ON pi_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Joins("LEFT JOIN purchases po_pc ON po_pc.id = pi_pc.purchase_id"). Joins("LEFT JOIN stock_transfer_details std_pc ON std_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("LEFT JOIN stock_transfers st_pc ON st_pc.id = std_pc.stock_transfer_id"). Joins("LEFT JOIN laying_transfer_targets ltt_pc ON ltt_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). Joins("LEFT JOIN laying_transfers lt_pc ON lt_pc.id = ltt_pc.laying_transfer_id"). Joins("LEFT JOIN adjustment_stocks ast_pc ON ast_pc.id = sa_pc.stockable_id AND sa_pc.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). Joins("LEFT JOIN product_warehouses pw_pc ON pw_pc.id = pc.product_warehouse_id"). Joins(fmt.Sprintf("LEFT JOIN products p_resolve ON p_resolve.id = CASE WHEN sa.stockable_type = '%s' THEN pw_pc.product_id ELSE COALESCE(pi.product_id, pw_ltt.product_id, pw.product_id) END", pfpType)). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume). Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()). Where(attributedProjectFlockKandangExpr+" = ?", projectFlockKandangID). Where("f.name IN ?", sapronakFlagsAll). Group(` p_resolve.id, p_resolve.name, f.name, pi_pc.received_date, st_pc.transfer_date, lt_pc.transfer_date, ast_pc.created_at, pc.chick_in_date, pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, po_pc.po_number, st_pc.movement_number, lt_pc.transfer_number, ast_pc.id, pc.id, po.po_number, st.movement_number, lt.transfer_number, ast.id, pi_pc.price, pi.price, p_resolve.product_price, sa.stockable_type `) query = r.joinSapronakProductFlag(query, "p_resolve") query = applyDateRange(query, dateExpr, start, end) return scanAndGroupDetails(query) } func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { if len(productIDs) == 0 { return []entity.Product{}, nil } var products []entity.Product err := r.DB().WithContext(ctx). Preload("Flags"). Where("id IN ?", productIDs). Find(&products).Error if err != nil { return nil, err } return products, nil }