Compare commits

..

1 Commits

Author SHA1 Message Date
giovanni 09c951c16a adjust max limit 2026-01-20 11:49:40 +07:00
26 changed files with 196 additions and 1072 deletions
@@ -1,3 +0,0 @@
ALTER TABLE recording_depletions
DROP COLUMN IF EXISTS pending_qty,
DROP COLUMN IF EXISTS source_product_warehouse_id;
@@ -1,17 +0,0 @@
ALTER TABLE recording_depletions
ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint;
UPDATE recording_depletions rd
SET source_product_warehouse_id = src.product_warehouse_id
FROM recordings r
JOIN LATERAL (
SELECT pfp.product_warehouse_id
FROM project_chickins pc
JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
ORDER BY pfp.created_at ASC, pfp.id ASC
LIMIT 1
) AS src ON true
WHERE r.id = rd.recording_id
AND rd.source_product_warehouse_id IS NULL;
-2
View File
@@ -4,9 +4,7 @@ type RecordingDepletion struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
Qty float64 `gorm:"column:qty;not null"` Qty float64 `gorm:"column:qty;not null"`
PendingQty float64 `gorm:"column:pending_qty"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
@@ -239,7 +239,6 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
Type: strings.ToLower(c.Query("type")), Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
} }
if raw := c.Query("kandang_id"); raw != "" { if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw) kandangInt, convErr := strconv.Atoi(raw)
@@ -278,45 +277,6 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetClosingSapronakSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")),
Search: c.Query("search"),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
}
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
result, err := u.ClosingService.GetClosingSapronakSummary(c, uint(id), query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved closing report (sapronak summary) successfully",
Data: result,
})
}
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error { func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("project_flock_id")
flag := c.Query("flag", "") flag := c.Query("flag", "")
+4 -4
View File
@@ -110,13 +110,13 @@ type ClosingPerformanceDTO struct {
AwgStd float64 `json:"awg_std"` AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"` FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"` FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct float64 `json:"hen_day_act,omitempty"` HenDayAct *float64 `json:"hen_day_act,omitempty"`
HendayStd float64 `json:"hen_day_std"` HendayStd float64 `json:"hen_day_std"`
EggMass float64 `json:"egg_mass,omitempty"` EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"` EggMassStd float64 `json:"egg_mass_std"`
EggWeight float64 `json:"egg_weight,omitempty"` EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"` EggWeightStd float64 `json:"egg_weight_std"`
HenHouseAct float64 `json:"hen_housed_act,omitempty"` HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"` HenHouseStd float64 `json:"hen_housed_std"`
} }
@@ -114,17 +114,6 @@ type ClosingSapronakDTO struct {
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"` OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
} }
type ClosingSapronakSummaryItemDTO struct {
Category string `json:"category"`
TotalQty int64 `json:"total_qty"`
Uom UomSummaryDTO `json:"uom"`
}
type UomSummaryDTO struct {
ID uint `json:"id"`
Name string `json:"name"`
}
// === Mapper Functions for Aggregated Sapronak Response === // === Mapper Functions for Aggregated Sapronak Response ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO { func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
@@ -212,50 +201,20 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
switch strings.ToLower(item.JenisTransaksi) { switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk": case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk row.QtyIn += item.QtyMasuk
if row.UnitPrice == 0 { row.TotalAmount += item.Nilai
if item.QtyMasuk > 0 && item.Nilai > 0 {
row.UnitPrice = item.Nilai / item.QtyMasuk
} else if item.Harga > 0 {
row.UnitPrice = item.Harga
}
}
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
case "pemakaian", "adjustment keluar": case "pemakaian", "adjustment keluar":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyUsed += item.QtyKeluar row.QtyUsed += item.QtyKeluar
row.TotalAmount += item.QtyKeluar * price case "mutasi keluar":
case "mutasi keluar", "penjualan":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyOut += item.QtyKeluar row.QtyOut += item.QtyKeluar
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
default: default:
row.QtyIn += item.QtyMasuk row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai row.TotalAmount += item.Nilai
}
if row.QtyIn > 0 { if row.QtyIn > 0 {
row.UnitPrice = row.TotalAmount / row.QtyIn row.UnitPrice = row.TotalAmount / row.QtyIn
} }
} }
}
for i := range target.Rows { for i := range target.Rows {
target.Rows[i].ID = i + 1 target.Rows[i].ID = i + 1
@@ -274,8 +233,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
total += r.TotalAmount total += r.TotalAmount
} }
avg := 0.0 avg := 0.0
if qtyUsed > 0 { if qtyIn > 0 {
avg = total / qtyUsed avg = total / qtyIn
} }
cat.Total = SapronakCategoryTotalDTO{ cat.Total = SapronakCategoryTotalDTO{
Label: label, Label: label,
@@ -17,7 +17,6 @@ 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)
GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
@@ -34,7 +33,6 @@ type ClosingRepository interface {
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (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) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
} }
@@ -61,18 +59,10 @@ type SapronakRow struct {
DestinationWarehouse string `gorm:"column:destination_warehouse"` DestinationWarehouse string `gorm:"column:destination_warehouse"`
Destination string `gorm:"column:destination"` Destination string `gorm:"column:destination"`
Quantity float64 `gorm:"column:quantity"` Quantity float64 `gorm:"column:quantity"`
UnitID uint `gorm:"column:unit_id"`
Unit string `gorm:"column:unit"` Unit string `gorm:"column:unit"`
Notes string `gorm:"column:notes"` 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 { type ExpeditionHPPRow struct {
SupplierName string `gorm:"column:supplier_name"` SupplierName string `gorm:"column:supplier_name"`
TotalAmount float64 `gorm:"column:total_amount"` TotalAmount float64 `gorm:"column:total_amount"`
@@ -84,7 +74,6 @@ type SapronakQueryParams struct {
ProjectFlockKandangIDs []uint ProjectFlockKandangIDs []uint
Limit int Limit int
Offset int Offset int
Search string
} }
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) { func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
@@ -120,36 +109,14 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
unionSQL := strings.Join(unionParts, " UNION ALL ") 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 var totalResults int64
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause) countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
countArgs := append(append([]any{}, args...), searchArgs...) if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
dataArgs := append(append([]any{}, args...), searchArgs...) dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
dataArgs = append(dataArgs, params.Limit, params.Offset) dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
var rows []SapronakRow var rows []SapronakRow
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil { if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
@@ -159,79 +126,6 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
return rows, totalResults, nil 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)
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
args = append(args, 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)
}
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, searchClause)
queryArgs := append(append([]any{}, args...), searchArgs...)
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) { func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 { if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil return 0, 0, nil
@@ -485,7 +379,6 @@ SELECT
w.name AS destination_warehouse, w.name AS destination_warehouse,
'' AS destination, '' AS destination,
pi.total_qty AS quantity, pi.total_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
COALESCE(p.notes, '') AS notes COALESCE(p.notes, '') AS notes
FROM purchase_items pi FROM purchase_items pi
@@ -534,7 +427,6 @@ SELECT
COALESCE(tw.name, '') AS destination_warehouse, COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination, '' AS destination,
std.usage_qty AS quantity, std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
'Stock Refill' AS notes 'Stock Refill' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -584,7 +476,6 @@ SELECT
COALESCE(tw.name, '') AS destination_warehouse, COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination, '' AS destination,
std.usage_qty AS quantity, std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
'Transfer to other unit' AS notes 'Transfer to other unit' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -631,15 +522,13 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category, ), '') AS product_sub_category,
w.name AS source_warehouse, w.name AS source_warehouse,
COALESCE(c.name, '') AS destination_warehouse, 'RETAIL CUSTOMER' AS destination_warehouse,
'' AS destination, '' AS destination,
mp.qty AS quantity, mp.qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
m.notes AS notes m.notes AS notes
FROM marketing_products mp FROM marketing_products mp
JOIN marketings m ON m.id = mp.marketing_id 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 product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN products prod ON prod.id = pw.product_id JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id JOIN uoms u ON u.id = prod.uom_id
@@ -1020,50 +909,17 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
COALESCE(p.product_price, 0) AS price COALESCE(p.product_price, 0) AS price
`). `).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). 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 product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id"). Joins("JOIN products p ON p.id = std.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("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll) Where("f.name IN ?", sapronakFlagsAll)
incoming, err := scanAndGroupDetails(incomingQuery) incoming, err := scanAndGroupDetails(incomingQuery)
if err != nil { if err != nil {
return nil, nil, err 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 laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.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").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
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)
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). outgoingQuery := r.withCtx(ctx).
Table("stock_allocations AS sa"). Table("stock_allocations AS sa").
Select(` Select(`
@@ -1080,13 +936,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). 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 product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.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"). Joins("JOIN products p ON p.id = std.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("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID). 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). Where("f.name IN ?", sapronakFlagsAll).
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price") Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
outgoing, err := scanAndGroupDetails(outgoingQuery) outgoing, err := scanAndGroupDetails(outgoingQuery)
@@ -1094,71 +947,9 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
return nil, nil, err 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_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
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").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("sa.status = ?", entity.StockAllocationStatusActive).
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("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
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 return incoming, outgoing, nil
} }
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
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 warehouses w ON w.id = pw.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("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
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")
return scanAndGroupDetails(query)
}
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
if len(productIDs) == 0 { if len(productIDs) == 0 {
return []entity.Product{}, nil return []entity.Product{}, nil
-1
View File
@@ -30,7 +30,6 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang) route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject) route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak) route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
route.Get("/:projectFlockId/sapronak/summary", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronakSummary)
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP) route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang) route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi) route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
@@ -40,7 +40,6 @@ type ClosingService interface {
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
} }
@@ -354,7 +353,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
ProjectFlockKandangIDs: projectFlockKandangIDs, ProjectFlockKandangIDs: projectFlockKandangIDs,
Limit: params.Limit, Limit: params.Limit,
Offset: offset, Offset: offset,
Search: params.Search,
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err) s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
@@ -389,74 +387,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
return items, totalResults, nil return items, totalResults, nil
} }
func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if params == nil {
params = &validation.ClosingSapronakQuery{}
}
if err := s.Validate.Struct(params); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
return nil, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
}
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
}
rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{
Type: params.Type,
WarehouseIDs: warehouseIDs,
ProjectFlockKandangIDs: projectFlockKandangIDs,
Search: params.Search,
})
if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data")
}
items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, dto.ClosingSapronakSummaryItemDTO{
Category: row.Category,
TotalQty: row.TotalQty,
Uom: dto.UomSummaryDTO{
ID: row.UomID,
Name: row.UomName,
},
})
}
return items, nil
}
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) { func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
var kandangIDs []uint var kandangIDs []uint
db := s.Repository.DB().WithContext(ctx) db := s.Repository.DB().WithContext(ctx)
@@ -930,19 +860,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
if !isGrowing { if !isGrowing {
if targetAverages.HenDayCount > 0 { if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = henDayAct performance.HenDayAct = &henDayAct
} }
if targetAverages.HenHouseCount > 0 { if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = henHouseAct performance.HenHouseAct = &henHouseAct
} }
if targetAverages.EggWeightCount > 0 { if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = eggWeight performance.EggWeight = &eggWeight
} }
if targetAverages.EggMassCount > 0 { if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg eggMass := targetAverages.EggMassAvg
performance.EggMass = eggMass performance.EggMass = &eggMass
} }
} }
performance.DeffFcr = performance.FcrStd - performance.FcrAct performance.DeffFcr = performance.FcrStd - performance.FcrAct
@@ -1100,3 +1030,4 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
return closest.Mortality, closest.FcrNumber return closest.Mortality, closest.FcrNumber
} }
@@ -2,7 +2,6 @@ package service
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@@ -263,7 +262,6 @@ type sapronakDetailMaps struct {
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 TransferOut map[uint][]dto.SapronakDetailDTO
SalesOut map[uint][]dto.SapronakDetailDTO
} }
func buildSapronakDetails( func buildSapronakDetails(
@@ -273,7 +271,6 @@ func buildSapronakDetails(
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, transferOutRows map[uint][]repository.SapronakDetailRow,
salesOutRows map[uint][]repository.SapronakDetailRow,
) sapronakDetailMaps { ) sapronakDetailMaps {
result := sapronakDetailMaps{ result := sapronakDetailMaps{
Incoming: make(map[uint][]dto.SapronakDetailDTO), Incoming: make(map[uint][]dto.SapronakDetailDTO),
@@ -282,7 +279,6 @@ func buildSapronakDetails(
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), TransferOut: make(map[uint][]dto.SapronakDetailDTO),
SalesOut: 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) {
@@ -315,7 +311,6 @@ func buildSapronakDetails(
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) addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
return result return result
} }
@@ -355,10 +350,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.KandangId)
if err != nil {
return nil, nil, 0, 0, err
}
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter)) filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
matchesFlag := func(f string) bool { matchesFlag := func(f string) bool {
@@ -371,34 +362,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
return candidate == filterFlag return candidate == filterFlag
} }
dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
result := make(map[uint][]dto.SapronakDetailDTO, len(src))
seen := make(map[string]struct{})
for pid, rows := range src {
for _, d := range rows {
dateKey := ""
if d.Tanggal != nil {
dateKey = d.Tanggal.Format("2006-01-02")
}
qtyKey := d.QtyMasuk
if qtyKey == 0 {
qtyKey = d.QtyKeluar
}
ref := strings.TrimSpace(d.NoReferensi)
key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
if ref == "" {
key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result[pid] = append(result[pid], d)
}
}
return result
}
// For project flocks with category GROWING, pullet usage from chickin // For project flocks with category GROWING, pullet usage from chickin
// should not be counted yet. Only when category is LAYING we allow // should not be counted yet. Only when category is LAYING we allow
@@ -437,17 +400,13 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...) usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
} }
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows) 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 transOutgoing := detailMaps.TransferOut
salesOutgoing := detailMaps.SalesOut
transIncoming = dedupTransfers(transIncoming)
transOutgoing = dedupTransfers(transOutgoing)
ensureGroup := func(flag string) *dto.SapronakGroupDTO { ensureGroup := func(flag string) *dto.SapronakGroupDTO {
if g, ok := groupMap[flag]; ok { if g, ok := groupMap[flag]; ok {
@@ -724,25 +683,6 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
} }
for productID, details := range salesOutgoing {
flag, name := resolveFlagName(productID, details)
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
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)
@@ -24,5 +24,4 @@ type ClosingSapronakQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"` KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
} }
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
if err != nil { if err != nil {
return "", err return "", err
} }
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq) movementNumber := fmt.Sprintf("ST-%05d", seq)
return movementNumber, nil return movementNumber, nil
} }
@@ -28,7 +28,6 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
} }
if projectFlockID > 0 { if projectFlockID > 0 {
query.ProjectFlockKandangId = uint(projectFlockID) query.ProjectFlockKandangId = uint(projectFlockID)
@@ -53,13 +53,6 @@ type RecordingLocationDTO struct {
Address string `json:"address"` Address string `json:"address"`
} }
type RecordingKandangDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Capacity float64 `json:"capacity"`
}
type RecordingWarehouseDTO struct { type RecordingWarehouseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -89,14 +82,12 @@ type RecordingListDTO struct {
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Kandang *RecordingKandangDTO `json:"kandang,omitempty"` Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
} }
type RecordingDetailDTO struct { type RecordingDetailDTO struct {
RecordingListDTO RecordingListDTO
ProductCategory string `json:"product_category"` ProductCategory string `json:"product_category"`
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
Depletions []RecordingDepletionDTO `json:"depletions"` Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"` Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"` Eggs []RecordingEggDTO `json:"eggs"`
@@ -143,7 +134,6 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
return RecordingDetailDTO{ return RecordingDetailDTO{
RecordingListDTO: listDTO, RecordingListDTO: listDTO,
ProductCategory: recordingProductCategory(e), ProductCategory: recordingProductCategory(e),
Warehouse: recordingWarehouseDTO(e),
Depletions: ToRecordingDepletionDTOs(e.Depletions), Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks), Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: ToRecordingEggDTOs(e.Eggs), Eggs: ToRecordingEggDTOs(e.Eggs),
@@ -212,8 +202,7 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO {
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser, CreatedUser: createdUser,
Kandang: recordingKandangDTO(e), Warehouse: recordingWarehouseDTO(e),
Location: recordingKandangLocationDTO(e),
} }
} }
@@ -332,34 +321,6 @@ func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
return mapWarehouseDTO(&pw.Warehouse) return mapWarehouseDTO(&pw.Warehouse)
} }
func recordingKandangDTO(e entity.Recording) *RecordingKandangDTO {
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
return nil
}
kandang := e.ProjectFlockKandang.Kandang
return &RecordingKandangDTO{
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
Capacity: kandang.Capacity,
}
}
func recordingKandangLocationDTO(e entity.Recording) *RecordingLocationDTO {
if e.ProjectFlockKandang == nil || e.ProjectFlockKandang.Kandang.Id == 0 {
return nil
}
location := e.ProjectFlockKandang.Kandang.Location
if location.Id == 0 {
return nil
}
return &RecordingLocationDTO{
Id: location.Id,
Name: location.Name,
Address: location.Address,
}
}
func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse { func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse {
if len(e.Stocks) > 0 { if len(e.Stocks) > 0 {
pw := e.Stocks[0].ProductWarehouse pw := e.Stocks[0].ProductWarehouse
@@ -74,28 +74,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err)) panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
} }
} }
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyRecordingDepletion,
Table: "recording_depletions",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "source_product_warehouse_id",
UsageQuantity: "qty",
PendingQuantity: "pending_qty",
CreatedAt: "id",
},
ExcludedStockables: []fifo.StockableKey{
fifo.StockableKeyTransferToLayingIn,
fifo.StockableKeyStockTransferIn,
fifo.StockableKeyAdjustmentIn,
fifo.StockableKeyPurchaseItems,
fifo.StockableKeyRecordingEgg,
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording depletion usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -17,7 +17,6 @@ type RecordingRepository interface {
repository.BaseRepository[entity.Recording] repository.BaseRepository[entity.Recording]
WithRelations(db *gorm.DB) *gorm.DB WithRelations(db *gorm.DB) *gorm.DB
ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
@@ -25,7 +24,6 @@ type RecordingRepository interface {
DeleteStocks(tx *gorm.DB, recordingID uint) error DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
DeleteDepletions(tx *gorm.DB, recordingID uint) error DeleteDepletions(tx *gorm.DB, recordingID uint) error
@@ -86,7 +84,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("CreatedUser"). Preload("CreatedUser").
Preload("ProjectFlockKandang"). Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.Kandang"). Preload("ProjectFlockKandang.Kandang").
Preload("ProjectFlockKandang.Kandang.Location").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard"). Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Fcr").
@@ -110,42 +107,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("Eggs.ProductWarehouse.Warehouse.Location") Preload("Eggs.ProductWarehouse.Warehouse.Location")
} }
func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
if normalized == "" {
return db
}
likeQuery := "%" + normalized + "%"
subQuery := db.Session(&gorm.Session{NewDB: true}).
Table("recordings").
Select("recordings.id").
Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id").
Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
Joins("LEFT JOIN kandangs k ON k.id = pfk.kandang_id").
Joins("LEFT JOIN locations l ON l.id = k.location_id").
Joins("LEFT JOIN recording_stocks rs ON rs.recording_id = recordings.id").
Joins("LEFT JOIN recording_depletions rd ON rd.recording_id = recordings.id").
Joins("LEFT JOIN recording_eggs re ON re.recording_id = recordings.id").
Joins("LEFT JOIN product_warehouses pws ON pws.id = rs.product_warehouse_id").
Joins("LEFT JOIN product_warehouses pwd ON pwd.id = rd.product_warehouse_id").
Joins("LEFT JOIN product_warehouses pwe ON pwe.id = re.product_warehouse_id").
Joins("LEFT JOIN warehouses ws ON ws.id = pws.warehouse_id").
Joins("LEFT JOIN warehouses wd ON wd.id = pwd.warehouse_id").
Joins("LEFT JOIN warehouses we ON we.id = pwe.warehouse_id").
Where(`
LOWER(pf.flock_name) LIKE ?
OR LOWER(k.name) LIKE ?
OR LOWER(l.name) LIKE ?
OR LOWER(l.address) LIKE ?
OR LOWER(ws.name) LIKE ?
OR LOWER(wd.name) LIKE ?
OR LOWER(we.name) LIKE ?`,
likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery,
)
return db.Where("recordings.id IN (?)", subQuery)
}
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) { func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
if projectFlockKandangId == 0 { if projectFlockKandangId == 0 {
return nil, errors.New("project_flock_kandang_id is required") return nil, errors.New("project_flock_kandang_id is required")
@@ -206,12 +167,6 @@ func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, us
}).Error }).Error
} }
func (r *RecordingRepositoryImpl) UpdateDepletionPending(tx *gorm.DB, depletionID uint, pendingQty float64) error {
return tx.Model(&entity.RecordingDepletion{}).
Where("id = ?", depletionID).
Update("pending_qty", pendingQty).Error
}
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error { func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
@@ -367,25 +322,38 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
} }
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
var result struct { var rows []struct {
TotalQty float64 TotalQty float64
UomName string
} }
if err := tx. if err := tx.
Table("recording_stocks"). Table("recording_stocks").
Select("COALESCE(SUM(COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0)), 0) AS total_qty"). Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id"). Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN uoms ON uoms.id = products.uom_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN"). Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
Where("recording_stocks.recording_id = ?", recordingID). Where("recording_stocks.recording_id = ?", recordingID).
Scan(&result).Error; err != nil { Scan(&rows).Error; err != nil {
return 0, err return 0, err
} }
if result.TotalQty <= 0 { var total float64
return 0, nil for _, row := range rows {
if row.TotalQty <= 0 {
continue
} }
return result.TotalQty * 1000, nil switch strings.TrimSpace(row.UomName) {
case "kilogram", "kg", "kilograms", "kilo":
total += row.TotalQty * 1000
case "gram", "g", "grams":
total += row.TotalQty
default:
total += row.TotalQty
}
}
return total, nil
} }
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) { func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
@@ -44,7 +44,6 @@ type RecordingFIFOIntegrationService interface {
} }
var recordingStockUsableKey = fifo.UsableKeyRecordingStock var recordingStockUsableKey = fifo.UsableKeyRecordingStock
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
type recordingService struct { type recordingService struct {
Log *logrus.Logger Log *logrus.Logger
@@ -117,8 +116,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
if params.ProjectFlockKandangId != 0 { if params.ProjectFlockKandangId != 0 {
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId) db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
} }
db = s.Repository.ApplySearchFilters(db, params.Search) return db.Order("record_datetime DESC").Order("created_at DESC")
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
}) })
if err != nil { if err != nil {
@@ -211,6 +209,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if !isLaying && len(req.Eggs) > 0 { if !isLaying && len(req.Eggs) > 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
} }
if isLaying && len(req.Eggs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
}
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil { if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
return nil, err return nil, err
@@ -279,24 +280,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
} }
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions) mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
if err != nil {
return err
}
for i := range mappedDepletions {
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
}
}
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
s.Log.Errorf("Failed to persist depletions: %+v", err) s.Log.Errorf("Failed to persist depletions: %+v", err)
return err return err
} }
if s.FifoSvc != nil {
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil {
return err
}
}
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs) mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs)
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
@@ -310,7 +297,11 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
} }
var warehouseDeltas map[uint]float64 var warehouseDeltas map[uint]float64
if s.FifoSvc != nil {
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, nil)
} else {
warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs) warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses: %+v", err) s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
return err return err
@@ -416,6 +407,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if !isLaying && len(req.Eggs) > 0 { if !isLaying && len(req.Eggs) > 0 {
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
} }
if isLaying && len(req.Eggs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks")
}
} }
if hasStockChanges { if hasStockChanges {
@@ -437,38 +431,17 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
if hasDepletionChanges { if hasDepletionChanges {
if s.FifoSvc != nil {
if err := s.releaseRecordingDepletions(ctx, tx, existingDepletions); err != nil {
return err
}
}
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil { if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear depletions: %+v", err) s.Log.Errorf("Failed to clear depletions: %+v", err)
return err return err
} }
mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions) mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions)
if s.FifoSvc != nil && len(mappedDepletions) > 0 {
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
if err != nil {
return err
}
for i := range mappedDepletions {
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
}
}
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
s.Log.Errorf("Failed to update depletions: %+v", err) s.Log.Errorf("Failed to update depletions: %+v", err)
return err return err
} }
if s.FifoSvc != nil {
if err := s.consumeRecordingDepletions(ctx, tx, mappedDepletions); err != nil {
return err
}
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err) s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
return err return err
@@ -674,11 +647,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
s.Log.Errorf("Failed to list depletions before delete: %+v", err) s.Log.Errorf("Failed to list depletions before delete: %+v", err)
return err return err
} }
if s.FifoSvc != nil {
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions); err != nil {
return err
}
}
oldEggs, err := s.Repository.ListEggs(tx, id) oldEggs, err := s.Repository.ListEggs(tx, id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -797,46 +765,6 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
return nil return nil
} }
func (s *recordingService) consumeRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 || s.FifoSvc == nil {
return nil
}
for _, depletion := range depletions {
if depletion.Id == 0 {
continue
}
sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil {
sourceWarehouseID = *depletion.SourceProductWarehouseId
}
if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
}
desired := depletion.Qty + depletion.PendingQty
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
ProductWarehouseID: sourceWarehouseID,
Quantity: desired,
AllowPending: false,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.consumeRecordingStocks(ctx, tx, stocks) return s.consumeRecordingStocks(ctx, tx, stocks)
} }
@@ -868,67 +796,10 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
return nil return nil
} }
func (s *recordingService) releaseRecordingDepletions(ctx context.Context, tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 || s.FifoSvc == nil {
return nil
}
for _, depletion := range depletions {
if depletion.Id == 0 {
continue
}
sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil {
sourceWarehouseID = *depletion.SourceProductWarehouseId
}
if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error { func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.releaseRecordingStocks(ctx, tx, stocks) return s.releaseRecordingStocks(ctx, tx, stocks)
} }
func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) {
if projectFlockKandangID == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
}
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to fetch populations for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data populasi")
}
for _, pop := range populations {
if pop.ProductWarehouseId > 0 && pop.TotalQty > 0 {
return pop.ProductWarehouseId, nil
}
}
for _, pop := range populations {
if pop.ProductWarehouseId > 0 {
return pop.ProductWarehouseId, nil
}
}
return 0, fiber.NewError(fiber.StatusBadRequest, "Source product warehouse populasi tidak ditemukan")
}
func buildWarehouseDeltas( func buildWarehouseDeltas(
oldDepletions, newDepletions []entity.RecordingDepletion, oldDepletions, newDepletions []entity.RecordingDepletion,
oldEggs, newEggs []entity.RecordingEgg, oldEggs, newEggs []entity.RecordingEgg,
@@ -1070,8 +941,10 @@ func (s *recordingService) syncRecordingStocks(
desired := item.Qty desired := item.Qty
stock.UsageQty = &desired stock.UsageQty = &desired
zero := 0.0 if item.PendingQty != nil {
stock.PendingQty = &zero pending := *item.PendingQty
stock.PendingQty = &pending
}
stocksToConsume = append(stocksToConsume, stock) stocksToConsume = append(stocksToConsume, stock)
} }
@@ -1117,20 +990,43 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
} }
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
hasPending := false
for _, item := range incoming {
if item.PendingQty != nil {
hasPending = true
break
}
}
existingUsage := make(map[uint]float64) existingUsage := make(map[uint]float64)
existingTotal := make(map[uint]float64)
for _, stock := range existing { for _, stock := range existing {
var usage float64 var usage float64
var pending float64
if stock.UsageQty != nil { if stock.UsageQty != nil {
usage = *stock.UsageQty usage = *stock.UsageQty
} }
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
existingUsage[stock.ProductWarehouseId] += usage existingUsage[stock.ProductWarehouseId] += usage
existingTotal[stock.ProductWarehouseId] += usage + pending
} }
incomingUsage := make(map[uint]float64) incomingUsage := make(map[uint]float64)
incomingTotal := make(map[uint]float64)
for _, item := range incoming { for _, item := range incoming {
var pending float64
if item.PendingQty != nil {
pending = *item.PendingQty
}
incomingUsage[item.ProductWarehouseId] += item.Qty incomingUsage[item.ProductWarehouseId] += item.Qty
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
} }
if hasPending {
return floatMapsMatch(existingTotal, incomingTotal)
}
return floatMapsMatch(existingUsage, incomingUsage) return floatMapsMatch(existingUsage, incomingUsage)
} }
@@ -1328,7 +1224,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggMass float64 var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 { if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMass = totalEggWeightGrams / remainingChick eggMass = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mass"] = eggMass updates["egg_mass"] = eggMass
recording.EggMass = &eggMass recording.EggMass = &eggMass
} else { } else {
@@ -1338,7 +1234,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggWeight float64 var eggWeight float64
if totalEggQty > 0 && totalEggWeightGrams > 0 { if totalEggQty > 0 && totalEggWeightGrams > 0 {
eggWeight = totalEggWeightGrams / totalEggQty eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
updates["egg_weight"] = eggWeight updates["egg_weight"] = eggWeight
recording.EggWeight = &eggWeight recording.EggWeight = &eggWeight
} else { } else {
@@ -4,6 +4,7 @@ type (
Stock struct { Stock struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Qty float64 `json:"qty" validate:"required,gte=0"` Qty float64 `json:"qty" validate:"required,gte=0"`
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
} }
Depletion struct { Depletion struct {
@@ -22,7 +23,7 @@ type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"` RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Stocks []Stock `json:"stocks" validate:"dive"` Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions" validate:"dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"` Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
} }
@@ -36,7 +37,6 @@ type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
Search string `query:"search" validate:"omitempty,max=50"`
} }
type Approve struct { type Approve struct {
@@ -345,52 +345,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
); err != nil { ); err != nil {
return nil, err return nil, err
} }
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil {
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
return nil, err
}
category := strings.TrimSpace(pfk.ProjectFlock.Category)
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
if strings.TrimSpace(standard.ProjectCategory) != "" {
category = standard.ProjectCategory
}
}
}
weekBase := 1
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
weekBase = 18
}
if req.Week < weekBase {
if weekBase == 18 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
}
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
}
var latestWeek int
if err := s.Repository.DB().WithContext(c.Context()).
Model(&entity.ProjectFlockKandangUniformity{}).
Where("project_flock_kandang_id = ? AND deleted_at IS NULL", req.ProjectFlockKandangId).
Select("COALESCE(MAX(week), 0)").
Scan(&latestWeek).Error; err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence")
}
if latestWeek == 0 && req.Week != weekBase {
if weekBase == 18 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
}
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
}
if latestWeek > 0 && req.Week > latestWeek+1 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
}
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week); err != nil {
return nil, err return nil, err
} }
@@ -532,35 +487,8 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
if req.ProjectFlockKandangId != nil { if req.ProjectFlockKandangId != nil {
targetPFKID = *req.ProjectFlockKandangId targetPFKID = *req.ProjectFlockKandangId
} }
if targetPFKID != 0 && targetWeek > 0 {
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetPFKID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
}
return nil, err
}
category := strings.TrimSpace(pfk.ProjectFlock.Category)
if s.ProductionStandardRepo != nil && pfk.ProjectFlock.ProductionStandardId != 0 {
if standard, err := s.ProductionStandardRepo.GetByID(c.Context(), pfk.ProjectFlock.ProductionStandardId, nil); err == nil {
if strings.TrimSpace(standard.ProjectCategory) != "" {
category = standard.ProjectCategory
}
}
}
weekBase := 1
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
weekBase = 18
}
if targetWeek < weekBase {
if weekBase == 18 {
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects")
}
return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects")
}
}
if targetDate != nil { if targetDate != nil {
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek); err != nil { if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil {
return nil, err return nil, err
} }
} }
@@ -676,7 +604,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
return s.GetOne(c, id) return s.GetOne(c, id)
} }
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int) error { func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error {
if projectFlockKandangID == 0 || week == 0 { if projectFlockKandangID == 0 || week == 0 {
return nil return nil
} }
@@ -122,6 +122,11 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
rows := make([]PurchaseSupplierRowDTO, 0, len(items)) rows := make([]PurchaseSupplierRowDTO, 0, len(items))
summary := PurchaseSupplierSummaryDTO{} summary := PurchaseSupplierSummaryDTO{}
var unitPriceSum float64
var unitPriceCount int
var transportUnitPriceSum float64
var transportUnitPriceCount int
for i := range items { for i := range items {
row := ToPurchaseSupplierRowDTO(&items[i]) row := ToPurchaseSupplierRowDTO(&items[i])
rows = append(rows, row) rows = append(rows, row)
@@ -131,16 +136,19 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
summary.TotalTransportValue += row.TransportValue summary.TotalTransportValue += row.TransportValue
summary.TotalAmount += row.TotalAmount summary.TotalAmount += row.TotalAmount
unitPriceSum += row.UnitPrice
unitPriceCount++
transportUnitPriceSum += row.TransportUnitPrice
transportUnitPriceCount++
} }
if summary.TotalQty > 0 { if unitPriceCount > 0 {
avg := summary.TotalPurchaseValue / summary.TotalQty summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount))
summary.TotalUnitPrice = math.Round(avg)
} }
if summary.TotalQty > 0 { if transportUnitPriceCount > 0 {
avg := summary.TotalTransportValue / summary.TotalQty summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount))
summary.TotalTransportUnitPrice = math.Round(avg)
} }
return PurchaseSupplierDTO{ return PurchaseSupplierDTO{
@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
@@ -18,8 +17,6 @@ type DebtSupplierRepository interface {
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error) GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error) GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
} }
@@ -28,11 +25,6 @@ type debtSupplierRepositoryImpl struct {
db *gorm.DB db *gorm.DB
} }
type PaymentReferenceSummary struct {
Total float64
LatestPaymentDate time.Time
}
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository { func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
return &debtSupplierRepositoryImpl{db: db} return &debtSupplierRepositoryImpl{db: db}
} }
@@ -175,8 +167,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context,
Model(&entity.Payment{}). Model(&entity.Payment{}).
Where("party_type = ?", string(utils.PaymentPartySupplier)). Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT"). Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs). Where("party_id IN ?", supplierIDs)
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal))
if strings.TrimSpace(filters.StartDate) != "" { if strings.TrimSpace(filters.StartDate) != "" {
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
@@ -247,7 +238,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
Where("direction = ?", "OUT"). Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs). Where("party_id IN ?", supplierIDs).
Where("reference_number IN ?", references). Where("reference_number IN ?", references).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
Group("reference_number"). Group("reference_number").
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
return nil, err return nil, err
@@ -264,75 +254,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
return result, nil return result, nil
} }
func (r *debtSupplierRepositoryImpl) GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error) {
if len(supplierIDs) == 0 || len(references) == 0 {
return map[string]PaymentReferenceSummary{}, nil
}
type paymentRow struct {
ReferenceNumber *string `gorm:"column:reference_number"`
Total float64 `gorm:"column:total"`
LatestPaymentDate time.Time `gorm:"column:latest_payment_date"`
}
rows := make([]paymentRow, 0)
if err := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("reference_number, SUM(nominal) AS total, MAX(payment_date) AS latest_payment_date").
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs).
Where("reference_number IN ?", references).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
Group("reference_number").
Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[string]PaymentReferenceSummary, len(rows))
for _, row := range rows {
if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" {
continue
}
result[*row.ReferenceNumber] = PaymentReferenceSummary{
Total: row.Total,
LatestPaymentDate: row.LatestPaymentDate,
}
}
return result, nil
}
func (r *debtSupplierRepositoryImpl) GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error) {
if len(supplierIDs) == 0 {
return map[uint]float64{}, nil
}
type balanceRow struct {
SupplierID uint `gorm:"column:supplier_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]balanceRow, 0)
if err := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS supplier_id, SUM(nominal) AS total").
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("party_id IN ?", supplierIDs).
Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)).
Group("party_id").
Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, row := range rows {
result[row.SupplierID] = row.Total
}
return result, nil
}
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) { func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" { if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
return map[uint]float64{}, nil return map[uint]float64{}, nil
@@ -392,7 +313,6 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
Where("party_type = ?", string(utils.PaymentPartySupplier)). Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT"). Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs). Where("party_id IN ?", supplierIDs).
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
Where("DATE(payment_date) < ?", dateFrom). Where("DATE(payment_date) < ?", dateFrom).
Group("party_id"). Group("party_id").
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
@@ -25,21 +25,6 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository {
return &purchaseSupplierRepositoryImpl{db: db} return &purchaseSupplierRepositoryImpl{db: db}
} }
func (r *purchaseSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB {
return r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.step_number, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowPurchase),
)
}
func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB {
dateColumn := "purchase_items.received_date" dateColumn := "purchase_items.received_date"
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
@@ -49,16 +34,10 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context,
dateColumn = "purchase_items.received_date" dateColumn = "purchase_items.received_date"
} }
latestApproval := r.latestPurchaseApproval(ctx)
db := r.db.WithContext(ctx). db := r.db.WithContext(ctx).
Model(&entity.Supplier{}). Model(&entity.Supplier{}).
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id")
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", latestApproval).
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Where("purchase_items.received_date IS NOT NULL")
if filters.SupplierId > 0 { if filters.SupplierId > 0 {
db = db.Where("suppliers.id = ?", filters.SupplierId) db = db.Where("suppliers.id = ?", filters.SupplierId)
@@ -173,11 +152,7 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context
Preload("ExpenseNonstock.Expense"). Preload("ExpenseNonstock.Expense").
Preload("ExpenseNonstock.Expense.Supplier"). Preload("ExpenseNonstock.Expense.Supplier").
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id"). Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("purchases.supplier_id IN ?", supplierIDs)
Where("purchases.supplier_id IN ?", supplierIDs).
Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Where("purchase_items.received_date IS NOT NULL")
if filters.ProductId > 0 { if filters.ProductId > 0 {
db = db.Where("purchase_items.product_id = ?", filters.ProductId) db = db.Where("purchase_items.product_id = ?", filters.ProductId)
@@ -1129,17 +1129,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
return nil, 0, err return nil, 0, err
} }
initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
if err != nil {
return nil, 0, err
}
references := collectDebtSupplierReferences(purchases)
paymentSummaries, err := s.DebtSupplierRepo.GetPaymentSummariesByReferences(c.Context(), supplierIDs, references)
if err != nil {
return nil, 0, err
}
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
if err != nil { if err != nil {
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
@@ -1161,7 +1150,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
continue continue
} }
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]) initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
items := purchasesBySupplier[supplierID] items := purchasesBySupplier[supplierID]
paymentItems := paymentsBySupplier[supplierID] paymentItems := paymentsBySupplier[supplierID]
total := dto.DebtSupplierTotalDTO{} total := dto.DebtSupplierTotalDTO{}
@@ -1169,16 +1158,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems)) combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
for _, purchase := range items { for _, purchase := range items {
row := buildDebtSupplierRow(purchase, now, location) row := buildDebtSupplierRow(purchase, now, location)
if reference := resolveDebtSupplierReference(purchase); reference != "" {
if summary, ok := paymentSummaries[reference]; ok {
if isDebtSupplierPaid(row.TotalPrice, summary.Total) {
row.Status = "Lunas"
if !summary.LatestPaymentDate.IsZero() {
row.Aging = calculateDebtSupplierAging(purchase, summary.LatestPaymentDate, location)
}
}
}
}
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
combinedRows = append(combinedRows, debtSupplierRowItem{ combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row, Row: row,
@@ -1395,55 +1374,6 @@ func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc
return purchase.CreatedAt.In(loc) return purchase.CreatedAt.In(loc)
} }
func collectDebtSupplierReferences(purchases []entity.Purchase) []string {
if len(purchases) == 0 {
return nil
}
seen := make(map[string]struct{}, len(purchases))
result := make([]string, 0, len(purchases))
for _, purchase := range purchases {
ref := resolveDebtSupplierReference(purchase)
if ref == "" {
continue
}
if _, ok := seen[ref]; ok {
continue
}
seen[ref] = struct{}{}
result = append(result, ref)
}
return result
}
func resolveDebtSupplierReference(purchase entity.Purchase) string {
if purchase.PoNumber != nil {
if ref := strings.TrimSpace(*purchase.PoNumber); ref != "" {
return ref
}
}
if ref := strings.TrimSpace(purchase.PrNumber); ref != "" {
return ref
}
return ""
}
func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool {
if totalPrice <= 0 {
return true
}
return paymentTotal >= totalPrice-0.000001
}
func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int {
prDate := purchase.CreatedAt.In(loc)
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
if stopDate.Before(startDate) {
return 0
}
return int(stopDate.Sub(startDate).Hours() / 24)
}
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) { func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
params, filters, err := s.parseHppPerKandangQuery(ctx) params, filters, err := s.parseHppPerKandangQuery(ctx)
if err != nil { if err != nil {
@@ -70,7 +70,7 @@ type HppPerKandangQuery struct {
type ProductionResultQuery struct { type ProductionResultQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"` ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
} }
-1
View File
@@ -3,7 +3,6 @@ package fifo
const ( const (
// Usable Keys // Usable Keys
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyRecordingDepletion UsableKey = "RECORDING_DEPLETION"
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT" UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
@@ -14,10 +14,15 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto
for _, item := range items { for _, item := range items {
usagePtr := new(float64) usagePtr := new(float64)
*usagePtr = item.Qty *usagePtr = item.Qty
pending := item.PendingQty
if pending == nil {
pending = new(float64)
}
result = append(result, entity.RecordingStock{ result = append(result, entity.RecordingStock{
RecordingId: recordingID, RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId, ProductWarehouseId: item.ProductWarehouseId,
UsageQty: usagePtr, UsageQty: usagePtr,
PendingQty: pending,
}) })
} }
return result return result