mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Merge branch 'development' into 'staging'
Development See merge request mbugroup/lti-api!209
This commit is contained in:
+4
@@ -0,0 +1,4 @@
|
||||
-- Rollback: Remove requested_qty column from laying_transfer_sources table
|
||||
|
||||
ALTER TABLE laying_transfer_sources
|
||||
DROP COLUMN IF EXISTS requested_qty;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-- Add requested_qty column to laying_transfer_sources table
|
||||
-- This field stores the quantity requested by user during create/update
|
||||
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
|
||||
|
||||
ALTER TABLE laying_transfer_sources
|
||||
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE recording_depletions
|
||||
DROP COLUMN IF EXISTS pending_qty,
|
||||
DROP COLUMN IF EXISTS source_product_warehouse_id;
|
||||
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
@@ -11,6 +11,7 @@ type LayingTransferSource struct {
|
||||
LayingTransferId uint `gorm:"index;not null"`
|
||||
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||
ProductWarehouseId *uint `gorm:""`
|
||||
RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
|
||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
|
||||
Note string `gorm:"type:text"`
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package entities
|
||||
|
||||
type RecordingDepletion struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
PendingQty float64 `gorm:"column:pending_qty"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
|
||||
@@ -236,9 +236,8 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
query := &validation.ClosingSapronakQuery{
|
||||
Type: strings.ToLower(c.Query("type")),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Type: strings.ToLower(c.Query("type")),
|
||||
Search: c.Query("search"),
|
||||
}
|
||||
if raw := c.Query("kandang_id"); raw != "" {
|
||||
kandangInt, convErr := strconv.Atoi(raw)
|
||||
@@ -249,10 +248,6 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||
query.KandangID = &kandangUint
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||
}
|
||||
@@ -277,6 +272,51 @@ 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")),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
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.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
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 {
|
||||
param := c.Params("project_flock_id")
|
||||
flag := c.Query("flag", "")
|
||||
|
||||
@@ -114,6 +114,17 @@ type ClosingSapronakDTO struct {
|
||||
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 ===
|
||||
|
||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
@@ -201,18 +212,48 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
switch strings.ToLower(item.JenisTransaksi) {
|
||||
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
if row.UnitPrice == 0 {
|
||||
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":
|
||||
price := row.UnitPrice
|
||||
if price == 0 {
|
||||
price = item.Harga
|
||||
}
|
||||
row.QtyUsed += item.QtyKeluar
|
||||
case "mutasi keluar":
|
||||
row.TotalAmount += item.QtyKeluar * price
|
||||
case "mutasi keluar", "penjualan":
|
||||
price := row.UnitPrice
|
||||
if price == 0 {
|
||||
price = item.Harga
|
||||
}
|
||||
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:
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
}
|
||||
|
||||
if row.QtyIn > 0 {
|
||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||
if row.QtyIn > 0 {
|
||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,8 +274,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
total += r.TotalAmount
|
||||
}
|
||||
avg := 0.0
|
||||
if qtyIn > 0 {
|
||||
avg = total / qtyIn
|
||||
if qtyUsed > 0 {
|
||||
avg = total / qtyUsed
|
||||
}
|
||||
cat.Total = SapronakCategoryTotalDTO{
|
||||
Label: label,
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
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)
|
||||
@@ -33,6 +34,7 @@ type ClosingRepository interface {
|
||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (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)
|
||||
FetchSapronakSales(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||
}
|
||||
|
||||
@@ -59,10 +61,18 @@ type SapronakRow struct {
|
||||
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"`
|
||||
@@ -74,6 +84,7 @@ type SapronakQueryParams struct {
|
||||
ProjectFlockKandangIDs []uint
|
||||
Limit int
|
||||
Offset int
|
||||
Search string
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||
@@ -109,14 +120,36 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
||||
|
||||
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
|
||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause)
|
||||
countArgs := append(append([]any{}, args...), searchArgs...)
|
||||
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||
dataArgs := append(append([]any{}, args...), searchArgs...)
|
||||
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, searchClause)
|
||||
|
||||
var rows []SapronakRow
|
||||
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||
@@ -126,6 +159,79 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
||||
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) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, nil
|
||||
@@ -379,6 +485,7 @@ SELECT
|
||||
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
|
||||
@@ -427,6 +534,7 @@ SELECT
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.usage_qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'Stock Refill' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -476,6 +584,7 @@ SELECT
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.usage_qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'Transfer to other unit' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -522,18 +631,27 @@ SELECT
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
w.name AS source_warehouse,
|
||||
'RETAIL CUSTOMER' AS destination_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')
|
||||
)
|
||||
`
|
||||
)
|
||||
|
||||
@@ -902,17 +1020,50 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
||||
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").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
|
||||
Where("f.name IN ?", sapronakFlagsAll)
|
||||
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 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).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
@@ -929,10 +1080,13 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
||||
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").
|
||||
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("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
|
||||
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
||||
@@ -940,9 +1094,71 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if len(productIDs) == 0 {
|
||||
return []entity.Product{}, nil
|
||||
|
||||
@@ -40,6 +40,7 @@ type ClosingService interface {
|
||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, 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)
|
||||
GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error)
|
||||
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||
}
|
||||
|
||||
@@ -353,6 +354,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||
Limit: params.Limit,
|
||||
Offset: offset,
|
||||
Search: params.Search,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||
@@ -387,6 +389,74 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
|
||||
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) {
|
||||
var kandangIDs []uint
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
@@ -1030,4 +1100,3 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
|
||||
|
||||
return closest.Mortality, closest.FcrNumber
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -111,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
||||
}
|
||||
|
||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||
@@ -262,6 +263,7 @@ type sapronakDetailMaps struct {
|
||||
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||
SalesOut map[uint][]dto.SapronakDetailDTO
|
||||
}
|
||||
|
||||
func buildSapronakDetails(
|
||||
@@ -271,6 +273,7 @@ func buildSapronakDetails(
|
||||
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||
salesOutRows map[uint][]repository.SapronakDetailRow,
|
||||
) sapronakDetailMaps {
|
||||
result := sapronakDetailMaps{
|
||||
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||
@@ -279,6 +282,7 @@ func buildSapronakDetails(
|
||||
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||
TransferIn: 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) {
|
||||
@@ -311,6 +315,7 @@ func buildSapronakDetails(
|
||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -350,6 +355,10 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
if err != nil {
|
||||
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))
|
||||
matchesFlag := func(f string) bool {
|
||||
@@ -362,6 +371,34 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
}
|
||||
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
|
||||
// should not be counted yet. Only when category is LAYING we allow
|
||||
@@ -400,13 +437,17 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||
}
|
||||
|
||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows)
|
||||
incomingDetails := detailMaps.Incoming
|
||||
usageDetails := detailMaps.Usage
|
||||
adjIncoming := detailMaps.AdjIncoming
|
||||
adjOutgoing := detailMaps.AdjOutgoing
|
||||
transIncoming := detailMaps.TransferIn
|
||||
transOutgoing := detailMaps.TransferOut
|
||||
salesOutgoing := detailMaps.SalesOut
|
||||
|
||||
transIncoming = dedupTransfers(transIncoming)
|
||||
transOutgoing = dedupTransfers(transOutgoing)
|
||||
|
||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||
if g, ok := groupMap[flag]; ok {
|
||||
@@ -683,6 +724,25 @@ 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))
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, *g)
|
||||
|
||||
@@ -24,4 +24,5 @@ type ClosingSapronakQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
@@ -396,6 +396,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
updateBody["supplier_id"] = *req.SupplierID
|
||||
}
|
||||
|
||||
if req.Notes != nil {
|
||||
updateBody["notes"] = *req.Notes
|
||||
}
|
||||
|
||||
if req.LocationID != nil {
|
||||
locationID := uint(*req.LocationID)
|
||||
updateBody["location_id"] = locationID
|
||||
@@ -568,20 +572,28 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||
}
|
||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||
|
||||
if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) {
|
||||
|
||||
approvalAction := entity.ApprovalActionUpdated
|
||||
|
||||
previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1
|
||||
|
||||
if previousStep < utils.ExpenseStepPengajuan {
|
||||
previousStep = utils.ExpenseStepPengajuan
|
||||
}
|
||||
|
||||
if _, err := approvalSvcTx.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowExpense,
|
||||
id,
|
||||
utils.ExpenseStepPengajuan,
|
||||
previousStep,
|
||||
&approvalAction,
|
||||
actorID,
|
||||
nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||
|
||||
@@ -31,6 +31,7 @@ type Update struct {
|
||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||
LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"`
|
||||
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||
}
|
||||
|
||||
@@ -4,20 +4,17 @@ import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
type TransferRelationDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
TransferReason string `json:"transfer_reason"`
|
||||
TransferDate string `json:"transfer_date"`
|
||||
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
|
||||
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type WarehouseSimpleDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Id uint64 `json:"id"`
|
||||
MovementNumber string `json:"movement_number"`
|
||||
TransferReason string `json:"transfer_reason"`
|
||||
TransferDate string `json:"transfer_date"`
|
||||
SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"`
|
||||
DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type ProductSimpleDTO struct {
|
||||
@@ -25,16 +22,6 @@ type ProductSimpleDTO struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AreaDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LocationDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SupplierSimpleDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -48,13 +35,6 @@ type DocumentDTO struct {
|
||||
Size float64 `json:"size"`
|
||||
}
|
||||
|
||||
type WarehouseDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location *LocationDTO `json:"location"`
|
||||
Area *AreaDTO `json:"area"`
|
||||
}
|
||||
|
||||
type TransferListDTO struct {
|
||||
TransferRelationDTO
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
@@ -97,16 +77,19 @@ type TransferDeliveryItemDTO struct {
|
||||
}
|
||||
|
||||
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||
var sourceWarehouse *WarehouseDetailDTO
|
||||
var sourceWarehouse *warehouseDTO.WarehouseRelationDTO
|
||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse)
|
||||
sourceWarehouse = &mapped
|
||||
}
|
||||
var destinationWarehouse *WarehouseDetailDTO
|
||||
var destinationWarehouse *warehouseDTO.WarehouseRelationDTO
|
||||
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
|
||||
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
|
||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse)
|
||||
destinationWarehouse = &mapped
|
||||
}
|
||||
return TransferRelationDTO{
|
||||
Id: e.Id,
|
||||
MovementNumber: e.MovementNumber,
|
||||
TransferReason: e.Reason,
|
||||
TransferDate: e.CreatedAt.Format("2006-01-02"),
|
||||
SourceWarehouse: sourceWarehouse,
|
||||
@@ -114,38 +97,6 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func toAreaDTO(a *entity.Area) *AreaDTO {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return &AreaDTO{
|
||||
Id: a.Id,
|
||||
Name: a.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toLocationDTO(l *entity.Location) *LocationDTO {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &LocationDTO{
|
||||
Id: l.Id,
|
||||
Name: l.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
return &WarehouseDetailDTO{
|
||||
Id: w.Id,
|
||||
Name: w.Name,
|
||||
Location: toLocationDTO(w.Location),
|
||||
Area: toAreaDTO(&w.Area),
|
||||
}
|
||||
}
|
||||
|
||||
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||
var createdUser *userDTO.UserRelationDTO
|
||||
if e.CreatedUser != nil {
|
||||
|
||||
@@ -40,6 +40,6 @@ func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
movementNumber := fmt.Sprintf("ST-%05d", seq)
|
||||
movementNumber := fmt.Sprintf("PND-LTI-%05d", seq)
|
||||
return movementNumber, nil
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
|
||||
route := v1.Group("/transfers")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
||||
route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
||||
route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
||||
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
||||
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
||||
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
||||
|
||||
}
|
||||
|
||||
@@ -99,7 +99,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
||||
searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
|
||||
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
|
||||
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
|
||||
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
|
||||
searchTerm, searchTerm, searchTerm)
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -118,9 +122,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id))
|
||||
}
|
||||
|
||||
return transferPtr, nil
|
||||
@@ -136,12 +140,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID))
|
||||
}
|
||||
if sourcePW.Quantity < product.ProductQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
|
||||
}
|
||||
pwIDs = append(pwIDs, sourcePW.Id)
|
||||
}
|
||||
@@ -161,10 +165,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan")
|
||||
}
|
||||
if projectFlockKandang.ClosedAt != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
@@ -192,16 +196,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID))
|
||||
}
|
||||
if supplier.Category != string(utils.SupplierCategoryBOP) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
|
||||
}
|
||||
}
|
||||
|
||||
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer")
|
||||
}
|
||||
|
||||
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
||||
@@ -239,16 +243,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID))
|
||||
}
|
||||
|
||||
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||
)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx := c.Context()
|
||||
@@ -256,14 +260,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set ProjectFlockKandangId hanya jika ada kandang
|
||||
var pfkID *uint
|
||||
if projectFlockKandangID > 0 {
|
||||
pfkID = &projectFlockKandangID
|
||||
}
|
||||
|
||||
destPW = &entity.ProductWarehouse{
|
||||
ProductId: uint(product.ProductID),
|
||||
WarehouseId: uint(req.DestinationWarehouseID),
|
||||
Quantity: 0,
|
||||
ProjectFlockKandangId: &projectFlockKandangID,
|
||||
ProjectFlockKandangId: pfkID,
|
||||
}
|
||||
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +320,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
for _, prod := range item.Products {
|
||||
detail, ok := detailMap[uint64(prod.ProductID)]
|
||||
if !ok {
|
||||
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
|
||||
}
|
||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
||||
StockTransferDeliveryId: delivery.Id,
|
||||
@@ -372,7 +383,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
@@ -381,7 +392,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
"usage_qty": consumeResult.UsageQuantity,
|
||||
"pending_qty": consumeResult.PendingQuantity,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("gagal update usage tracking: %w", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID))
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||
@@ -394,7 +405,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
@@ -402,7 +413,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
Updates(map[string]interface{}{
|
||||
"total_qty": replenishResult.AddedQuantity,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("gagal update total tracking: %w", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +447,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err))
|
||||
}
|
||||
|
||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||
@@ -446,8 +457,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
|
||||
if len(expensePayloads) > 0 {
|
||||
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
|
||||
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,32 +471,26 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
|
||||
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
||||
}
|
||||
|
||||
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
|
||||
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
|
||||
}
|
||||
|
||||
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||
}
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID))
|
||||
}
|
||||
|
||||
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
|
||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
|
||||
}
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif")
|
||||
}
|
||||
|
||||
return uint(projectFlockKandang.Id), nil
|
||||
|
||||
@@ -140,12 +140,21 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
action := entity.ApprovalActionUpdated
|
||||
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
||||
svc := commonSvc.NewApprovalService(approvalRepo)
|
||||
action := entity.ApprovalActionCreated
|
||||
|
||||
for id := range expenseIDs {
|
||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if latestApproval == nil {
|
||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
||||
|
||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" {
|
||||
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||
}
|
||||
|
||||
@@ -148,18 +148,30 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
||||
}
|
||||
|
||||
if filters.WarehouseId > 0 {
|
||||
if filters.WarehouseId > 0 || filters.Search != "" {
|
||||
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
||||
}
|
||||
|
||||
if filters.Search != "" {
|
||||
db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id")
|
||||
}
|
||||
db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id")
|
||||
|
||||
if filters.Search != "" {
|
||||
searchPattern := "%" + filters.Search + "%"
|
||||
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?",
|
||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
db = db.Where(`(
|
||||
marketing_delivery_products.vehicle_number ILIKE ? OR
|
||||
customers.name ILIKE ? OR
|
||||
warehouses.name ILIKE ? OR
|
||||
products.name ILIKE ? OR
|
||||
sales_users.name ILIKE ? OR
|
||||
CONCAT(
|
||||
marketings.so_number,
|
||||
'-',
|
||||
COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''),
|
||||
'-',
|
||||
COALESCE(product_warehouses.warehouse_id::text, '')
|
||||
) ILIKE ?
|
||||
)`,
|
||||
searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
if filters.CustomerId > 0 {
|
||||
@@ -178,6 +190,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||
}
|
||||
|
||||
if filters.AreaId > 0 || filters.LocationId > 0 {
|
||||
db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id")
|
||||
|
||||
if filters.AreaId > 0 {
|
||||
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
|
||||
}
|
||||
|
||||
if filters.LocationId > 0 {
|
||||
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.MarketingType != "" {
|
||||
db = db.Joins("LEFT JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||
Group("marketing_delivery_products.id")
|
||||
@@ -186,7 +211,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
case "ayam":
|
||||
db = db.Where("flags.name IN (?)", []string{
|
||||
string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer),
|
||||
string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati),
|
||||
})
|
||||
case "telur":
|
||||
db = db.Where("flags.name IN (?)", []string{
|
||||
|
||||
@@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
|
||||
newChikins = append(newChikins, newChickin)
|
||||
|
||||
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
||||
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId))
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
|
||||
availableQty := productWarehouse.Quantity - totalPopulationQty
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -53,6 +54,7 @@ type ProjectFlockKandangListDTO struct {
|
||||
ProjectFlockKandangRelationDTO
|
||||
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||
NameWithPeriod string `json:"name_with_period"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||
@@ -104,6 +106,7 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
|
||||
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
||||
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||
Kandang: toKandangRelation(e.Kandang),
|
||||
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
|
||||
CreatedAt: e.CreatedAt,
|
||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||
@@ -126,6 +129,16 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
|
||||
return &mapped
|
||||
}
|
||||
|
||||
func toNameWithPeriod(kandang entity.Kandang, period int) string {
|
||||
if kandang.Name == "" {
|
||||
return ""
|
||||
}
|
||||
if period == 0 {
|
||||
return kandang.Name
|
||||
}
|
||||
return kandang.Name + " Period " + strconv.Itoa(period)
|
||||
}
|
||||
|
||||
func toApprovalDTOSelector(
|
||||
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
|
||||
approval := selector(e)
|
||||
@@ -147,6 +160,7 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
|
||||
ProjectFlockKandangRelationDTO: ToProjectFlockKandangRelationDTO(e),
|
||||
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||
Kandang: toKandangRelation(e.Kandang),
|
||||
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
|
||||
CreatedAt: e.CreatedAt,
|
||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||
|
||||
+22
-1
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -16,6 +17,7 @@ type ProjectFlockPopulationRepository interface {
|
||||
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error)
|
||||
|
||||
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
||||
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||
@@ -111,7 +113,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(c
|
||||
err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Select("COALESCE(SUM(total_qty), 0)").
|
||||
Select("COALESCE(SUM(total_qty - total_used_qty), 0)").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -135,3 +137,22 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) {
|
||||
var total float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("project_flock_populations").
|
||||
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if total < 0 {
|
||||
total = 0
|
||||
}
|
||||
|
||||
return int64(math.Round(total)), nil
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
||||
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search"),
|
||||
}
|
||||
if projectFlockID > 0 {
|
||||
query.ProjectFlockKandangId = uint(projectFlockID)
|
||||
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
// === DTO Structs ===
|
||||
|
||||
type RecordingProjectFlockDTO struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||
FlockName string `json:"flock_name"`
|
||||
ProjectFlockCategory string `json:"project_flock_category"`
|
||||
Period int `json:"period"`
|
||||
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
|
||||
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
|
||||
TotalChickQty float64 `json:"total_chick_qty"`
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||
FlockName string `json:"flock_name"`
|
||||
ProjectFlockCategory string `json:"project_flock_category"`
|
||||
Period int `json:"period"`
|
||||
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
|
||||
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
|
||||
TotalChickQty float64 `json:"total_chick_qty"`
|
||||
}
|
||||
|
||||
type RecordingProductionStandardDTO struct {
|
||||
@@ -53,6 +53,13 @@ type RecordingLocationDTO struct {
|
||||
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 {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -82,12 +89,14 @@ type RecordingListDTO struct {
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
||||
Kandang *RecordingKandangDTO `json:"kandang,omitempty"`
|
||||
Location *RecordingLocationDTO `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingDetailDTO struct {
|
||||
RecordingListDTO
|
||||
ProductCategory string `json:"product_category"`
|
||||
ProductCategory string `json:"product_category"`
|
||||
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
|
||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||
Stocks []RecordingStockDTO `json:"stocks"`
|
||||
Eggs []RecordingEggDTO `json:"eggs"`
|
||||
@@ -133,10 +142,11 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||
|
||||
return RecordingDetailDTO{
|
||||
RecordingListDTO: listDTO,
|
||||
ProductCategory: recordingProductCategory(e),
|
||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||
ProductCategory: recordingProductCategory(e),
|
||||
Warehouse: recordingWarehouseDTO(e),
|
||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +212,8 @@ func toRecordingListDTO(e entity.Recording) RecordingListDTO {
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedUser: createdUser,
|
||||
Warehouse: recordingWarehouseDTO(e),
|
||||
Kandang: recordingKandangDTO(e),
|
||||
Location: recordingKandangLocationDTO(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,20 +225,20 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||
}
|
||||
|
||||
return RecordingRelationDTO{
|
||||
Id: e.Id,
|
||||
ProjectFlock: toRecordingProjectFlockDTO(e),
|
||||
RecordDatetime: e.RecordDatetime,
|
||||
Day: intValue(e.Day),
|
||||
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
||||
CumDepletionRate: floatValue(e.CumDepletionRate),
|
||||
CumIntake: intValue(e.CumIntake),
|
||||
FcrValue: floatValue(e.FcrValue),
|
||||
HenDay: floatValue(e.HenDay),
|
||||
HenHouse: floatValue(e.HenHouse),
|
||||
FeedIntake: floatValue(e.FeedIntake),
|
||||
EggMass: floatValue(e.EggMass),
|
||||
EggWeight: floatValue(e.EggWeight),
|
||||
Approval: latestApproval,
|
||||
Id: e.Id,
|
||||
ProjectFlock: toRecordingProjectFlockDTO(e),
|
||||
RecordDatetime: e.RecordDatetime,
|
||||
Day: intValue(e.Day),
|
||||
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
||||
CumDepletionRate: floatValue(e.CumDepletionRate),
|
||||
CumIntake: intValue(e.CumIntake),
|
||||
FcrValue: floatValue(e.FcrValue),
|
||||
HenDay: floatValue(e.HenDay),
|
||||
HenHouse: floatValue(e.HenHouse),
|
||||
FeedIntake: floatValue(e.FeedIntake),
|
||||
EggMass: floatValue(e.EggMass),
|
||||
EggWeight: floatValue(e.EggWeight),
|
||||
Approval: latestApproval,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +332,34 @@ func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO {
|
||||
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 {
|
||||
if len(e.Stocks) > 0 {
|
||||
pw := e.Stocks[0].ProductWarehouse
|
||||
|
||||
@@ -74,6 +74,28 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
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)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
@@ -17,6 +17,7 @@ type RecordingRepository interface {
|
||||
repository.BaseRepository[entity.Recording]
|
||||
|
||||
WithRelations(db *gorm.DB) *gorm.DB
|
||||
ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB
|
||||
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||
|
||||
@@ -24,6 +25,7 @@ type RecordingRepository interface {
|
||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, 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
|
||||
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||
@@ -84,6 +86,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("CreatedUser").
|
||||
Preload("ProjectFlockKandang").
|
||||
Preload("ProjectFlockKandang.Kandang").
|
||||
Preload("ProjectFlockKandang.Kandang.Location").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||
@@ -107,6 +110,42 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
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) {
|
||||
if projectFlockKandangId == 0 {
|
||||
return nil, errors.New("project_flock_kandang_id is required")
|
||||
@@ -167,6 +206,12 @@ func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, us
|
||||
}).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 {
|
||||
if len(depletions) == 0 {
|
||||
return nil
|
||||
@@ -322,38 +367,25 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var rows []struct {
|
||||
var result struct {
|
||||
TotalQty float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
if err := tx.
|
||||
Table("recording_stocks").
|
||||
Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
|
||||
Select("COALESCE(SUM(COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0)), 0) AS total_qty").
|
||||
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 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").
|
||||
Where("recording_stocks.recording_id = ?", recordingID).
|
||||
Scan(&rows).Error; err != nil {
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, row := range rows {
|
||||
if row.TotalQty <= 0 {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
if result.TotalQty <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return total, nil
|
||||
return result.TotalQty * 1000, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
|
||||
|
||||
@@ -44,6 +44,7 @@ type RecordingFIFOIntegrationService interface {
|
||||
}
|
||||
|
||||
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
|
||||
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
|
||||
|
||||
type recordingService struct {
|
||||
Log *logrus.Logger
|
||||
@@ -116,7 +117,8 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
if params.ProjectFlockKandangId != 0 {
|
||||
db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId)
|
||||
}
|
||||
return db.Order("record_datetime DESC").Order("created_at DESC")
|
||||
db = s.Repository.ApplySearchFilters(db, params.Search)
|
||||
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -209,9 +211,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
if !isLaying && len(req.Eggs) > 0 {
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -280,10 +279,24 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
}
|
||||
|
||||
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 {
|
||||
s.Log.Errorf("Failed to persist depletions: %+v", 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)
|
||||
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
|
||||
@@ -297,11 +310,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
}
|
||||
|
||||
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 {
|
||||
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
|
||||
return err
|
||||
@@ -407,9 +416,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
if !isLaying && len(req.Eggs) > 0 {
|
||||
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 {
|
||||
@@ -431,17 +437,38 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
|
||||
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 {
|
||||
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
s.Log.Errorf("Failed to update depletions: %+v", 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 {
|
||||
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
|
||||
return err
|
||||
@@ -647,6 +674,11 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
s.Log.Errorf("Failed to list depletions before delete: %+v", 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)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -765,6 +797,46 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
|
||||
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 {
|
||||
return s.consumeRecordingStocks(ctx, tx, stocks)
|
||||
}
|
||||
@@ -796,10 +868,67 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
|
||||
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 {
|
||||
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(
|
||||
oldDepletions, newDepletions []entity.RecordingDepletion,
|
||||
oldEggs, newEggs []entity.RecordingEgg,
|
||||
@@ -941,10 +1070,8 @@ func (s *recordingService) syncRecordingStocks(
|
||||
|
||||
desired := item.Qty
|
||||
stock.UsageQty = &desired
|
||||
if item.PendingQty != nil {
|
||||
pending := *item.PendingQty
|
||||
stock.PendingQty = &pending
|
||||
}
|
||||
zero := 0.0
|
||||
stock.PendingQty = &zero
|
||||
stocksToConsume = append(stocksToConsume, stock)
|
||||
}
|
||||
|
||||
@@ -990,43 +1117,20 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error {
|
||||
}
|
||||
|
||||
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)
|
||||
existingTotal := make(map[uint]float64)
|
||||
for _, stock := range existing {
|
||||
var usage float64
|
||||
var pending float64
|
||||
if stock.UsageQty != nil {
|
||||
usage = *stock.UsageQty
|
||||
}
|
||||
if stock.PendingQty != nil {
|
||||
pending = *stock.PendingQty
|
||||
}
|
||||
existingUsage[stock.ProductWarehouseId] += usage
|
||||
existingTotal[stock.ProductWarehouseId] += usage + pending
|
||||
}
|
||||
|
||||
incomingUsage := make(map[uint]float64)
|
||||
incomingTotal := make(map[uint]float64)
|
||||
for _, item := range incoming {
|
||||
var pending float64
|
||||
if item.PendingQty != nil {
|
||||
pending = *item.PendingQty
|
||||
}
|
||||
incomingUsage[item.ProductWarehouseId] += item.Qty
|
||||
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
|
||||
}
|
||||
|
||||
if hasPending {
|
||||
return floatMapsMatch(existingTotal, incomingTotal)
|
||||
}
|
||||
return floatMapsMatch(existingUsage, incomingUsage)
|
||||
}
|
||||
|
||||
@@ -1224,7 +1328,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
|
||||
var eggMass float64
|
||||
if remainingChick > 0 && totalEggWeightGrams > 0 {
|
||||
eggMass = (totalEggWeightGrams / remainingChick) * 1000
|
||||
eggMass = totalEggWeightGrams / remainingChick
|
||||
updates["egg_mass"] = eggMass
|
||||
recording.EggMass = &eggMass
|
||||
} else {
|
||||
@@ -1234,7 +1338,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
|
||||
var eggWeight float64
|
||||
if totalEggQty > 0 && totalEggWeightGrams > 0 {
|
||||
eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
|
||||
eggWeight = totalEggWeightGrams / totalEggQty
|
||||
updates["egg_weight"] = eggWeight
|
||||
recording.EggWeight = &eggWeight
|
||||
} else {
|
||||
|
||||
@@ -2,9 +2,8 @@ package validation
|
||||
|
||||
type (
|
||||
Stock struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
Depletion struct {
|
||||
@@ -20,23 +19,24 @@ type (
|
||||
)
|
||||
|
||||
type Create struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||
Stocks []Stock `json:"stocks" validate:"dive"`
|
||||
Depletions []Depletion `json:"depletions" validate:"dive"`
|
||||
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||
RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||
Stocks []Stock `json:"stocks" validate:"dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" 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"`
|
||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type Approve struct {
|
||||
|
||||
+6
-3
@@ -25,8 +25,12 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer
|
||||
|
||||
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
TransferDate: c.Query("transfer_date", ""),
|
||||
FlockSource: uint(c.QueryInt("flock_source", 0)),
|
||||
FlockDestination: uint(c.QueryInt("flock_destination", 0)),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
@@ -179,7 +183,6 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error {
|
||||
projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32)
|
||||
if err != nil {
|
||||
|
||||
@@ -162,9 +162,19 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse
|
||||
}
|
||||
|
||||
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
|
||||
// Tampilkan requested qty sebelum approve, consumed qty setelah approve
|
||||
var displayQty float64
|
||||
if source.UsageQty > 0 {
|
||||
// Sudah di-approve dan di-consume, tampilkan actual consumed quantity
|
||||
displayQty = source.UsageQty
|
||||
} else {
|
||||
// Belum di-approve, tampilkan requested quantity
|
||||
displayQty = source.RequestedQty
|
||||
}
|
||||
|
||||
return LayingTransferSourceDTO{
|
||||
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
||||
Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity)
|
||||
Qty: displayQty,
|
||||
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
||||
Note: source.Note,
|
||||
}
|
||||
|
||||
@@ -110,8 +110,31 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
// Apply search and filters
|
||||
if params.Search != "" {
|
||||
searchPattern := "%" + params.Search + "%"
|
||||
db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id").
|
||||
Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id").
|
||||
Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?",
|
||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
if params.TransferDate != "" {
|
||||
db = db.Where("transfer_date::date = ?::date", params.TransferDate)
|
||||
}
|
||||
|
||||
if params.FlockSource > 0 {
|
||||
db = db.Where("from_project_flock_id = ?", params.FlockSource)
|
||||
}
|
||||
|
||||
if params.FlockDestination > 0 {
|
||||
db = db.Where("to_project_flock_id = ?", params.FlockDestination)
|
||||
}
|
||||
|
||||
db = db.Order("created_at DESC")
|
||||
|
||||
db = s.withRelations(db)
|
||||
|
||||
return db
|
||||
})
|
||||
|
||||
@@ -216,7 +239,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
|
||||
for _, sourceDetail := range req.SourceKandangs {
|
||||
if sourceDetail.Quantity <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0")
|
||||
continue
|
||||
}
|
||||
totalSourceQty += sourceDetail.Quantity
|
||||
|
||||
@@ -247,11 +270,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
|
||||
for _, targetDetail := range req.TargetKandangs {
|
||||
if targetDetail.Quantity <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0")
|
||||
continue
|
||||
}
|
||||
totalTargetQty += targetDetail.Quantity
|
||||
}
|
||||
|
||||
if totalSourceQty == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0")
|
||||
}
|
||||
if totalTargetQty == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0")
|
||||
}
|
||||
|
||||
if totalSourceQty != totalTargetQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty))
|
||||
}
|
||||
@@ -279,11 +309,16 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
}
|
||||
|
||||
for _, sourceDetail := range req.SourceKandangs {
|
||||
if sourceDetail.Quantity == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
||||
|
||||
source := entity.LayingTransferSource{
|
||||
LayingTransferId: createBody.Id,
|
||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
|
||||
UsageQty: 0,
|
||||
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
||||
ProductWarehouseId: &productWarehouseId,
|
||||
@@ -295,6 +330,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
}
|
||||
|
||||
for _, targetDetail := range req.TargetKandangs {
|
||||
if targetDetail.Quantity == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
@@ -463,8 +501,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
|
||||
source := entity.LayingTransferSource{
|
||||
LayingTransferId: id,
|
||||
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||
RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user
|
||||
UsageQty: 0,
|
||||
PendingUsageQty: sourceDetail.Quantity,
|
||||
PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval
|
||||
ProductWarehouseId: &productWarehouseId,
|
||||
}
|
||||
if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil {
|
||||
@@ -700,7 +739,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber)
|
||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
||||
StockableID: target.Id,
|
||||
@@ -814,15 +853,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project
|
||||
|
||||
kandangAvailableQty := make(map[uint]float64)
|
||||
for _, kandang := range kandangs {
|
||||
|
||||
totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
|
||||
// Gunakan fungsi repository yang sama dengan recording service
|
||||
totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id)
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err)
|
||||
s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err)
|
||||
kandangAvailableQty[kandang.Id] = 0
|
||||
continue
|
||||
}
|
||||
|
||||
kandangAvailableQty[kandang.Id] = totalQty
|
||||
kandangAvailableQty[kandang.Id] = totalAvailable
|
||||
}
|
||||
|
||||
return pf, kandangAvailableQty, nil
|
||||
|
||||
+8
-4
@@ -2,12 +2,12 @@ package validation
|
||||
|
||||
type SourceKandangDetail struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
type TargetKandangDetail struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
type Create struct {
|
||||
@@ -29,8 +29,12 @@ type Update struct {
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,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"`
|
||||
Search string `query:"search" validate:"omitempty"`
|
||||
TransferDate string `query:"transfer_date" validate:"omitempty"`
|
||||
FlockSource uint `query:"flock_source" validate:"omitempty,number"`
|
||||
FlockDestination uint `query:"flock_destination" validate:"omitempty,number"`
|
||||
}
|
||||
|
||||
type Approve struct {
|
||||
|
||||
@@ -345,7 +345,52 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -487,8 +532,35 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
||||
if req.ProjectFlockKandangId != nil {
|
||||
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 err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil {
|
||||
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -604,7 +676,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error {
|
||||
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int) error {
|
||||
if projectFlockKandangID == 0 || week == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -167,12 +167,21 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
action := entity.ApprovalActionUpdated
|
||||
approvalRepo := commonRepo.NewApprovalRepository(b.db)
|
||||
svc := commonSvc.NewApprovalService(approvalRepo)
|
||||
action := entity.ApprovalActionCreated
|
||||
|
||||
for id := range expenseIDs {
|
||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if latestApproval == nil {
|
||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
|
||||
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
||||
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
||||
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
||||
AreaId: int64(ctx.QueryInt("area_id", 0)),
|
||||
LocationId: int64(ctx.QueryInt("location_id", 0)),
|
||||
MarketingType: ctx.Query("marketing_type", ""),
|
||||
FilterBy: ctx.Query("filter_by", ""),
|
||||
StartDate: ctx.Query("start_date", ""),
|
||||
|
||||
@@ -27,10 +27,13 @@ type HppPerKandangResponseData struct {
|
||||
type HppPerKandangRowDTO struct {
|
||||
ID int `json:"id"`
|
||||
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
|
||||
NameWithPeriode string `json:"name_with_periode"`
|
||||
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
|
||||
AvgWeightKg float64 `json:"avg_weight_kg"`
|
||||
EggProductionPieces int64 `json:"egg_production_pieces"`
|
||||
EggProductionKg float64 `json:"egg_production_kg"`
|
||||
// EggProductionTotalWeightKg float64 `json:"egg_production_total_weight_kg"`
|
||||
// EggProductionTotalPieces int64 `json:"egg_production_total_pieces"`
|
||||
// FeedCostRp float64 `json:"feed_cost_rp"`
|
||||
// OvkCostRp float64 `json:"ovk_cost_rp"`
|
||||
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
|
||||
@@ -38,8 +41,8 @@ type HppPerKandangRowDTO struct {
|
||||
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
|
||||
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
|
||||
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
|
||||
HppRp float64 `json:"hpp_rp"`
|
||||
RemainingValueRp int64 `json:"remaining_value_rp"`
|
||||
// HppRp float64 `json:"hpp_rp"`
|
||||
// RemainingValueRp int64 `json:"remaining_value_rp"`
|
||||
}
|
||||
|
||||
type HppPerKandangRowKandangDTO struct {
|
||||
|
||||
@@ -27,12 +27,12 @@ type PurchaseSupplierRowDTO struct {
|
||||
}
|
||||
|
||||
type PurchaseSupplierSummaryDTO struct {
|
||||
TotalQty float64 `json:"total_qty"`
|
||||
TotalPurchaseValue float64 `json:"total_purchase_value"`
|
||||
TotalTransportValue float64 `json:"total_transport_value"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
TotalUnitPrice float64 `json:"total_unit_price"`
|
||||
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
|
||||
TotalQty float64 `json:"total_qty"`
|
||||
TotalPurchaseValue float64 `json:"total_purchase_value"`
|
||||
TotalTransportValue float64 `json:"total_transport_value"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
TotalUnitPrice float64 `json:"total_unit_price"`
|
||||
TotalTransportUnitPrice float64 `json:"total_transport_unit_price"`
|
||||
}
|
||||
|
||||
type PurchaseSupplierDTO struct {
|
||||
@@ -122,11 +122,6 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
|
||||
rows := make([]PurchaseSupplierRowDTO, 0, len(items))
|
||||
summary := PurchaseSupplierSummaryDTO{}
|
||||
|
||||
var unitPriceSum float64
|
||||
var unitPriceCount int
|
||||
var transportUnitPriceSum float64
|
||||
var transportUnitPriceCount int
|
||||
|
||||
for i := range items {
|
||||
row := ToPurchaseSupplierRowDTO(&items[i])
|
||||
rows = append(rows, row)
|
||||
@@ -136,19 +131,16 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem
|
||||
summary.TotalTransportValue += row.TransportValue
|
||||
summary.TotalAmount += row.TotalAmount
|
||||
|
||||
unitPriceSum += row.UnitPrice
|
||||
unitPriceCount++
|
||||
|
||||
transportUnitPriceSum += row.TransportUnitPrice
|
||||
transportUnitPriceCount++
|
||||
}
|
||||
|
||||
if unitPriceCount > 0 {
|
||||
summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount))
|
||||
if summary.TotalQty > 0 {
|
||||
avg := summary.TotalPurchaseValue / summary.TotalQty
|
||||
summary.TotalUnitPrice = math.Round(avg)
|
||||
}
|
||||
|
||||
if transportUnitPriceCount > 0 {
|
||||
summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount))
|
||||
if summary.TotalQty > 0 {
|
||||
avg := summary.TotalTransportValue / summary.TotalQty
|
||||
summary.TotalTransportUnitPrice = math.Round(avg)
|
||||
}
|
||||
|
||||
return PurchaseSupplierDTO{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||
@@ -17,6 +18,8 @@ type DebtSupplierRepository interface {
|
||||
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, 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)
|
||||
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)
|
||||
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||
}
|
||||
@@ -25,6 +28,11 @@ type debtSupplierRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type PaymentReferenceSummary struct {
|
||||
Total float64
|
||||
LatestPaymentDate time.Time
|
||||
}
|
||||
|
||||
func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
||||
return &debtSupplierRepositoryImpl{db: db}
|
||||
}
|
||||
@@ -167,7 +175,8 @@ func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context,
|
||||
Model(&entity.Payment{}).
|
||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||
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 dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||
@@ -238,6 +247,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
||||
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
|
||||
@@ -254,6 +264,75 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
||||
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) {
|
||||
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||
return map[uint]float64{}, nil
|
||||
@@ -313,6 +392,7 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
|
||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||
Where("direction = ?", "OUT").
|
||||
Where("party_id IN ?", supplierIDs).
|
||||
Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
|
||||
Where("DATE(payment_date) < ?", dateFrom).
|
||||
Group("party_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
|
||||
@@ -11,37 +11,40 @@ import (
|
||||
)
|
||||
|
||||
type HppPerKandangRow struct {
|
||||
ProjectFlockKandangID uint
|
||||
KandangID uint
|
||||
KandangName string
|
||||
KandangStatus string
|
||||
LocationID uint
|
||||
LocationName string
|
||||
PicID uint
|
||||
PicName string
|
||||
RecordingCount int64
|
||||
RemainingChickenBirds float64
|
||||
RemainingChickenWeight float64
|
||||
EggProductionWeightKg float64
|
||||
EggProductionPieces float64
|
||||
ProjectFlockKandangID uint
|
||||
ProjectFlockPeriod int
|
||||
KandangID uint
|
||||
KandangName string
|
||||
KandangStatus string
|
||||
LocationID uint
|
||||
LocationName string
|
||||
PicID uint
|
||||
PicName string
|
||||
RecordingCount int64
|
||||
// RemainingChickenBirds float64
|
||||
// RemainingChickenWeight float64
|
||||
EggProductionWeightKgRemaining float64
|
||||
EggProductionPiecesRemaining float64
|
||||
EggProductionTotalWeightKg float64
|
||||
EggProductionTotalPieces float64
|
||||
}
|
||||
|
||||
type HppPerKandangCostRow struct {
|
||||
KandangID uint
|
||||
FeedCost float64
|
||||
OvkCost float64
|
||||
DocCost float64
|
||||
DocQty float64
|
||||
BudgetCost float64
|
||||
ExpenseCost float64
|
||||
ProjectFlockKandangID uint
|
||||
FeedCost float64
|
||||
OvkCost float64
|
||||
DocCost float64
|
||||
DocQty float64
|
||||
BudgetCost float64
|
||||
ExpenseCost float64
|
||||
}
|
||||
|
||||
type HppPerKandangSupplierRow struct {
|
||||
KandangID uint
|
||||
SupplierID uint
|
||||
SupplierName string
|
||||
SupplierAlias string
|
||||
Category string
|
||||
ProjectFlockKandangID uint
|
||||
SupplierID uint
|
||||
SupplierName string
|
||||
SupplierAlias string
|
||||
Category string
|
||||
}
|
||||
|
||||
type HppPerKandangRepository interface {
|
||||
@@ -86,6 +89,7 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
||||
Table("project_flocks AS pf").
|
||||
Select(`
|
||||
pfk.id AS project_flock_kandang_id,
|
||||
pfk.period AS project_flock_period,
|
||||
k.id AS kandang_id,
|
||||
k.name AS kandang_name,
|
||||
k.status AS kandang_status,
|
||||
@@ -97,18 +101,27 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en
|
||||
COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds,
|
||||
0 AS remaining_chicken_weight,
|
||||
0 AS egg_production_weight_kg,
|
||||
0 AS egg_production_pieces`).
|
||||
0 AS egg_production_pieces,
|
||||
0 AS egg_production_total_weight_kg,
|
||||
0 AS egg_production_total_pieces`).
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||
Joins(`
|
||||
LEFT JOIN (
|
||||
SELECT project_flock_kandang_id, MIN(chick_in_date) AS chick_in_date
|
||||
FROM project_chickins
|
||||
GROUP BY project_flock_kandang_id
|
||||
) AS pc ON pc.project_flock_kandang_id = pfk.id`).
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||
Joins("JOIN users AS pic ON pic.id = k.pic_id").
|
||||
Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings).
|
||||
Where("pfk.closed_at IS NULL")
|
||||
Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
|
||||
Where("(pfk.closed_at IS NULL OR ? BETWEEN pc.chick_in_date AND pfk.closed_at)", start)
|
||||
|
||||
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
|
||||
|
||||
query = query.Group("pfk.id, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
||||
Order("k.id ASC")
|
||||
query = query.Group("pfk.id, pfk.period, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
|
||||
Order("pfk.id ASC")
|
||||
|
||||
if err := query.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -139,7 +152,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
query := r.db.WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select(`
|
||||
k.id AS kandang_id,
|
||||
pfk.id AS project_flock_kandang_id,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0)
|
||||
@@ -164,29 +177,29 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
||||
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.record_datetime < ?", end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected))
|
||||
|
||||
query = query.Group("k.id").Order("k.id ASC")
|
||||
query = query.Group("pfk.id").Order("pfk.id ASC")
|
||||
|
||||
if err := query.Scan(&rows).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
docRows := make([]struct {
|
||||
KandangID uint
|
||||
DocCost float64
|
||||
DocQty float64
|
||||
SupplierID *uint
|
||||
SupplierName *string
|
||||
SupplierAlias *string
|
||||
ProjectFlockKandangID uint
|
||||
DocCost float64
|
||||
DocQty float64
|
||||
SupplierID *uint
|
||||
SupplierName *string
|
||||
SupplierAlias *string
|
||||
}, 0)
|
||||
|
||||
docQuery := r.db.WithContext(ctx).
|
||||
Table("project_chickins AS pc").
|
||||
Select(`
|
||||
pfk.kandang_id AS kandang_id,
|
||||
pfk.id AS project_flock_kandang_id,
|
||||
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
|
||||
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
|
||||
s.id AS supplier_id,
|
||||
@@ -199,7 +212,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
||||
Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Group("pfk.kandang_id, s.id, s.name, s.alias")
|
||||
Group("pfk.id, s.id, s.name, s.alias")
|
||||
|
||||
if err := docQuery.Scan(&docRows).Error; err != nil {
|
||||
return nil, nil, err
|
||||
@@ -208,28 +221,28 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
|
||||
for i := range rows {
|
||||
row := rows[i]
|
||||
costMap[row.KandangID] = &rows[i]
|
||||
costMap[row.ProjectFlockKandangID] = &rows[i]
|
||||
}
|
||||
|
||||
docSuppliers := make([]HppPerKandangSupplierRow, 0)
|
||||
docSeen := make(map[uint]map[uint]bool)
|
||||
for _, doc := range docRows {
|
||||
entry, ok := costMap[doc.KandangID]
|
||||
entry, ok := costMap[doc.ProjectFlockKandangID]
|
||||
if !ok {
|
||||
rows = append(rows, HppPerKandangCostRow{
|
||||
KandangID: doc.KandangID,
|
||||
ProjectFlockKandangID: doc.ProjectFlockKandangID,
|
||||
})
|
||||
entry = &rows[len(rows)-1]
|
||||
costMap[doc.KandangID] = entry
|
||||
costMap[doc.ProjectFlockKandangID] = entry
|
||||
}
|
||||
entry.DocCost += doc.DocCost
|
||||
entry.DocQty += doc.DocQty
|
||||
if doc.SupplierID != nil {
|
||||
if docSeen[doc.KandangID] == nil {
|
||||
docSeen[doc.KandangID] = make(map[uint]bool)
|
||||
if docSeen[doc.ProjectFlockKandangID] == nil {
|
||||
docSeen[doc.ProjectFlockKandangID] = make(map[uint]bool)
|
||||
}
|
||||
if !docSeen[doc.KandangID][*doc.SupplierID] {
|
||||
docSeen[doc.KandangID][*doc.SupplierID] = true
|
||||
if !docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] {
|
||||
docSeen[doc.ProjectFlockKandangID][*doc.SupplierID] = true
|
||||
supplierName := ""
|
||||
if doc.SupplierName != nil {
|
||||
supplierName = *doc.SupplierName
|
||||
@@ -239,19 +252,19 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
supplierAlias = *doc.SupplierAlias
|
||||
}
|
||||
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
|
||||
KandangID: doc.KandangID,
|
||||
SupplierID: *doc.SupplierID,
|
||||
SupplierName: supplierName,
|
||||
SupplierAlias: supplierAlias,
|
||||
Category: "DOC",
|
||||
ProjectFlockKandangID: doc.ProjectFlockKandangID,
|
||||
SupplierID: *doc.SupplierID,
|
||||
SupplierName: supplierName,
|
||||
SupplierAlias: supplierAlias,
|
||||
Category: "DOC",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
budgetRows := make([]struct {
|
||||
KandangID uint
|
||||
BudgetCost float64
|
||||
ProjectFlockKandangID uint
|
||||
BudgetCost float64
|
||||
}, 0)
|
||||
|
||||
pfkUsageSub := r.db.
|
||||
@@ -272,7 +285,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
budgetQuery := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs AS pfk").
|
||||
Select(`
|
||||
k.id AS kandang_id,
|
||||
pfk.id AS project_flock_kandang_id,
|
||||
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||
@@ -280,7 +293,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
|
||||
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
|
||||
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
||||
Group("k.id")
|
||||
Group("pfk.id")
|
||||
// budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
|
||||
|
||||
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
|
||||
@@ -288,33 +301,33 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
}
|
||||
|
||||
for _, budget := range budgetRows {
|
||||
entry, ok := costMap[budget.KandangID]
|
||||
entry, ok := costMap[budget.ProjectFlockKandangID]
|
||||
if !ok {
|
||||
rows = append(rows, HppPerKandangCostRow{
|
||||
KandangID: budget.KandangID,
|
||||
ProjectFlockKandangID: budget.ProjectFlockKandangID,
|
||||
})
|
||||
entry = &rows[len(rows)-1]
|
||||
costMap[budget.KandangID] = entry
|
||||
costMap[budget.ProjectFlockKandangID] = entry
|
||||
}
|
||||
entry.BudgetCost += budget.BudgetCost
|
||||
}
|
||||
|
||||
expenseRows := make([]struct {
|
||||
KandangID uint
|
||||
ExpenseCost float64
|
||||
ProjectFlockKandangID uint
|
||||
ExpenseCost float64
|
||||
}, 0)
|
||||
|
||||
expenseQuery := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs AS pfk").
|
||||
Select(`
|
||||
k.id AS kandang_id,
|
||||
pfk.id AS project_flock_kandang_id,
|
||||
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
|
||||
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
|
||||
Where("pfk.id IN (?)", projectFlockKandangIDs).
|
||||
Group("k.id")
|
||||
Group("pfk.id")
|
||||
// expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
|
||||
|
||||
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
|
||||
@@ -322,13 +335,13 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
}
|
||||
|
||||
for _, exp := range expenseRows {
|
||||
entry, ok := costMap[exp.KandangID]
|
||||
entry, ok := costMap[exp.ProjectFlockKandangID]
|
||||
if !ok {
|
||||
rows = append(rows, HppPerKandangCostRow{
|
||||
KandangID: exp.KandangID,
|
||||
ProjectFlockKandangID: exp.ProjectFlockKandangID,
|
||||
})
|
||||
entry = &rows[len(rows)-1]
|
||||
costMap[exp.KandangID] = entry
|
||||
costMap[exp.ProjectFlockKandangID] = entry
|
||||
}
|
||||
entry.ExpenseCost += exp.ExpenseCost
|
||||
}
|
||||
@@ -337,7 +350,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
|
||||
feedQuery := r.db.WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select("DISTINCT k.id AS kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
|
||||
Select("DISTINCT pfk.id AS project_flock_kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN locations AS loc ON loc.id = k.location_id").
|
||||
@@ -349,7 +362,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
|
||||
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
||||
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.record_datetime < ?", end).
|
||||
Where("r.deleted_at IS NULL")
|
||||
// feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
|
||||
|
||||
@@ -358,11 +371,11 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
}
|
||||
|
||||
for i := range feedSuppliers {
|
||||
if _, exists := costMap[feedSuppliers[i].KandangID]; !exists {
|
||||
if _, exists := costMap[feedSuppliers[i].ProjectFlockKandangID]; !exists {
|
||||
rows = append(rows, HppPerKandangCostRow{
|
||||
KandangID: feedSuppliers[i].KandangID,
|
||||
ProjectFlockKandangID: feedSuppliers[i].ProjectFlockKandangID,
|
||||
})
|
||||
costMap[feedSuppliers[i].KandangID] = &rows[len(rows)-1]
|
||||
costMap[feedSuppliers[i].ProjectFlockKandangID] = &rows[len(rows)-1]
|
||||
}
|
||||
feedSuppliers[i].Category = "FEED"
|
||||
}
|
||||
@@ -391,9 +404,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
|
||||
)
|
||||
|
||||
type eggRow struct {
|
||||
ProjectFlockKandangID uint
|
||||
EggProductionWeightKg float64
|
||||
EggProductionPieces float64
|
||||
ProjectFlockKandangID uint
|
||||
EggProductionWeightKgRemaining float64
|
||||
EggProductionPiecesRemaining float64
|
||||
EggProductionTotalWeightKg float64
|
||||
EggProductionTotalPieces float64
|
||||
}
|
||||
|
||||
eggRows := make([]eggRow, 0)
|
||||
@@ -401,12 +416,14 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
|
||||
Table("recordings AS r").
|
||||
Select(`
|
||||
r.project_flock_kandangs_id AS project_flock_kandang_id,
|
||||
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
|
||||
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
|
||||
COALESCE(SUM((re.total_qty - re.total_used) * re.weight / 1000), 0) AS egg_production_weight_kg_remaining,
|
||||
COALESCE(SUM(re.total_qty - re.total_used), 0) AS egg_production_pieces_remaining,
|
||||
COALESCE(SUM(re.weight / 1000), 0) AS egg_production_total_weight_kg,
|
||||
COALESCE(SUM(re.total_qty), 0) AS egg_production_total_pieces`).
|
||||
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
|
||||
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
|
||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
||||
// Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.record_datetime < ?", end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||
Group("r.project_flock_kandangs_id")
|
||||
@@ -418,9 +435,11 @@ func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx c
|
||||
result := make(map[uint]HppPerKandangRow, len(eggRows))
|
||||
for _, row := range eggRows {
|
||||
result[row.ProjectFlockKandangID] = HppPerKandangRow{
|
||||
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
||||
EggProductionWeightKg: row.EggProductionWeightKg,
|
||||
EggProductionPieces: row.EggProductionPieces,
|
||||
ProjectFlockKandangID: row.ProjectFlockKandangID,
|
||||
EggProductionWeightKgRemaining: row.EggProductionWeightKgRemaining,
|
||||
EggProductionPiecesRemaining: row.EggProductionPiecesRemaining,
|
||||
EggProductionTotalWeightKg: row.EggProductionTotalWeightKg,
|
||||
EggProductionTotalPieces: row.EggProductionTotalPieces,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +454,7 @@ func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int
|
||||
query = query.Where("k.location_id IN ?", locationIDs)
|
||||
}
|
||||
if len(kandangIDs) > 0 {
|
||||
query = query.Where("k.id IN ?", kandangIDs)
|
||||
query = query.Where("pfk.id IN ?", kandangIDs)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -25,6 +25,21 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository {
|
||||
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 {
|
||||
dateColumn := "purchase_items.received_date"
|
||||
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
|
||||
@@ -34,10 +49,16 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context,
|
||||
dateColumn = "purchase_items.received_date"
|
||||
}
|
||||
|
||||
latestApproval := r.latestPurchaseApproval(ctx)
|
||||
|
||||
db := r.db.WithContext(ctx).
|
||||
Model(&entity.Supplier{}).
|
||||
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 {
|
||||
db = db.Where("suppliers.id = ?", filters.SupplierId)
|
||||
@@ -152,7 +173,11 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context
|
||||
Preload("ExpenseNonstock.Expense").
|
||||
Preload("ExpenseNonstock.Expense.Supplier").
|
||||
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
|
||||
Where("purchases.supplier_id IN ?", supplierIDs)
|
||||
Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
|
||||
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 {
|
||||
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
|
||||
|
||||
@@ -464,9 +464,13 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
result = append(result, item)
|
||||
|
||||
if len(item.Rows) > 0 {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
totalCustomers = int64(len(result))
|
||||
return result, totalCustomers, nil
|
||||
}
|
||||
|
||||
@@ -503,14 +507,8 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
||||
row.Status = status
|
||||
|
||||
if status == "LUNAS" {
|
||||
if previousBalance >= tx.TotalPrice {
|
||||
days := 0
|
||||
row.AgingDay = &days
|
||||
} else if paymentDate != nil {
|
||||
if paymentDate != nil {
|
||||
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
row.AgingDay = &days
|
||||
} else {
|
||||
days := 0
|
||||
@@ -518,9 +516,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
||||
}
|
||||
} else {
|
||||
days := int(time.Since(tx.TransDate).Hours() / 24)
|
||||
if days < 0 {
|
||||
days = 0
|
||||
}
|
||||
row.AgingDay = &days
|
||||
}
|
||||
} else if tx.TransactionType == "PAYMENT" {
|
||||
@@ -586,6 +581,67 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo
|
||||
// 4. BELUM LUNAS: no payment at all
|
||||
|
||||
if previousBalance >= currentSales.TotalPrice {
|
||||
// Cari payment yang digunakan untuk melunasi sales ini dengan FIFO
|
||||
// Track payment allocations that are consumed by previous sales
|
||||
type paymentAllocation struct {
|
||||
date time.Time
|
||||
amount float64
|
||||
consumed float64
|
||||
}
|
||||
allocations := []paymentAllocation{}
|
||||
runningBalance := 0.0
|
||||
|
||||
// Process all transactions before current sales to build allocation map
|
||||
for i := 0; i < currentIndex; i++ {
|
||||
if transactions[i].TransactionType == "PAYMENT" {
|
||||
allocations = append(allocations, paymentAllocation{
|
||||
date: transactions[i].TransDate,
|
||||
amount: transactions[i].PaymentAmount,
|
||||
consumed: 0,
|
||||
})
|
||||
runningBalance += transactions[i].PaymentAmount
|
||||
} else if transactions[i].TransactionType == "SALES" {
|
||||
salesAmount := transactions[i].TotalPrice
|
||||
remainingToConsume := salesAmount
|
||||
|
||||
// Consume from oldest allocations first (FIFO)
|
||||
for j := range allocations {
|
||||
if remainingToConsume <= 0 {
|
||||
break
|
||||
}
|
||||
available := allocations[j].amount - allocations[j].consumed
|
||||
if available > 0 {
|
||||
consume := available
|
||||
if consume > remainingToConsume {
|
||||
consume = remainingToConsume
|
||||
}
|
||||
allocations[j].consumed += consume
|
||||
remainingToConsume -= consume
|
||||
}
|
||||
}
|
||||
runningBalance -= salesAmount
|
||||
}
|
||||
}
|
||||
|
||||
// Now find which allocation covers the current sales
|
||||
amountNeeded := currentSales.TotalPrice
|
||||
for _, alloc := range allocations {
|
||||
available := alloc.amount - alloc.consumed
|
||||
if available > 0 {
|
||||
if amountNeeded <= available {
|
||||
// This allocation fully covers the sales
|
||||
return "LUNAS", &alloc.date
|
||||
} else {
|
||||
// This allocation partially covers, continue to next
|
||||
amountNeeded -= available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, use the oldest allocation
|
||||
if len(allocations) > 0 {
|
||||
return "LUNAS", &allocations[0].date
|
||||
}
|
||||
return "LUNAS", nil
|
||||
}
|
||||
|
||||
@@ -1073,6 +1129,17 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||
@@ -1094,7 +1161,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
continue
|
||||
}
|
||||
|
||||
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
|
||||
items := purchasesBySupplier[supplierID]
|
||||
paymentItems := paymentsBySupplier[supplierID]
|
||||
total := dto.DebtSupplierTotalDTO{}
|
||||
@@ -1102,6 +1169,16 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||
for _, purchase := range items {
|
||||
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)
|
||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||
Row: row,
|
||||
@@ -1318,6 +1395,55 @@ func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, 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) {
|
||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||
if err != nil {
|
||||
@@ -1370,15 +1496,17 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
}
|
||||
for pfkID, egg := range eggMap {
|
||||
if rowIdx, ok := pfkIndex[pfkID]; ok {
|
||||
repoRows[rowIdx].EggProductionWeightKg = egg.EggProductionWeightKg
|
||||
repoRows[rowIdx].EggProductionPieces = egg.EggProductionPieces
|
||||
repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining
|
||||
repoRows[rowIdx].EggProductionPiecesRemaining = egg.EggProductionPiecesRemaining
|
||||
repoRows[rowIdx].EggProductionTotalWeightKg = egg.EggProductionTotalWeightKg
|
||||
repoRows[rowIdx].EggProductionTotalPieces = egg.EggProductionTotalPieces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
costMap := make(map[uint]HppCostAggregate, len(costRows))
|
||||
for _, row := range costRows {
|
||||
costMap[row.KandangID] = HppCostAggregate{
|
||||
costMap[row.ProjectFlockKandangID] = HppCostAggregate{
|
||||
FeedCost: row.FeedCost,
|
||||
OvkCost: row.OvkCost,
|
||||
DocCost: row.DocCost,
|
||||
@@ -1407,15 +1535,15 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
category = "DOC"
|
||||
}
|
||||
|
||||
if seen[sup.KandangID] == nil {
|
||||
seen[sup.KandangID] = make(map[uint]bool)
|
||||
if seen[sup.ProjectFlockKandangID] == nil {
|
||||
seen[sup.ProjectFlockKandangID] = make(map[uint]bool)
|
||||
}
|
||||
if seen[sup.KandangID][sup.SupplierID] {
|
||||
if seen[sup.ProjectFlockKandangID][sup.SupplierID] {
|
||||
continue
|
||||
}
|
||||
seen[sup.KandangID][sup.SupplierID] = true
|
||||
seen[sup.ProjectFlockKandangID][sup.SupplierID] = true
|
||||
|
||||
targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{
|
||||
targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], dto.HppPerKandangSupplierDTO{
|
||||
ID: int64(sup.SupplierID),
|
||||
Name: sup.SupplierName,
|
||||
Alias: sup.SupplierAlias,
|
||||
@@ -1442,12 +1570,12 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
|
||||
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
|
||||
var totalBirds int64
|
||||
var totalWeight float64
|
||||
// var totalWeight float64
|
||||
var totalEggPieces int64
|
||||
var totalEggKg float64
|
||||
var totalRemainingValueRp int64
|
||||
// var totalRemainingValueRp int64
|
||||
var totalEggValueRp int64
|
||||
var totalHppSum float64
|
||||
// var totalHppSum float64
|
||||
var totalHppCount int
|
||||
var totalDocPriceSum float64
|
||||
var totalDocPriceCount int
|
||||
@@ -1461,26 +1589,34 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
continue
|
||||
}
|
||||
|
||||
birdsFloat := row.RemainingChickenBirds
|
||||
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
||||
birdsFloat = 0
|
||||
// birdsFloat := row.RemainingChickenBirds
|
||||
// if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
|
||||
// birdsFloat = 0
|
||||
// }
|
||||
// weightFloat := row.RemainingChickenWeight
|
||||
// if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
|
||||
// weightFloat = 0
|
||||
// }
|
||||
eggPiecesFloatRemaining := row.EggProductionPiecesRemaining
|
||||
if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) {
|
||||
eggPiecesFloatRemaining = 0
|
||||
}
|
||||
weightFloat := row.RemainingChickenWeight
|
||||
if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
|
||||
weightFloat = 0
|
||||
eggTotalPiecesFloat := row.EggProductionTotalPieces
|
||||
if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) {
|
||||
eggTotalPiecesFloat = 0
|
||||
}
|
||||
eggPiecesFloat := row.EggProductionPieces
|
||||
if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) {
|
||||
eggPiecesFloat = 0
|
||||
eggRemainingWeightFloatRemaining := row.EggProductionWeightKgRemaining
|
||||
if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) {
|
||||
eggRemainingWeightFloatRemaining = 0
|
||||
}
|
||||
eggWeightFloat := row.EggProductionWeightKg
|
||||
eggWeightFloat := row.EggProductionTotalWeightKg
|
||||
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
|
||||
eggWeightFloat = 0
|
||||
}
|
||||
|
||||
avgWeight := 0.0
|
||||
if eggPiecesFloat > 0 {
|
||||
avgWeight = eggWeightFloat / eggPiecesFloat
|
||||
if eggTotalPiecesFloat > 0 {
|
||||
avgWeight = eggWeightFloat / eggTotalPiecesFloat
|
||||
}
|
||||
if params.WeightMin != nil && avgWeight < *params.WeightMin {
|
||||
continue
|
||||
@@ -1496,28 +1632,30 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
weightMax := weightMin + 0.09
|
||||
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
|
||||
|
||||
rowBirds := int64(math.Round(birdsFloat))
|
||||
costEntry := costMap[row.KandangID]
|
||||
// rowBirds := int64(math.Round(birdsFloat))
|
||||
costEntry := costMap[row.ProjectFlockKandangID]
|
||||
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
|
||||
hppRp := 0.0
|
||||
if weightFloat > 0 {
|
||||
hppRp = totalCost / weightFloat
|
||||
}
|
||||
// hppRp := 0.0
|
||||
// if weightFloat > 0 {
|
||||
// hppRp = totalCost / weightFloat
|
||||
// }
|
||||
eggHpp := 0.0
|
||||
if eggWeightFloat > 0 {
|
||||
eggHpp = totalCost / eggWeightFloat
|
||||
eggHpp = (totalCost / eggWeightFloat) / 1000
|
||||
}
|
||||
|
||||
rowEggPieces := int64(math.Round(eggPiecesFloat))
|
||||
rowEggValue := int64(eggHpp * eggWeightFloat)
|
||||
rowRemainingValue := int64(hppRp * weightFloat)
|
||||
rowEggPieces := int64(math.Round(eggPiecesFloatRemaining))
|
||||
rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining)
|
||||
// rowRemainingValue := int64(hppRp * weightFloat)
|
||||
avgDocPrice := int64(0)
|
||||
if costEntry.DocQty > 0 {
|
||||
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
|
||||
}
|
||||
|
||||
nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod)
|
||||
|
||||
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
|
||||
ID: int(row.KandangID),
|
||||
ID: int(row.ProjectFlockKandangID),
|
||||
Kandang: dto.HppPerKandangRowKandangDTO{
|
||||
ID: int64(row.KandangID),
|
||||
Name: row.KandangName,
|
||||
@@ -1535,32 +1673,35 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
WeightMin: weightMin,
|
||||
WeightMax: weightMax,
|
||||
},
|
||||
AvgWeightKg: avgWeight,
|
||||
AvgWeightKg: avgWeight,
|
||||
NameWithPeriode: nameWithPeriod,
|
||||
// FeedCostRp: costEntry.FeedCost,
|
||||
// OvkCostRp: costEntry.OvkCost,
|
||||
DocSuppliers: docSupplierMap[row.KandangID],
|
||||
FeedSuppliers: feedSupplierMap[row.KandangID],
|
||||
EggProductionPieces: rowEggPieces,
|
||||
EggProductionKg: eggWeightFloat,
|
||||
AverageDocPriceRp: avgDocPrice,
|
||||
DocSuppliers: docSupplierMap[row.ProjectFlockKandangID],
|
||||
FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID],
|
||||
EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)),
|
||||
EggProductionKg: eggRemainingWeightFloatRemaining,
|
||||
// EggProductionTotalWeightKg: eggWeightFloat,
|
||||
// EggProductionTotalPieces: int64(math.Round(eggTotalPiecesFloat)),
|
||||
AverageDocPriceRp: avgDocPrice,
|
||||
// HppRp: hppRp,
|
||||
EggHppRpPerKg: eggHpp,
|
||||
RemainingValueRp: rowRemainingValue,
|
||||
EggValueRp: rowEggValue,
|
||||
EggHppRpPerKg: eggHpp,
|
||||
// RemainingValueRp: rowRemainingValue,
|
||||
EggValueRp: rowEggValue,
|
||||
})
|
||||
|
||||
totalBirds += rowBirds
|
||||
totalWeight += weightFloat
|
||||
// totalBirds += rowBirds
|
||||
// totalWeight += weightFloat
|
||||
totalEggPieces += rowEggPieces
|
||||
totalEggKg += eggWeightFloat
|
||||
totalRemainingValueRp += rowRemainingValue
|
||||
totalEggKg += eggRemainingWeightFloatRemaining
|
||||
// totalRemainingValueRp += rowRemainingValue
|
||||
totalEggValueRp += rowEggValue
|
||||
totalAvgWeightSum += avgWeight
|
||||
totalAvgWeightCount++
|
||||
if weightFloat > 0 {
|
||||
totalHppSum += hppRp
|
||||
totalHppCount++
|
||||
}
|
||||
// if weightFloat > 0 {
|
||||
// totalHppSum += hppRp
|
||||
// totalHppCount++
|
||||
// }
|
||||
if avgDocPrice > 0 {
|
||||
totalDocPriceSum += float64(avgDocPrice)
|
||||
totalDocPriceCount++
|
||||
@@ -1587,23 +1728,23 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes
|
||||
}
|
||||
|
||||
rangeSummary := rangeAgg.Summary
|
||||
rangeAgg.RemainingBirds += rowBirds
|
||||
rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
|
||||
// rangeAgg.RemainingBirds += rowBirds
|
||||
// rangeAgg.RemainingWeightKg += row.RemainingChickenWeight
|
||||
rangeAgg.AvgWeightSum += avgWeight
|
||||
rangeAgg.AvgWeightCount++
|
||||
for _, supplier := range feedSupplierMap[row.KandangID] {
|
||||
for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] {
|
||||
if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok {
|
||||
rangeAgg.FeedSuppliers[supplier.ID] = supplier
|
||||
}
|
||||
}
|
||||
for _, supplier := range docSupplierMap[row.KandangID] {
|
||||
for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] {
|
||||
if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok {
|
||||
rangeAgg.DocSuppliers[supplier.ID] = supplier
|
||||
}
|
||||
}
|
||||
rangeSummary.EggProductionPieces += rowEggPieces
|
||||
rangeSummary.EggProductionKg += eggWeightFloat
|
||||
rangeSummary.RemainingValueRp += rowRemainingValue
|
||||
rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining
|
||||
// rangeSummary.RemainingValueRp += rowRemainingValue
|
||||
rangeSummary.EggValueRp += rowEggValue
|
||||
if eggWeightFloat > 0 {
|
||||
rangeAgg.EggHppSum += eggHpp
|
||||
@@ -1750,6 +1891,9 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
|
||||
if err != nil {
|
||||
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
if weightMin != nil && weightMax != nil && *weightMin > *weightMax {
|
||||
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max")
|
||||
}
|
||||
|
||||
params := &validation.HppPerKandangQuery{
|
||||
Page: page,
|
||||
|
||||
@@ -17,12 +17,14 @@ type ExpenseQuery struct {
|
||||
|
||||
type MarketingQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=100"`
|
||||
CustomerId int64 `query:"customer_id" validate:"omitempty"`
|
||||
ProductId int64 `query:"product_id" validate:"omitempty"`
|
||||
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
||||
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
||||
AreaId int64 `query:"area_id" validate:"omitempty"`
|
||||
LocationId int64 `query:"location_id" validate:"omitempty"`
|
||||
MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"`
|
||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"`
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
@@ -56,7 +58,7 @@ type DebtSupplierQuery struct {
|
||||
|
||||
type HppPerKandangQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
||||
Period string `query:"period" validate:"required"`
|
||||
ShowUnrecorded bool `query:"show_unrecorded"`
|
||||
AreaIDs []int64 `query:"-"`
|
||||
@@ -68,7 +70,7 @@ type HppPerKandangQuery struct {
|
||||
|
||||
type ProductionResultQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
|
||||
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@ package fifo
|
||||
|
||||
const (
|
||||
// Usable Keys
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
|
||||
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
||||
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyRecordingDepletion UsableKey = "RECORDING_DEPLETION"
|
||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||
UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT"
|
||||
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
||||
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
||||
|
||||
// Stockable Keys
|
||||
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
|
||||
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
||||
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
||||
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
||||
StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN"
|
||||
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
||||
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
||||
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
||||
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
||||
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
|
||||
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
|
||||
)
|
||||
|
||||
@@ -14,15 +14,10 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto
|
||||
for _, item := range items {
|
||||
usagePtr := new(float64)
|
||||
*usagePtr = item.Qty
|
||||
pending := item.PendingQty
|
||||
if pending == nil {
|
||||
pending = new(float64)
|
||||
}
|
||||
result = append(result, entity.RecordingStock{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
UsageQty: usagePtr,
|
||||
PendingQty: pending,
|
||||
})
|
||||
}
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user