mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
codex: initiated changes
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Farm Stock Attribution Design Note
|
||||
|
||||
## Goal
|
||||
|
||||
Allow farm-level physical stock to be used directly by kandang-level operations without forcing transfers, while keeping kandang attribution, FIFO-v2 compatibility, traceability, and HPP/COGS intact.
|
||||
|
||||
## Core Model
|
||||
|
||||
- Physical stock stays on the real `product_warehouse_id` that was consumed or received.
|
||||
- Kandang attribution comes from the transaction or allocation path, not from `product_warehouses.project_flock_kandang_id`.
|
||||
- Existing kandang-bound warehouses remain valid for historical and current kandang-only flows.
|
||||
- Shared farm warehouses must stay shareable; application code must stop silently converting them into kandang-owned warehouses.
|
||||
|
||||
## Attribution Rules
|
||||
|
||||
- `recording_stocks`: consumer kandang is the parent `recordings.project_flock_kandangs_id`; physical stock source remains `recording_stocks.product_warehouse_id`.
|
||||
- `recording_depletions`: source kandang is the recording kandang and is stored explicitly for compatibility; physical source remains `source_product_warehouse_id`, destination stock remains `product_warehouse_id`.
|
||||
- `recording_eggs`: producer kandang is the recording kandang and is stored explicitly for compatibility; physical stock remains `product_warehouse_id`, which may be a farm warehouse.
|
||||
- `marketing_delivery_products`: outbound kandang attribution comes from active `stock_allocations` to `PROJECT_FLOCK_POPULATION`, `RECORDING_DEPLETION`, or `RECORDING_EGG`, with product-warehouse kandang ownership only as a fallback for historical/non-FIFO rows.
|
||||
|
||||
## Reporting and HPP
|
||||
|
||||
- Feed and OVK cost attribution should continue to follow recording-level consumption plus FIFO allocations to incoming stock.
|
||||
- Egg and live-bird sales attribution should be derived from `stock_allocations` back to the originating kandang transactions or populations.
|
||||
- Queries that filter or group by kandang must use explicit transaction attribution or FIFO allocation provenance, not warehouse ownership, when pooled farm stock is involved.
|
||||
|
||||
## Live-Data Safety
|
||||
|
||||
- Schema changes are additive and nullable.
|
||||
- Historical rows are backfilled only when attribution is deterministic from existing rows.
|
||||
- No FIFO-v2 route-rule behavior is changed unless the current code is only resyncing or constraining allocation metadata around already-created FIFO allocations.
|
||||
@@ -0,0 +1,224 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MarketingDeliveryAttributionRow struct {
|
||||
MarketingDeliveryProductID uint `gorm:"column:marketing_delivery_product_id"`
|
||||
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
|
||||
ProjectFlockID uint `gorm:"column:project_flock_id"`
|
||||
ProjectFlockCategory string `gorm:"column:project_flock_category"`
|
||||
AllocatedQty float64 `gorm:"column:allocated_qty"`
|
||||
}
|
||||
|
||||
func MarketingDeliveryAttributionRowsQuery(db *gorm.DB) *gorm.DB {
|
||||
sql := `
|
||||
WITH mapped AS (
|
||||
SELECT
|
||||
sa.usable_id AS marketing_delivery_product_id,
|
||||
pc.project_flock_kandang_id AS project_flock_kandang_id,
|
||||
pfk.project_flock_id AS project_flock_id,
|
||||
pf.category AS project_flock_category,
|
||||
SUM(sa.qty) AS allocated_qty
|
||||
FROM stock_allocations sa
|
||||
JOIN project_flock_populations pfp
|
||||
ON pfp.id = sa.stockable_id
|
||||
AND sa.stockable_type = ?
|
||||
JOIN project_chickins pc ON pc.id = pfp.project_chickin_id
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = pc.project_flock_kandang_id
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
WHERE sa.usable_type = ?
|
||||
AND sa.status = ?
|
||||
AND sa.allocation_purpose = ?
|
||||
GROUP BY sa.usable_id, pc.project_flock_kandang_id, pfk.project_flock_id, pf.category
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
sa.usable_id AS marketing_delivery_product_id,
|
||||
COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id) AS project_flock_kandang_id,
|
||||
pfk.project_flock_id AS project_flock_id,
|
||||
pf.category AS project_flock_category,
|
||||
SUM(sa.qty) AS allocated_qty
|
||||
FROM stock_allocations sa
|
||||
JOIN recording_eggs re
|
||||
ON re.id = sa.stockable_id
|
||||
AND sa.stockable_type = ?
|
||||
LEFT JOIN recordings r ON r.id = re.recording_id
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id)
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
WHERE sa.usable_type = ?
|
||||
AND sa.status = ?
|
||||
AND sa.allocation_purpose = ?
|
||||
GROUP BY sa.usable_id, COALESCE(re.project_flock_kandang_id, r.project_flock_kandangs_id), pfk.project_flock_id, pf.category
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
sa.usable_id AS marketing_delivery_product_id,
|
||||
COALESCE(rd.source_project_flock_kandang_id, r.project_flock_kandangs_id) AS project_flock_kandang_id,
|
||||
pfk.project_flock_id AS project_flock_id,
|
||||
pf.category AS project_flock_category,
|
||||
SUM(sa.qty) AS allocated_qty
|
||||
FROM stock_allocations sa
|
||||
JOIN recording_depletions rd
|
||||
ON rd.id = sa.stockable_id
|
||||
AND sa.stockable_type = ?
|
||||
LEFT JOIN recordings r ON r.id = rd.recording_id
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = COALESCE(rd.source_project_flock_kandang_id, r.project_flock_kandangs_id)
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
WHERE sa.usable_type = ?
|
||||
AND sa.status = ?
|
||||
AND sa.allocation_purpose = ?
|
||||
GROUP BY sa.usable_id, COALESCE(rd.source_project_flock_kandang_id, r.project_flock_kandangs_id), pfk.project_flock_id, pf.category
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
sa.usable_id AS marketing_delivery_product_id,
|
||||
pi.project_flock_kandang_id AS project_flock_kandang_id,
|
||||
pfk.project_flock_id AS project_flock_id,
|
||||
pf.category AS project_flock_category,
|
||||
SUM(sa.qty) AS allocated_qty
|
||||
FROM stock_allocations sa
|
||||
JOIN purchase_items pi
|
||||
ON pi.id = sa.stockable_id
|
||||
AND sa.stockable_type = ?
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = pi.project_flock_kandang_id
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
WHERE sa.usable_type = ?
|
||||
AND sa.status = ?
|
||||
AND sa.allocation_purpose = ?
|
||||
AND pi.project_flock_kandang_id IS NOT NULL
|
||||
GROUP BY sa.usable_id, pi.project_flock_kandang_id, pfk.project_flock_id, pf.category
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
sa.usable_id AS marketing_delivery_product_id,
|
||||
source_pw.project_flock_kandang_id AS project_flock_kandang_id,
|
||||
pfk.project_flock_id AS project_flock_id,
|
||||
pf.category AS project_flock_category,
|
||||
SUM(sa.qty) AS allocated_qty
|
||||
FROM stock_allocations sa
|
||||
JOIN stock_transfer_details std
|
||||
ON std.id = sa.stockable_id
|
||||
AND sa.stockable_type = ?
|
||||
JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = source_pw.project_flock_kandang_id
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
WHERE sa.usable_type = ?
|
||||
AND sa.status = ?
|
||||
AND sa.allocation_purpose = ?
|
||||
AND source_pw.project_flock_kandang_id IS NOT NULL
|
||||
GROUP BY sa.usable_id, source_pw.project_flock_kandang_id, pfk.project_flock_id, pf.category
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
sa.usable_id AS marketing_delivery_product_id,
|
||||
ltt.target_project_flock_kandang_id AS project_flock_kandang_id,
|
||||
pfk.project_flock_id AS project_flock_id,
|
||||
pf.category AS project_flock_category,
|
||||
SUM(sa.qty) AS allocated_qty
|
||||
FROM stock_allocations sa
|
||||
JOIN laying_transfer_targets ltt
|
||||
ON ltt.id = sa.stockable_id
|
||||
AND sa.stockable_type = ?
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = ltt.target_project_flock_kandang_id
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
WHERE sa.usable_type = ?
|
||||
AND sa.status = ?
|
||||
AND sa.allocation_purpose = ?
|
||||
GROUP BY sa.usable_id, ltt.target_project_flock_kandang_id, pfk.project_flock_id, pf.category
|
||||
)
|
||||
SELECT
|
||||
src.marketing_delivery_product_id,
|
||||
src.project_flock_kandang_id,
|
||||
src.project_flock_id,
|
||||
src.project_flock_category,
|
||||
SUM(src.allocated_qty) AS allocated_qty
|
||||
FROM (
|
||||
SELECT
|
||||
mapped.marketing_delivery_product_id,
|
||||
mapped.project_flock_kandang_id,
|
||||
mapped.project_flock_id,
|
||||
mapped.project_flock_category,
|
||||
mapped.allocated_qty
|
||||
FROM mapped
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
mdp.id AS marketing_delivery_product_id,
|
||||
pw.project_flock_kandang_id AS project_flock_kandang_id,
|
||||
pfk.project_flock_id AS project_flock_id,
|
||||
pf.category AS project_flock_category,
|
||||
COALESCE(mdp.usage_qty, 0) AS allocated_qty
|
||||
FROM marketing_delivery_products mdp
|
||||
JOIN marketing_products mp ON mp.id = mdp.marketing_product_id
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = pw.project_flock_kandang_id
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
LEFT JOIN mapped ON mapped.marketing_delivery_product_id = mdp.id
|
||||
WHERE mapped.marketing_delivery_product_id IS NULL
|
||||
AND pw.project_flock_kandang_id IS NOT NULL
|
||||
AND COALESCE(mdp.usage_qty, 0) > 0
|
||||
) src
|
||||
GROUP BY
|
||||
src.marketing_delivery_product_id,
|
||||
src.project_flock_kandang_id,
|
||||
src.project_flock_id,
|
||||
src.project_flock_category
|
||||
`
|
||||
|
||||
return db.Raw(
|
||||
sql,
|
||||
fifo.StockableKeyProjectFlockPopulation.String(),
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
fifo.StockableKeyRecordingEgg.String(),
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
fifo.StockableKeyRecordingDepletion.String(),
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
fifo.StockableKeyStockTransferIn.String(),
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
fifo.StockableKeyTransferToLayingIn.String(),
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
)
|
||||
}
|
||||
|
||||
func MarketingDeliverySingleAttributionQuery(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Table("(?) AS mda", MarketingDeliveryAttributionRowsQuery(db)).
|
||||
Select(`
|
||||
mda.marketing_delivery_product_id,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT mda.project_flock_kandang_id) = 1 THEN MIN(mda.project_flock_kandang_id)
|
||||
ELSE NULL
|
||||
END AS attributed_project_flock_kandang_id
|
||||
`).
|
||||
Group("mda.marketing_delivery_product_id")
|
||||
}
|
||||
|
||||
func MarketingDeliveryAttributionFilterSQL(column string) string {
|
||||
return fmt.Sprintf("EXISTS (SELECT 1 FROM (?) AS mda WHERE mda.marketing_delivery_product_id = %s)", column)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestMarketingDeliveryAttributionRowsQueryIncludesMappedAndFallbackRows(t *testing.T) {
|
||||
db := setupMarketingAttributionTestDB(t)
|
||||
|
||||
statements := []string{
|
||||
`INSERT INTO project_flocks (id, category) VALUES (1, 'LAYING')`,
|
||||
`INSERT INTO project_flock_kandangs (id, project_flock_id) VALUES (101, 1), (102, 1)`,
|
||||
`INSERT INTO project_chickins (id, project_flock_kandang_id) VALUES (201, 101), (202, 102)`,
|
||||
`INSERT INTO project_flock_populations (id, project_chickin_id) VALUES (301, 201), (302, 202)`,
|
||||
`INSERT INTO product_warehouses (id, project_flock_kandang_id) VALUES (401, NULL), (402, 101)`,
|
||||
`INSERT INTO marketing_products (id, product_warehouse_id) VALUES (501, 401), (502, 402), (503, 401)`,
|
||||
`INSERT INTO marketing_delivery_products (id, marketing_product_id, usage_qty) VALUES (601, 501, 100), (602, 502, 25), (603, 503, 12)`,
|
||||
`INSERT INTO recording_eggs (id, recording_id, project_flock_kandang_id) VALUES (701, NULL, 101)`,
|
||||
`INSERT INTO stock_allocations (id, product_warehouse_id, stockable_type, stockable_id, usable_type, usable_id, qty, status, allocation_purpose) VALUES
|
||||
(1, 401, 'PROJECT_FLOCK_POPULATION', 301, 'MARKETING_DELIVERY', 601, 60, 'ACTIVE', 'CONSUME'),
|
||||
(2, 401, 'PROJECT_FLOCK_POPULATION', 302, 'MARKETING_DELIVERY', 601, 40, 'ACTIVE', 'CONSUME'),
|
||||
(3, 401, 'RECORDING_EGG', 701, 'MARKETING_DELIVERY', 603, 12, 'ACTIVE', 'CONSUME')`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed seeding fixtures: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var rows []MarketingDeliveryAttributionRow
|
||||
if err := db.Table("(?) AS mda", MarketingDeliveryAttributionRowsQuery(db)).
|
||||
Order("mda.marketing_delivery_product_id ASC, mda.project_flock_kandang_id ASC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
t.Fatalf("failed scanning attribution rows: %v", err)
|
||||
}
|
||||
|
||||
if len(rows) != 4 {
|
||||
t.Fatalf("expected 4 attribution rows, got %d", len(rows))
|
||||
}
|
||||
if rows[0].MarketingDeliveryProductID != 601 || rows[0].ProjectFlockKandangID != 101 || rows[0].AllocatedQty != 60 {
|
||||
t.Fatalf("unexpected first attribution row: %+v", rows[0])
|
||||
}
|
||||
if rows[1].MarketingDeliveryProductID != 601 || rows[1].ProjectFlockKandangID != 102 || rows[1].AllocatedQty != 40 {
|
||||
t.Fatalf("unexpected second attribution row: %+v", rows[1])
|
||||
}
|
||||
if rows[2].MarketingDeliveryProductID != 602 || rows[2].ProjectFlockKandangID != 101 || rows[2].AllocatedQty != 25 {
|
||||
t.Fatalf("unexpected fallback attribution row: %+v", rows[2])
|
||||
}
|
||||
if rows[3].MarketingDeliveryProductID != 603 || rows[3].ProjectFlockKandangID != 101 || rows[3].AllocatedQty != 12 {
|
||||
t.Fatalf("unexpected egg attribution row: %+v", rows[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarketingDeliverySingleAttributionQueryOnlyReturnsSingleSourceRows(t *testing.T) {
|
||||
db := setupMarketingAttributionTestDB(t)
|
||||
|
||||
statements := []string{
|
||||
`INSERT INTO project_flocks (id, category) VALUES (1, 'LAYING')`,
|
||||
`INSERT INTO project_flock_kandangs (id, project_flock_id) VALUES (101, 1), (102, 1)`,
|
||||
`INSERT INTO project_chickins (id, project_flock_kandang_id) VALUES (201, 101), (202, 102)`,
|
||||
`INSERT INTO project_flock_populations (id, project_chickin_id) VALUES (301, 201), (302, 202)`,
|
||||
`INSERT INTO product_warehouses (id, project_flock_kandang_id) VALUES (401, NULL), (402, 101)`,
|
||||
`INSERT INTO marketing_products (id, product_warehouse_id) VALUES (501, 401), (502, 402), (503, 401)`,
|
||||
`INSERT INTO marketing_delivery_products (id, marketing_product_id, usage_qty) VALUES (601, 501, 100), (602, 502, 25), (603, 503, 12)`,
|
||||
`INSERT INTO recording_eggs (id, recording_id, project_flock_kandang_id) VALUES (701, NULL, 101)`,
|
||||
`INSERT INTO stock_allocations (id, product_warehouse_id, stockable_type, stockable_id, usable_type, usable_id, qty, status, allocation_purpose) VALUES
|
||||
(1, 401, 'PROJECT_FLOCK_POPULATION', 301, 'MARKETING_DELIVERY', 601, 60, 'ACTIVE', 'CONSUME'),
|
||||
(2, 401, 'PROJECT_FLOCK_POPULATION', 302, 'MARKETING_DELIVERY', 601, 40, 'ACTIVE', 'CONSUME'),
|
||||
(3, 401, 'RECORDING_EGG', 701, 'MARKETING_DELIVERY', 603, 12, 'ACTIVE', 'CONSUME')`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed seeding fixtures: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type singleRow struct {
|
||||
MarketingDeliveryProductID uint `gorm:"column:marketing_delivery_product_id"`
|
||||
AttributedProjectFlockKandangID *uint `gorm:"column:attributed_project_flock_kandang_id"`
|
||||
}
|
||||
|
||||
var rows []singleRow
|
||||
if err := db.Table("(?) AS mda", MarketingDeliverySingleAttributionQuery(db)).
|
||||
Order("mda.marketing_delivery_product_id ASC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
t.Fatalf("failed scanning single attribution rows: %v", err)
|
||||
}
|
||||
|
||||
if len(rows) != 3 {
|
||||
t.Fatalf("expected 3 rows, got %d", len(rows))
|
||||
}
|
||||
if rows[0].MarketingDeliveryProductID != 601 || rows[0].AttributedProjectFlockKandangID != nil {
|
||||
t.Fatalf("expected pooled delivery 601 to have nil single attribution, got %+v", rows[0])
|
||||
}
|
||||
if rows[1].MarketingDeliveryProductID != 602 || rows[1].AttributedProjectFlockKandangID == nil || *rows[1].AttributedProjectFlockKandangID != 101 {
|
||||
t.Fatalf("expected fallback delivery 602 to map to kandang 101, got %+v", rows[1])
|
||||
}
|
||||
if rows[2].MarketingDeliveryProductID != 603 || rows[2].AttributedProjectFlockKandangID == nil || *rows[2].AttributedProjectFlockKandangID != 101 {
|
||||
t.Fatalf("expected egg delivery 603 to map to kandang 101, got %+v", rows[2])
|
||||
}
|
||||
}
|
||||
|
||||
func setupMarketingAttributionTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed opening sqlite db: %v", err)
|
||||
}
|
||||
|
||||
statements := []string{
|
||||
`CREATE TABLE stock_allocations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
product_warehouse_id INTEGER,
|
||||
stockable_type TEXT,
|
||||
stockable_id INTEGER,
|
||||
usable_type TEXT,
|
||||
usable_id INTEGER,
|
||||
qty NUMERIC(15,3),
|
||||
status TEXT,
|
||||
allocation_purpose TEXT
|
||||
)`,
|
||||
`CREATE TABLE project_flock_populations (id INTEGER PRIMARY KEY, project_chickin_id INTEGER)`,
|
||||
`CREATE TABLE project_chickins (id INTEGER PRIMARY KEY, project_flock_kandang_id INTEGER)`,
|
||||
`CREATE TABLE project_flock_kandangs (id INTEGER PRIMARY KEY, project_flock_id INTEGER)`,
|
||||
`CREATE TABLE project_flocks (id INTEGER PRIMARY KEY, category TEXT)`,
|
||||
`CREATE TABLE marketing_delivery_products (id INTEGER PRIMARY KEY, marketing_product_id INTEGER, usage_qty NUMERIC(15,3))`,
|
||||
`CREATE TABLE marketing_products (id INTEGER PRIMARY KEY, product_warehouse_id INTEGER)`,
|
||||
`CREATE TABLE product_warehouses (id INTEGER PRIMARY KEY, project_flock_kandang_id INTEGER NULL)`,
|
||||
`CREATE TABLE recording_eggs (id INTEGER PRIMARY KEY, recording_id INTEGER, project_flock_kandang_id INTEGER NULL)`,
|
||||
`CREATE TABLE recordings (id INTEGER PRIMARY KEY, project_flock_kandangs_id INTEGER NULL)`,
|
||||
`CREATE TABLE recording_depletions (id INTEGER PRIMARY KEY, recording_id INTEGER, source_project_flock_kandang_id INTEGER NULL)`,
|
||||
`CREATE TABLE purchase_items (id INTEGER PRIMARY KEY, project_flock_kandang_id INTEGER NULL)`,
|
||||
`CREATE TABLE stock_transfer_details (id INTEGER PRIMARY KEY, source_product_warehouse_id INTEGER NULL)`,
|
||||
`CREATE TABLE laying_transfer_targets (id INTEGER PRIMARY KEY, target_project_flock_kandang_id INTEGER NULL)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed preparing schema: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS idx_recording_depletions_source_project_flock_kandang_id;
|
||||
DROP INDEX IF EXISTS idx_recording_eggs_project_flock_kandang_id;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP CONSTRAINT IF EXISTS fk_recording_depletions_source_project_flock_kandang_id;
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
DROP CONSTRAINT IF EXISTS fk_recording_eggs_project_flock_kandang_id;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP COLUMN IF EXISTS source_project_flock_kandang_id;
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
DROP COLUMN IF EXISTS project_flock_kandang_id;
|
||||
|
||||
COMMIT;
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD COLUMN IF NOT EXISTS source_project_flock_kandang_id BIGINT NULL;
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT NULL;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_recording_depletions_source_project_flock_kandang_id'
|
||||
) THEN
|
||||
ALTER TABLE recording_depletions
|
||||
ADD CONSTRAINT fk_recording_depletions_source_project_flock_kandang_id
|
||||
FOREIGN KEY (source_project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE SET NULL
|
||||
ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_recording_eggs_project_flock_kandang_id'
|
||||
) THEN
|
||||
ALTER TABLE recording_eggs
|
||||
ADD CONSTRAINT fk_recording_eggs_project_flock_kandang_id
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE SET NULL
|
||||
ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_depletions_source_project_flock_kandang_id
|
||||
ON recording_depletions(source_project_flock_kandang_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_recording_eggs_project_flock_kandang_id
|
||||
ON recording_eggs(project_flock_kandang_id);
|
||||
|
||||
UPDATE recording_depletions rd
|
||||
SET source_project_flock_kandang_id = r.project_flock_kandangs_id
|
||||
FROM recordings r
|
||||
WHERE r.id = rd.recording_id
|
||||
AND rd.source_project_flock_kandang_id IS NULL
|
||||
AND r.project_flock_kandangs_id IS NOT NULL;
|
||||
|
||||
UPDATE recording_eggs re
|
||||
SET project_flock_kandang_id = r.project_flock_kandangs_id
|
||||
FROM recordings r
|
||||
WHERE r.id = re.recording_id
|
||||
AND re.project_flock_kandang_id IS NULL
|
||||
AND r.project_flock_kandangs_id IS NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -5,20 +5,22 @@ import (
|
||||
)
|
||||
|
||||
type MarketingDeliveryProduct struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||
ProductWarehouseId uint `gorm:"not null"`
|
||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||
ProductWarehouseId uint `gorm:"not null"`
|
||||
AttributedProjectFlockKandangId *uint `gorm:"->;column:attributed_project_flock_kandang_id"`
|
||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||
|
||||
// FIFO Fields
|
||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
|
||||
|
||||
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||
AttributedProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:AttributedProjectFlockKandangId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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"`
|
||||
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
UsageQty float64 `gorm:"column:usage_qty"`
|
||||
PendingQty float64 `gorm:"column:pending_qty"`
|
||||
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"`
|
||||
SourceProjectFlockKandangId *uint `gorm:"column:source_project_flock_kandang_id"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
UsageQty float64 `gorm:"column:usage_qty"`
|
||||
PendingQty float64 `gorm:"column:pending_qty"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -3,18 +3,20 @@ package entities
|
||||
import "time"
|
||||
|
||||
type RecordingEgg struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty int `gorm:"column:qty;not null"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalUsed float64 `gorm:"column:total_used"`
|
||||
Weight *float64 `gorm:"column:weight"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
ProductFlagName *string `gorm:"->;column:product_flag_name" json:"-"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
|
||||
Qty int `gorm:"column:qty;not null"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalUsed float64 `gorm:"column:total_used"`
|
||||
Weight *float64 `gorm:"column:weight"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
ProductFlagName *string `gorm:"->;column:product_flag_name" json:"-"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ type PenjualanRealisasiResponseDTO struct {
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
projectFlockKandang := resolveMarketingDeliveryProjectFlockKandang(e)
|
||||
|
||||
productFlags := make([]string, len(e.MarketingProduct.ProductWarehouse.Product.Flags))
|
||||
for i, f := range e.MarketingProduct.ProductWarehouse.Product.Flags {
|
||||
@@ -51,11 +52,11 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
}
|
||||
|
||||
var category string
|
||||
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||
category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
|
||||
if projectFlockKandang != nil {
|
||||
category = projectFlockKandang.ProjectFlock.Category
|
||||
}
|
||||
|
||||
ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
|
||||
ageInDay, ageInWeeks := calculateAgeFromChickin(projectFlockKandang, e.DeliveryDate, productFlags, category)
|
||||
|
||||
var product *productDTO.ProductRelationDTO
|
||||
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||
@@ -70,8 +71,8 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
}
|
||||
|
||||
var kandang *kandangDTO.KandangRelationDTO
|
||||
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
|
||||
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
|
||||
if projectFlockKandang != nil && projectFlockKandang.Kandang.Id != 0 {
|
||||
mapped := kandangDTO.ToKandangRelationDTO(projectFlockKandang.Kandang)
|
||||
kandang = &mapped
|
||||
}
|
||||
|
||||
@@ -102,6 +103,7 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
}
|
||||
|
||||
func ToSalesAgeDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
projectFlockKandang := resolveMarketingDeliveryProjectFlockKandang(e)
|
||||
|
||||
productFlags := make([]string, len(e.MarketingProduct.ProductWarehouse.Product.Flags))
|
||||
for i, f := range e.MarketingProduct.ProductWarehouse.Product.Flags {
|
||||
@@ -109,11 +111,11 @@ func ToSalesAgeDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
}
|
||||
|
||||
var category string
|
||||
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||
category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
|
||||
if projectFlockKandang != nil {
|
||||
category = projectFlockKandang.ProjectFlock.Category
|
||||
}
|
||||
|
||||
ageInDay, _ := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
|
||||
ageInDay, _ := calculateAgeFromChickin(projectFlockKandang, e.DeliveryDate, productFlags, category)
|
||||
|
||||
return SalesDTO{
|
||||
Age: ageInDay,
|
||||
@@ -164,6 +166,13 @@ func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) Penjua
|
||||
}
|
||||
}
|
||||
|
||||
func resolveMarketingDeliveryProjectFlockKandang(e entity.MarketingDeliveryProduct) *entity.ProjectFlockKandang {
|
||||
if e.AttributedProjectFlockKandang != nil {
|
||||
return e.AttributedProjectFlockKandang
|
||||
}
|
||||
return e.MarketingProduct.ProductWarehouse.ProjectFlockKandang
|
||||
}
|
||||
|
||||
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string, category string) (int, int) {
|
||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
||||
return 0, 0
|
||||
|
||||
@@ -298,10 +298,11 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c
|
||||
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("recording_stocks rs").
|
||||
Joins("JOIN recordings rec ON rec.id = rs.recording_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("rec.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", "PAKAN").
|
||||
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
||||
Scan(&usageAgg).Error
|
||||
@@ -340,10 +341,11 @@ func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx cont
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_depletions rd").
|
||||
Joins("JOIN recordings rec ON rec.id = rd.recording_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("COALESCE(rd.source_project_flock_kandang_id, rec.project_flock_kandangs_id) IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", utils.FlagAyamCulling).
|
||||
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
|
||||
Scan(&agg).Error
|
||||
@@ -358,52 +360,14 @@ func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
return r.sumMarketingAttributedByProjectFlockKandangIDs(ctx, projectFlockKandangIDs, nil)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(mdp.total_weight), 0) AS total_weight, COALESCE(SUM(mdp.usage_qty), 0) AS total_qty, COALESCE(SUM(mdp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
return r.sumMarketingAttributedByProjectFlockKandangIDs(ctx, projectFlockKandangIDs, flagNames)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
|
||||
@@ -417,10 +381,11 @@ func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFla
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs re").
|
||||
Joins("JOIN recordings rec ON rec.id = re.recording_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("COALESCE(re.project_flock_kandang_id, rec.project_flock_kandangs_id) IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
|
||||
Scan(&agg).Error
|
||||
@@ -817,6 +782,52 @@ type SapronakDetailRow struct {
|
||||
|
||||
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
|
||||
|
||||
func (r *ClosingRepositoryImpl) sumMarketingAttributedByProjectFlockKandangIDs(
|
||||
ctx context.Context,
|
||||
projectFlockKandangIDs []uint,
|
||||
flagNames []string,
|
||||
) (float64, float64, float64, error) {
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
query := r.withCtx(ctx).
|
||||
Table("(?) AS mda", repository.MarketingDeliveryAttributionRowsQuery(r.withCtx(ctx))).
|
||||
Joins("JOIN marketing_delivery_products mdp ON mdp.id = mda.marketing_delivery_product_id").
|
||||
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Where("mda.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("mdp.delivery_date IS NOT NULL")
|
||||
|
||||
if len(flagNames) > 0 {
|
||||
query = query.
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("f.name IN ?", flagNames)
|
||||
}
|
||||
|
||||
err := query.
|
||||
Select(`
|
||||
COALESCE(SUM(CASE
|
||||
WHEN COALESCE(mdp.usage_qty, 0) > 0 THEN mdp.total_weight * (mda.allocated_qty / mdp.usage_qty)
|
||||
ELSE 0
|
||||
END), 0) AS total_weight,
|
||||
COALESCE(SUM(mda.allocated_qty), 0) AS total_qty,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN COALESCE(mdp.usage_qty, 0) > 0 THEN mdp.total_price * (mda.allocated_qty / mdp.usage_qty)
|
||||
ELSE 0
|
||||
END), 0) AS total_price
|
||||
`).
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
}
|
||||
|
||||
func applyDateRange(db *gorm.DB, column string, start, end *time.Time) *gorm.DB {
|
||||
if start != nil {
|
||||
db = db.Where(column+"::date >= ?", start)
|
||||
@@ -1453,6 +1464,16 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
|
||||
attributedProjectFlockKandangExpr := `
|
||||
COALESCE(
|
||||
pc.project_flock_kandang_id,
|
||||
pi.project_flock_kandang_id,
|
||||
source_pw.project_flock_kandang_id,
|
||||
ltt.target_project_flock_kandang_id,
|
||||
pw.project_flock_kandang_id
|
||||
)
|
||||
`
|
||||
|
||||
query := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
@@ -1470,9 +1491,15 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF
|
||||
Joins("JOIN marketings m ON m.id = mp.marketing_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
|
||||
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
|
||||
Joins("LEFT JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id").
|
||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where(attributedProjectFlockKandangExpr+" = ?", projectFlockKandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
|
||||
|
||||
@@ -1548,6 +1575,16 @@ func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.C
|
||||
END
|
||||
`, pfpType)
|
||||
|
||||
attributedProjectFlockKandangExpr := `
|
||||
COALESCE(
|
||||
pc.project_flock_kandang_id,
|
||||
pi.project_flock_kandang_id,
|
||||
source_pw.project_flock_kandang_id,
|
||||
ltt.target_project_flock_kandang_id,
|
||||
pw_sales.project_flock_kandang_id
|
||||
)
|
||||
`
|
||||
|
||||
query := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(fmt.Sprintf(`
|
||||
@@ -1600,6 +1637,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.C
|
||||
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
|
||||
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
|
||||
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||
Joins("LEFT JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id").
|
||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
||||
Joins("LEFT JOIN product_warehouses pw_ltt ON pw_ltt.id = ltt.product_warehouse_id").
|
||||
@@ -1619,7 +1657,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.C
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("sa.allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where(attributedProjectFlockKandangExpr+" = ?", projectFlockKandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Group(`
|
||||
p_resolve.id, p_resolve.name, f.name,
|
||||
|
||||
+58
-32
@@ -84,31 +84,28 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWareho
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT NULL", productId, warehouseId).
|
||||
Order("id DESC").
|
||||
Preload("ProjectFlockKandang").
|
||||
First(&productWarehouse).Error
|
||||
|
||||
if err == nil {
|
||||
|
||||
if productWarehouse.ProjectFlockKandang.ClosedAt == nil {
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = r.DB().WithContext(ctx).
|
||||
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NULL", productId, warehouseId).
|
||||
First(&productWarehouse).Error
|
||||
|
||||
warehouseIsKandang, err := r.isKandangWarehouse(ctx, warehouseId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &productWarehouse, nil
|
||||
if warehouseIsKandang {
|
||||
if productWarehouse, err := r.findOpenKandangOwnedWarehouse(ctx, productId, warehouseId); err == nil {
|
||||
return productWarehouse, nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.findSharedWarehouse(ctx, productId, warehouseId)
|
||||
}
|
||||
|
||||
if productWarehouse, err := r.findSharedWarehouse(ctx, productId, warehouseId); err == nil {
|
||||
return productWarehouse, nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.findOpenKandangOwnedWarehouse(ctx, productId, warehouseId)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error) {
|
||||
@@ -266,18 +263,8 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
||||
projectFlockKandangID *uint,
|
||||
createdBy uint,
|
||||
) (uint, error) {
|
||||
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||
record, err := r.FindByProductWarehouseAndPfk(ctx, productID, warehouseID, projectFlockKandangID)
|
||||
if err == nil {
|
||||
// Backfill project_flock_kandang_id when it's missing and caller provides one.
|
||||
if projectFlockKandangID != nil && (record.ProjectFlockKandangId == nil || *record.ProjectFlockKandangId == 0) {
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProductWarehouse{}).
|
||||
Where("id = ?", record.Id).
|
||||
Update("project_flock_kandang_id", *projectFlockKandangID).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
record.ProjectFlockKandangId = projectFlockKandangID
|
||||
}
|
||||
return record.Id, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -301,6 +288,45 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
||||
return entity.Id, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) isKandangWarehouse(ctx context.Context, warehouseID uint) (bool, error) {
|
||||
var kandangID *uint
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Table("warehouses").
|
||||
Select("kandang_id").
|
||||
Where("id = ?", warehouseID).
|
||||
Scan(&kandangID).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return kandangID != nil && *kandangID != 0, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) findOpenKandangOwnedWarehouse(ctx context.Context, productID uint, warehouseID uint) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err := r.DB().WithContext(ctx).
|
||||
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT NULL", productID, warehouseID).
|
||||
Order("id DESC").
|
||||
Preload("ProjectFlockKandang").
|
||||
First(&productWarehouse).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if productWarehouse.ProjectFlockKandang != nil && productWarehouse.ProjectFlockKandang.ClosedAt == nil {
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) findSharedWarehouse(ctx context.Context, productID uint, warehouseID uint) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NULL", productID, warehouseID).
|
||||
Preload("ProjectFlockKandang").
|
||||
First(&productWarehouse).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetByProductWarehouseAndProjectFlockKandang(
|
||||
ctx context.Context,
|
||||
productId uint,
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestGetProductWarehouseByProductAndWarehouseIDPrefersSharedForFarmWarehouse(t *testing.T) {
|
||||
db := setupProductWarehouseRepoTestDB(t)
|
||||
repo := NewProductWarehouseRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
insertProductWarehouseTestFixtures(t, db)
|
||||
|
||||
got, err := repo.GetProductWarehouseByProductAndWarehouseID(ctx, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got.Id != 2 {
|
||||
t.Fatalf("expected shared farm warehouse id 2, got %d", got.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProductWarehouseByProductAndWarehouseIDPrefersOpenKandangOwnedForKandangWarehouse(t *testing.T) {
|
||||
db := setupProductWarehouseRepoTestDB(t)
|
||||
repo := NewProductWarehouseRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
insertProductWarehouseTestFixtures(t, db)
|
||||
|
||||
got, err := repo.GetProductWarehouseByProductAndWarehouseID(ctx, 1, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got.Id != 3 {
|
||||
t.Fatalf("expected kandang-owned warehouse id 3, got %d", got.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureProductWarehouseDoesNotBackfillSharedWarehouse(t *testing.T) {
|
||||
db := setupProductWarehouseRepoTestDB(t)
|
||||
repo := NewProductWarehouseRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
insertProductWarehouseTestFixtures(t, db)
|
||||
|
||||
projectFlockKandangID := uint(101)
|
||||
createdID, err := repo.EnsureProductWarehouse(ctx, 1, 1, &projectFlockKandangID, 9)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if createdID == 2 {
|
||||
t.Fatalf("expected new kandang-attributed row instead of reusing shared row")
|
||||
}
|
||||
|
||||
var sharedPfkID *uint
|
||||
if err := db.WithContext(ctx).
|
||||
Table("product_warehouses").
|
||||
Select("project_flock_kandang_id").
|
||||
Where("id = ?", 2).
|
||||
Scan(&sharedPfkID).Error; err != nil {
|
||||
t.Fatalf("failed to load shared warehouse row: %v", err)
|
||||
}
|
||||
if sharedPfkID != nil {
|
||||
t.Fatalf("expected shared row attribution to stay nil, got %v", *sharedPfkID)
|
||||
}
|
||||
}
|
||||
|
||||
func setupProductWarehouseRepoTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed opening sqlite db: %v", err)
|
||||
}
|
||||
|
||||
statements := []string{
|
||||
`CREATE TABLE warehouses (id INTEGER PRIMARY KEY, kandang_id INTEGER NULL)`,
|
||||
`CREATE TABLE project_flock_kandangs (id INTEGER PRIMARY KEY, closed_at TIMESTAMP NULL)`,
|
||||
`CREATE TABLE product_warehouses (
|
||||
id INTEGER PRIMARY KEY,
|
||||
product_id INTEGER NOT NULL,
|
||||
warehouse_id INTEGER NOT NULL,
|
||||
project_flock_kandang_id INTEGER NULL,
|
||||
qty NUMERIC(15,3) NOT NULL DEFAULT 0
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed preparing schema: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func insertProductWarehouseTestFixtures(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
statements := []string{
|
||||
`INSERT INTO warehouses (id, kandang_id) VALUES (1, NULL), (2, 7)`,
|
||||
`INSERT INTO project_flock_kandangs (id, closed_at) VALUES (101, NULL)`,
|
||||
`INSERT INTO product_warehouses (id, product_id, warehouse_id, project_flock_kandang_id, qty) VALUES
|
||||
(1, 1, 1, 101, 10),
|
||||
(2, 1, 1, NULL, 20),
|
||||
(3, 1, 2, 101, 30),
|
||||
(4, 1, 2, NULL, 40)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed seeding fixtures: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+235
-127
@@ -2,9 +2,10 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type MarketingDeliveryProductRepository interface {
|
||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||
commonRepo.BaseRepository[entity.MarketingDeliveryProduct]
|
||||
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetClosingPenjualanByCategory(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error)
|
||||
@@ -23,26 +24,27 @@ type MarketingDeliveryProductRepository interface {
|
||||
GetUsageQty(ctx context.Context, id uint) (float64, error)
|
||||
ResetFifoFields(ctx context.Context, id uint) error
|
||||
GetClosingPenjualanForAgeChickDataProduction(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetAttributionRowsByDeliveryProductIDs(ctx context.Context, deliveryProductIDs []uint) ([]commonRepo.MarketingDeliveryAttributionRow, error)
|
||||
}
|
||||
|
||||
type MarketingDeliveryProductRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
||||
*commonRepo.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
||||
}
|
||||
|
||||
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
||||
return &MarketingDeliveryProductRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||
BaseRepositoryImpl: commonRepo.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
attributionQuery := commonRepo.MarketingDeliveryAttributionRowsQuery(r.DB().WithContext(ctx))
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Joins("JOIN (?) AS mda ON mda.marketing_delivery_product_id = marketing_delivery_products.id", attributionQuery).
|
||||
Where("mda.project_flock_id = ?", projectFlockID).
|
||||
Distinct("marketing_delivery_products.*")
|
||||
|
||||
if callback != nil {
|
||||
@@ -57,139 +59,50 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualan(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
||||
Distinct("marketing_delivery_products.*")
|
||||
|
||||
if projectFlockKandangID != nil {
|
||||
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
|
||||
}
|
||||
|
||||
db = db.
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||
Preload("MarketingProduct.Marketing").
|
||||
Preload("MarketingProduct.Marketing.Customer").
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
|
||||
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||
attributionRows, err := r.getClosingAttributionRows(ctx, projectFlockID, projectFlockKandangID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deliveryProducts, nil
|
||||
return r.fetchClosingDeliveryProducts(ctx, attributionRows, projectFlockKandangID)
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanForAgeChickDataProduction(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Where("flags.name IN (?)", []string{
|
||||
string(utils.FlagAyamAfkir),
|
||||
string(utils.FlagAyamCulling),
|
||||
string(utils.FlagPullet),
|
||||
string(utils.FlagLayer),
|
||||
}).
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
||||
Distinct("marketing_delivery_products.*")
|
||||
|
||||
if projectFlockKandangID != nil {
|
||||
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
|
||||
}
|
||||
|
||||
db = db.
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
|
||||
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||
attributionRows, err := r.getClosingAttributionRows(ctx, projectFlockID, projectFlockKandangID, []string{
|
||||
string(utils.FlagAyamAfkir),
|
||||
string(utils.FlagAyamCulling),
|
||||
string(utils.FlagPullet),
|
||||
string(utils.FlagLayer),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deliveryProducts, nil
|
||||
return r.fetchClosingDeliveryProducts(ctx, attributionRows, projectFlockKandangID)
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetClosingPenjualanByCategory(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint, category string) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
||||
Distinct("marketing_delivery_products.*")
|
||||
|
||||
if projectFlockKandangID != nil {
|
||||
db = db.Where("product_warehouses.project_flock_kandang_id = ?", *projectFlockKandangID)
|
||||
flagNames := []string{
|
||||
string(utils.FlagDOC),
|
||||
string(utils.FlagPullet),
|
||||
string(utils.FlagLayer),
|
||||
string(utils.FlagAyamAfkir),
|
||||
string(utils.FlagAyamCulling),
|
||||
string(utils.FlagAyamMati),
|
||||
}
|
||||
|
||||
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||
db = db.Where("flags.name IN (?)", []string{
|
||||
flagNames = []string{
|
||||
string(utils.FlagTelur),
|
||||
string(utils.FlagTelurUtuh),
|
||||
string(utils.FlagTelurPecah),
|
||||
string(utils.FlagTelurPutih),
|
||||
string(utils.FlagTelurRetak),
|
||||
})
|
||||
} else {
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
db = db.
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||
Preload("MarketingProduct.Marketing").
|
||||
Preload("MarketingProduct.Marketing.Customer").
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
|
||||
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||
attributionRows, err := r.getClosingAttributionRows(ctx, projectFlockID, projectFlockKandangID, flagNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deliveryProducts, nil
|
||||
return r.fetchClosingDeliveryProducts(ctx, attributionRows, projectFlockKandangID)
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
@@ -219,12 +132,199 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con
|
||||
return &deliveryProduct, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetAttributionRowsByDeliveryProductIDs(ctx context.Context, deliveryProductIDs []uint) ([]commonRepo.MarketingDeliveryAttributionRow, error) {
|
||||
if len(deliveryProductIDs) == 0 {
|
||||
return []commonRepo.MarketingDeliveryAttributionRow{}, nil
|
||||
}
|
||||
|
||||
var rows []commonRepo.MarketingDeliveryAttributionRow
|
||||
query := r.DB().WithContext(ctx).
|
||||
Table("(?) AS mda", commonRepo.MarketingDeliveryAttributionRowsQuery(r.DB().WithContext(ctx))).
|
||||
Where("mda.marketing_delivery_product_id IN ?", deliveryProductIDs).
|
||||
Order("mda.marketing_delivery_product_id ASC, mda.project_flock_kandang_id ASC")
|
||||
|
||||
if err := query.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) getClosingAttributionRows(
|
||||
ctx context.Context,
|
||||
projectFlockID uint,
|
||||
projectFlockKandangID *uint,
|
||||
flagNames []string,
|
||||
) ([]commonRepo.MarketingDeliveryAttributionRow, error) {
|
||||
var rows []commonRepo.MarketingDeliveryAttributionRow
|
||||
|
||||
attributionQuery := commonRepo.MarketingDeliveryAttributionRowsQuery(r.DB().WithContext(ctx))
|
||||
query := r.DB().WithContext(ctx).
|
||||
Table("(?) AS mda", attributionQuery).
|
||||
Joins("JOIN marketing_delivery_products mdp ON mdp.id = mda.marketing_delivery_product_id").
|
||||
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Where("mda.project_flock_id = ?", projectFlockID).
|
||||
Where("mdp.delivery_date IS NOT NULL")
|
||||
|
||||
if projectFlockKandangID != nil {
|
||||
query = query.Where("mda.project_flock_kandang_id = ?", *projectFlockKandangID)
|
||||
}
|
||||
if len(flagNames) > 0 {
|
||||
query = query.
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("f.name IN ?", flagNames)
|
||||
}
|
||||
|
||||
query = query.
|
||||
Select(`
|
||||
mda.marketing_delivery_product_id,
|
||||
mda.project_flock_kandang_id,
|
||||
mda.project_flock_id,
|
||||
mda.project_flock_category,
|
||||
SUM(mda.allocated_qty) AS allocated_qty
|
||||
`).
|
||||
Group(`
|
||||
mda.marketing_delivery_product_id,
|
||||
mda.project_flock_kandang_id,
|
||||
mda.project_flock_id,
|
||||
mda.project_flock_category
|
||||
`).
|
||||
Order("mda.marketing_delivery_product_id ASC, mda.project_flock_kandang_id ASC")
|
||||
|
||||
if err := query.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) fetchClosingDeliveryProducts(
|
||||
ctx context.Context,
|
||||
attributionRows []commonRepo.MarketingDeliveryAttributionRow,
|
||||
projectFlockKandangID *uint,
|
||||
) ([]entity.MarketingDeliveryProduct, error) {
|
||||
deliveryIDs := orderedDeliveryProductIDs(attributionRows)
|
||||
if len(deliveryIDs) == 0 {
|
||||
return []entity.MarketingDeliveryProduct{}, nil
|
||||
}
|
||||
|
||||
query := r.closingDeliveryProductsQuery(ctx).
|
||||
Where("marketing_delivery_products.id IN ?", deliveryIDs).
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
|
||||
if projectFlockKandangID == nil {
|
||||
query = query.Joins(
|
||||
"LEFT JOIN (?) AS mda_single ON mda_single.marketing_delivery_product_id = marketing_delivery_products.id",
|
||||
commonRepo.MarketingDeliverySingleAttributionQuery(r.DB().WithContext(ctx)),
|
||||
).Select("marketing_delivery_products.*, mda_single.attributed_project_flock_kandang_id")
|
||||
}
|
||||
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
if err := query.Find(&deliveryProducts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if projectFlockKandangID == nil {
|
||||
return deliveryProducts, nil
|
||||
}
|
||||
|
||||
return scaleDeliveryProductsByAttribution(deliveryProducts, attributionRows, *projectFlockKandangID), nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) closingDeliveryProductsQuery(ctx context.Context) *gorm.DB {
|
||||
return r.DB().WithContext(ctx).
|
||||
Model(&entity.MarketingDeliveryProduct{}).
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||
Preload("MarketingProduct.Marketing").
|
||||
Preload("MarketingProduct.Marketing.Customer").
|
||||
Preload("AttributedProjectFlockKandang").
|
||||
Preload("AttributedProjectFlockKandang.ProjectFlock").
|
||||
Preload("AttributedProjectFlockKandang.Kandang").
|
||||
Preload("AttributedProjectFlockKandang.Chickins")
|
||||
}
|
||||
|
||||
func orderedDeliveryProductIDs(rows []commonRepo.MarketingDeliveryAttributionRow) []uint {
|
||||
seen := make(map[uint]struct{}, len(rows))
|
||||
ids := make([]uint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row.MarketingDeliveryProductID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[row.MarketingDeliveryProductID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[row.MarketingDeliveryProductID] = struct{}{}
|
||||
ids = append(ids, row.MarketingDeliveryProductID)
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
return ids
|
||||
}
|
||||
|
||||
func scaleDeliveryProductsByAttribution(
|
||||
deliveryProducts []entity.MarketingDeliveryProduct,
|
||||
rows []commonRepo.MarketingDeliveryAttributionRow,
|
||||
projectFlockKandangID uint,
|
||||
) []entity.MarketingDeliveryProduct {
|
||||
if len(deliveryProducts) == 0 || projectFlockKandangID == 0 {
|
||||
return deliveryProducts
|
||||
}
|
||||
|
||||
totalByDelivery := make(map[uint]float64, len(rows))
|
||||
selectedByDelivery := make(map[uint]float64, len(rows))
|
||||
for _, row := range rows {
|
||||
totalByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty
|
||||
if row.ProjectFlockKandangID == projectFlockKandangID {
|
||||
selectedByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty
|
||||
}
|
||||
}
|
||||
|
||||
filtered := make([]entity.MarketingDeliveryProduct, 0, len(deliveryProducts))
|
||||
for _, delivery := range deliveryProducts {
|
||||
selectedQty := selectedByDelivery[delivery.Id]
|
||||
totalQty := totalByDelivery[delivery.Id]
|
||||
if selectedQty <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
share := 1.0
|
||||
if totalQty > 0 {
|
||||
share = selectedQty / totalQty
|
||||
}
|
||||
|
||||
cloned := delivery
|
||||
cloned.AttributedProjectFlockKandangId = &projectFlockKandangID
|
||||
cloned.UsageQty = selectedQty
|
||||
cloned.PendingQty = 0
|
||||
cloned.TotalWeight = delivery.TotalWeight * share
|
||||
cloned.TotalPrice = delivery.TotalPrice * share
|
||||
filtered = append(filtered, cloned)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
var total int64
|
||||
|
||||
baseDB := r.DB().WithContext(ctx)
|
||||
singleAttributionQuery := commonRepo.MarketingDeliverySingleAttributionQuery(baseDB)
|
||||
db := r.DB().WithContext(ctx).
|
||||
Model(&entity.MarketingDeliveryProduct{}).
|
||||
Select("marketing_delivery_products.*, mda_single.attributed_project_flock_kandang_id").
|
||||
Joins("LEFT JOIN (?) AS mda_single ON mda_single.marketing_delivery_product_id = marketing_delivery_products.id", singleAttributionQuery).
|
||||
Preload("MarketingProduct", func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("Marketing").
|
||||
@@ -237,6 +337,9 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
Preload("ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock")
|
||||
}).
|
||||
Preload("AttributedProjectFlockKandang").
|
||||
Preload("AttributedProjectFlockKandang.ProjectFlock").
|
||||
Preload("AttributedProjectFlockKandang.Kandang").
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id").
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL")
|
||||
@@ -292,22 +395,27 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
}
|
||||
|
||||
if filters.AreaId > 0 || filters.LocationId > 0 || filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil {
|
||||
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")
|
||||
|
||||
buildAttrFilter := func() *gorm.DB {
|
||||
return r.DB().WithContext(ctx).
|
||||
Table("(?) AS mda", commonRepo.MarketingDeliveryAttributionRowsQuery(r.DB().WithContext(ctx))).
|
||||
Select("1").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = mda.project_flock_kandang_id").
|
||||
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
||||
Where("mda.marketing_delivery_product_id = marketing_delivery_products.id")
|
||||
}
|
||||
if filters.AreaId > 0 {
|
||||
db = db.Where("project_flocks.area_id = ?", filters.AreaId)
|
||||
db = db.Where("EXISTS (?)", buildAttrFilter().Where("pf.area_id = ?", filters.AreaId))
|
||||
}
|
||||
|
||||
if filters.LocationId > 0 {
|
||||
db = db.Where("project_flocks.location_id = ?", filters.LocationId)
|
||||
db = db.Where("EXISTS (?)", buildAttrFilter().Where("pf.location_id = ?", filters.LocationId))
|
||||
}
|
||||
|
||||
if filters.AllowedAreaIDs != nil {
|
||||
if len(filters.AllowedAreaIDs) == 0 {
|
||||
db = db.Where("1 = 0")
|
||||
} else {
|
||||
db = db.Where("project_flocks.area_id IN ?", filters.AllowedAreaIDs)
|
||||
db = db.Where("EXISTS (?)", buildAttrFilter().Where("pf.area_id IN ?", filters.AllowedAreaIDs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +423,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
if len(filters.AllowedLocationIDs) == 0 {
|
||||
db = db.Where("1 = 0")
|
||||
} else {
|
||||
db = db.Where("project_flocks.location_id IN ?", filters.AllowedLocationIDs)
|
||||
db = db.Where("EXISTS (?)", buildAttrFilter().Where("pf.location_id IN ?", filters.AllowedLocationIDs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
func TestScaleDeliveryProductsByAttribution(t *testing.T) {
|
||||
projectFlockKandangID := uint(101)
|
||||
|
||||
deliveryProducts := []entity.MarketingDeliveryProduct{
|
||||
{
|
||||
Id: 55,
|
||||
UsageQty: 100,
|
||||
TotalWeight: 180,
|
||||
TotalPrice: 3600,
|
||||
},
|
||||
}
|
||||
attributionRows := []commonRepo.MarketingDeliveryAttributionRow{
|
||||
{MarketingDeliveryProductID: 55, ProjectFlockKandangID: 101, AllocatedQty: 60},
|
||||
{MarketingDeliveryProductID: 55, ProjectFlockKandangID: 102, AllocatedQty: 40},
|
||||
}
|
||||
|
||||
got := scaleDeliveryProductsByAttribution(deliveryProducts, attributionRows, projectFlockKandangID)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected 1 scaled delivery, got %d", len(got))
|
||||
}
|
||||
if got[0].UsageQty != 60 {
|
||||
t.Fatalf("expected usage qty 60, got %.2f", got[0].UsageQty)
|
||||
}
|
||||
if got[0].TotalWeight != 108 {
|
||||
t.Fatalf("expected total weight 108, got %.2f", got[0].TotalWeight)
|
||||
}
|
||||
if got[0].TotalPrice != 2160 {
|
||||
t.Fatalf("expected total price 2160, got %.2f", got[0].TotalPrice)
|
||||
}
|
||||
if got[0].AttributedProjectFlockKandangId == nil || *got[0].AttributedProjectFlockKandangId != projectFlockKandangID {
|
||||
t.Fatalf("expected attributed kandang id %d, got %+v", projectFlockKandangID, got[0].AttributedProjectFlockKandangId)
|
||||
}
|
||||
}
|
||||
@@ -643,6 +643,11 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
||||
return nil
|
||||
}
|
||||
|
||||
affectedKandangIDs, err := s.marketingPopulationKandangIDsFromActiveAllocations(ctx, tx, deliveryProduct.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deliveryProduct.UsageQty = 0
|
||||
deliveryProduct.PendingQty = 0
|
||||
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
||||
@@ -670,6 +675,9 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
||||
if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.resyncPopulationUsageByKandangIDs(ctx, tx, affectedKandangIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releasedUsage := currentUsage - deliveryProduct.UsageQty
|
||||
if actorID > 0 && releasedUsage > 0 {
|
||||
@@ -725,29 +733,378 @@ func (s deliveryOrdersService) allocatePopulationForMarketingDelivery(
|
||||
return nil
|
||||
}
|
||||
|
||||
pw, err := s.ProductWarehouseRepo.WithTx(tx).GetByID(ctx, productWarehouseID, nil)
|
||||
exactAllocations, err := s.findDirectPopulationAllocationsForMarketing(ctx, tx, deliveryProduct.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pw.ProjectFlockKandangId == nil || *pw.ProjectFlockKandangId == 0 {
|
||||
if len(exactAllocations) > 0 {
|
||||
if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.applyDirectPopulationAllocationsForMarketing(ctx, tx, productWarehouseID, deliveryProduct.Id, exactAllocations); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingAllocationKandangIDs(exactAllocations))
|
||||
}
|
||||
|
||||
sourceGroups, err := s.findPopulationSourceGroupsForMarketing(ctx, tx, deliveryProduct.Id, productWarehouseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sourceGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
populations, err := s.ProjectFlockPopulationRepo.WithTx(tx).GetByProjectFlockKandangIDAndProductWarehouseID(ctx, *pw.ProjectFlockKandangId, productWarehouseID)
|
||||
if err != nil {
|
||||
if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(populations) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk delivery")
|
||||
for _, group := range sourceGroups {
|
||||
populations, err := s.ProjectFlockPopulationRepo.WithTx(tx).GetByProjectFlockKandangIDAndProductWarehouseID(
|
||||
ctx,
|
||||
group.ProjectFlockKandangID,
|
||||
group.ProductWarehouseID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(populations) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk delivery")
|
||||
}
|
||||
if err := s.allocatePopulationConsumptionWithoutRelease(
|
||||
ctx,
|
||||
tx,
|
||||
populations,
|
||||
productWarehouseID,
|
||||
deliveryProduct.Id,
|
||||
group.Qty,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingSourceGroupKandangIDs(sourceGroups))
|
||||
}
|
||||
|
||||
type marketingPopulationAllocation struct {
|
||||
ProjectFlockPopulationID uint `gorm:"column:project_flock_population_id"`
|
||||
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
|
||||
Qty float64 `gorm:"column:qty"`
|
||||
}
|
||||
|
||||
type marketingPopulationSourceGroup struct {
|
||||
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
|
||||
ProductWarehouseID uint `gorm:"column:product_warehouse_id"`
|
||||
Qty float64 `gorm:"column:qty"`
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) findDirectPopulationAllocationsForMarketing(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
deliveryProductID uint,
|
||||
) ([]marketingPopulationAllocation, error) {
|
||||
var rows []marketingPopulationAllocation
|
||||
err := tx.WithContext(ctx).
|
||||
Table("stock_allocations sa").
|
||||
Select(`
|
||||
pfp.id AS project_flock_population_id,
|
||||
pc.project_flock_kandang_id AS project_flock_kandang_id,
|
||||
SUM(sa.qty) AS qty
|
||||
`).
|
||||
Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
deliveryProductID,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Group("pfp.id, pc.project_flock_kandang_id").
|
||||
Order("pfp.id ASC").
|
||||
Scan(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) findPopulationSourceGroupsForMarketing(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
deliveryProductID uint,
|
||||
productWarehouseID uint,
|
||||
) ([]marketingPopulationSourceGroup, error) {
|
||||
groups := make(map[string]marketingPopulationSourceGroup)
|
||||
|
||||
appendGroup := func(projectFlockKandangID uint, sourceProductWarehouseID uint, qty float64) {
|
||||
if projectFlockKandangID == 0 || sourceProductWarehouseID == 0 || qty <= 0 {
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf("%d:%d", projectFlockKandangID, sourceProductWarehouseID)
|
||||
current := groups[key]
|
||||
current.ProjectFlockKandangID = projectFlockKandangID
|
||||
current.ProductWarehouseID = sourceProductWarehouseID
|
||||
current.Qty += qty
|
||||
groups[key] = current
|
||||
}
|
||||
|
||||
return fifoV2.AllocatePopulationConsumption(
|
||||
ctx,
|
||||
tx,
|
||||
populations,
|
||||
productWarehouseID,
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
deliveryProduct.Id,
|
||||
deliveryProduct.UsageQty,
|
||||
)
|
||||
var transferRows []marketingPopulationSourceGroup
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("stock_allocations sa").
|
||||
Select(`
|
||||
source_pw.project_flock_kandang_id AS project_flock_kandang_id,
|
||||
std.source_product_warehouse_id AS product_warehouse_id,
|
||||
SUM(sa.qty) AS qty
|
||||
`).
|
||||
Joins("JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
|
||||
Joins("JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id").
|
||||
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
deliveryProductID,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Where("source_pw.project_flock_kandang_id IS NOT NULL").
|
||||
Group("source_pw.project_flock_kandang_id, std.source_product_warehouse_id").
|
||||
Scan(&transferRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range transferRows {
|
||||
appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
|
||||
}
|
||||
|
||||
var purchaseRows []marketingPopulationSourceGroup
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("stock_allocations sa").
|
||||
Select(`
|
||||
pi.project_flock_kandang_id AS project_flock_kandang_id,
|
||||
pi.product_warehouse_id AS product_warehouse_id,
|
||||
SUM(sa.qty) AS qty
|
||||
`).
|
||||
Joins("JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
|
||||
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
deliveryProductID,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Where("pi.project_flock_kandang_id IS NOT NULL").
|
||||
Where("pi.product_warehouse_id IS NOT NULL").
|
||||
Group("pi.project_flock_kandang_id, pi.product_warehouse_id").
|
||||
Scan(&purchaseRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range purchaseRows {
|
||||
appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
|
||||
}
|
||||
|
||||
var layingRows []marketingPopulationSourceGroup
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("stock_allocations sa").
|
||||
Select(`
|
||||
ltt.target_project_flock_kandang_id AS project_flock_kandang_id,
|
||||
ltt.product_warehouse_id AS product_warehouse_id,
|
||||
SUM(sa.qty) AS qty
|
||||
`).
|
||||
Joins("JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
deliveryProductID,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Where("ltt.product_warehouse_id IS NOT NULL").
|
||||
Group("ltt.target_project_flock_kandang_id, ltt.product_warehouse_id").
|
||||
Scan(&layingRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range layingRows {
|
||||
appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
|
||||
}
|
||||
|
||||
if len(groups) == 0 {
|
||||
pw, err := s.ProductWarehouseRepo.WithTx(tx).GetByID(ctx, productWarehouseID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId != 0 {
|
||||
appendGroup(*pw.ProjectFlockKandangId, productWarehouseID, 0)
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]marketingPopulationSourceGroup, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
if group.Qty == 0 {
|
||||
group.Qty = s.resolveMarketingRequestedUsageQty(ctx, tx, deliveryProductID)
|
||||
}
|
||||
if group.Qty > 0 {
|
||||
result = append(result, group)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) applyDirectPopulationAllocationsForMarketing(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
productWarehouseID uint,
|
||||
deliveryProductID uint,
|
||||
allocations []marketingPopulationAllocation,
|
||||
) error {
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
||||
for _, allocation := range allocations {
|
||||
if allocation.ProjectFlockPopulationID == 0 || allocation.Qty <= 0 {
|
||||
continue
|
||||
}
|
||||
record := &entity.StockAllocation{
|
||||
ProductWarehouseId: productWarehouseID,
|
||||
StockableType: fifo.StockableKeyProjectFlockPopulation.String(),
|
||||
StockableId: allocation.ProjectFlockPopulationID,
|
||||
UsableType: fifo.UsableKeyMarketingDelivery.String(),
|
||||
UsableId: deliveryProductID,
|
||||
Qty: allocation.Qty,
|
||||
Status: entity.StockAllocationStatusActive,
|
||||
AllocationPurpose: entity.StockAllocationPurposeConsume,
|
||||
}
|
||||
if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id = ?", allocation.ProjectFlockPopulationID).
|
||||
Update("total_used_qty", gorm.Expr("total_used_qty + ?", allocation.Qty)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) allocatePopulationConsumptionWithoutRelease(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
populations []entity.ProjectFlockPopulation,
|
||||
productWarehouseID uint,
|
||||
deliveryProductID uint,
|
||||
consumeQty float64,
|
||||
) error {
|
||||
if consumeQty <= 0 {
|
||||
return nil
|
||||
}
|
||||
remaining := consumeQty
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
||||
for _, population := range populations {
|
||||
available := population.TotalQty - population.TotalUsedQty
|
||||
if available <= 0 {
|
||||
continue
|
||||
}
|
||||
portion := available
|
||||
if remaining < portion {
|
||||
portion = remaining
|
||||
}
|
||||
if portion <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
record := &entity.StockAllocation{
|
||||
ProductWarehouseId: productWarehouseID,
|
||||
StockableType: fifo.StockableKeyProjectFlockPopulation.String(),
|
||||
StockableId: population.Id,
|
||||
UsableType: fifo.UsableKeyMarketingDelivery.String(),
|
||||
UsableId: deliveryProductID,
|
||||
Qty: portion,
|
||||
Status: entity.StockAllocationStatusActive,
|
||||
AllocationPurpose: entity.StockAllocationPurposeConsume,
|
||||
}
|
||||
if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("id = ?", population.Id).
|
||||
Update("total_used_qty", gorm.Expr("total_used_qty + ?", portion)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remaining -= portion
|
||||
if remaining <= 0.000001 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if remaining > 0.000001 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak mencukupi")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) marketingPopulationKandangIDsFromActiveAllocations(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
deliveryProductID uint,
|
||||
) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := tx.WithContext(ctx).
|
||||
Table("stock_allocations sa").
|
||||
Distinct("pc.project_flock_kandang_id").
|
||||
Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
||||
fifo.UsableKeyMarketingDelivery.String(),
|
||||
deliveryProductID,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Pluck("pc.project_flock_kandang_id", &ids).Error
|
||||
return ids, err
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) resyncPopulationUsageByKandangIDs(ctx context.Context, tx *gorm.DB, kandangIDs []uint) error {
|
||||
for _, kandangID := range uniqueUintIDs(kandangIDs) {
|
||||
if kandangID == 0 {
|
||||
continue
|
||||
}
|
||||
if err := s.ProjectFlockPopulationRepo.WithTx(tx).ResyncUsageByProjectFlockKandangID(ctx, tx, kandangID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) resolveMarketingRequestedUsageQty(ctx context.Context, tx *gorm.DB, deliveryProductID uint) float64 {
|
||||
var usageQty float64
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("marketing_delivery_products").
|
||||
Select("usage_qty").
|
||||
Where("id = ?", deliveryProductID).
|
||||
Scan(&usageQty).Error; err != nil {
|
||||
return 0
|
||||
}
|
||||
return usageQty
|
||||
}
|
||||
|
||||
func marketingAllocationKandangIDs(rows []marketingPopulationAllocation) []uint {
|
||||
ids := make([]uint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, row.ProjectFlockKandangID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func marketingSourceGroupKandangIDs(rows []marketingPopulationSourceGroup) []uint {
|
||||
ids := make([]uint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
ids = append(ids, row.ProjectFlockKandangID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func uniqueUintIDs(ids []uint) []uint {
|
||||
seen := make(map[uint]struct{}, len(ids))
|
||||
result := make([]uint, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
+56
@@ -18,6 +18,7 @@ type ProjectFlockPopulationRepository interface {
|
||||
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error)
|
||||
ResyncUsageByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) 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
|
||||
@@ -167,3 +168,58 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKand
|
||||
|
||||
return int64(math.Round(total)), nil
|
||||
}
|
||||
|
||||
func (r *projectFlockPopulationRepositoryImpl) ResyncUsageByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
idsSubquery := `
|
||||
SELECT pfp.id
|
||||
FROM project_flock_populations pfp
|
||||
JOIN project_chickins pc ON pc.id = pfp.project_chickin_id
|
||||
WHERE pc.project_flock_kandang_id = ?
|
||||
`
|
||||
|
||||
updateWithAlloc := `
|
||||
UPDATE project_flock_populations p
|
||||
SET total_used_qty = COALESCE(a.used, 0)
|
||||
FROM (
|
||||
SELECT stockable_id, SUM(qty) AS used
|
||||
FROM stock_allocations
|
||||
WHERE stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||
AND status = 'ACTIVE'
|
||||
AND allocation_purpose = 'CONSUME'
|
||||
GROUP BY stockable_id
|
||||
) a
|
||||
WHERE p.id = a.stockable_id
|
||||
AND p.id IN (` + idsSubquery + `)
|
||||
`
|
||||
|
||||
resetMissing := `
|
||||
UPDATE project_flock_populations p
|
||||
SET total_used_qty = 0
|
||||
WHERE p.id IN (` + idsSubquery + `)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM stock_allocations sa
|
||||
WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||
AND sa.status = 'ACTIVE'
|
||||
AND sa.allocation_purpose = 'CONSUME'
|
||||
AND sa.stockable_id = p.id
|
||||
)
|
||||
`
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
if err := db.Exec(updateWithAlloc, projectFlockKandangID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Exec(resetMissing, projectFlockKandangID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -366,6 +366,15 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
if err := s.ensureProductWarehousesByFlags(ctx, depletionIDs, []string{"AYAM-AFKIR", "AYAM-CULLING", "AYAM-MATI"}, "depletion"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
depletionSourceIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint {
|
||||
if d.SourceProductWarehouseId == nil {
|
||||
return 0
|
||||
}
|
||||
return *d.SourceProductWarehouseId
|
||||
})
|
||||
if err := s.ensureProductWarehousesByFlags(ctx, depletionSourceIDs, []string{"DOC", "PULLET", "LAYER"}, "depletion source"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eggIDs := recordingutil.CollectWarehouseIDs(req.Eggs, func(e validation.Egg) uint { return e.ProductWarehouseId })
|
||||
if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR-UTUH", "TELUR-PECAH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR"}, "egg"); err != nil {
|
||||
return nil, err
|
||||
@@ -435,11 +444,16 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
if err := s.ensureDepletionWithinPopulation(ctx, tx, req.ProjectFlockKandangId, sumDepletionQty(mappedDepletions), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceProjectFlockKandangID := req.ProjectFlockKandangId
|
||||
for i := range mappedDepletions {
|
||||
mappedDepletions[i].SourceProjectFlockKandangId = &sourceProjectFlockKandangID
|
||||
if mappedDepletions[i].SourceProductWarehouseId != nil && *mappedDepletions[i].SourceProductWarehouseId != 0 {
|
||||
continue
|
||||
}
|
||||
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, req.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
|
||||
}
|
||||
}
|
||||
@@ -460,7 +474,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
return err
|
||||
}
|
||||
|
||||
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs)
|
||||
mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.ProjectFlockKandangId, createdRecording.CreatedBy, req.Eggs)
|
||||
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
|
||||
s.Log.Errorf("Failed to persist eggs: %+v", err)
|
||||
return err
|
||||
@@ -590,13 +604,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||
return err
|
||||
}
|
||||
existingTotals := recordingutil.TotalsByWarehouse(existingDepletions, func(dep entity.RecordingDepletion) (uint, float64) {
|
||||
return dep.ProductWarehouseId, dep.Qty
|
||||
existingTotals := recordingutil.DepletionTotalsByRoute(existingDepletions, func(dep entity.RecordingDepletion) (uint, *uint, float64) {
|
||||
return dep.ProductWarehouseId, dep.SourceProductWarehouseId, dep.Qty
|
||||
})
|
||||
incomingTotals := recordingutil.TotalsByWarehouse(req.Depletions, func(dep validation.Depletion) (uint, float64) {
|
||||
return dep.ProductWarehouseId, dep.Qty
|
||||
incomingTotals := recordingutil.DepletionTotalsByRoute(req.Depletions, func(dep validation.Depletion) (uint, *uint, float64) {
|
||||
return dep.ProductWarehouseId, dep.SourceProductWarehouseId, dep.Qty
|
||||
})
|
||||
match := recordingutil.FloatMapsEqual(existingTotals, incomingTotals)
|
||||
match := recordingutil.DepletionRouteMapsEqual(existingTotals, incomingTotals)
|
||||
if match {
|
||||
hasDepletionChanges = false
|
||||
} else {
|
||||
@@ -613,6 +627,15 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
if err := s.ensureProductWarehousesByFlags(ctx, depletionIDs, []string{"AYAM-AFKIR", "AYAM-CULLING", "AYAM-MATI"}, "depletion"); err != nil {
|
||||
return err
|
||||
}
|
||||
depletionSourceIDs := recordingutil.CollectWarehouseIDs(req.Depletions, func(d validation.Depletion) uint {
|
||||
if d.SourceProductWarehouseId == nil {
|
||||
return 0
|
||||
}
|
||||
return *d.SourceProductWarehouseId
|
||||
})
|
||||
if err := s.ensureProductWarehousesByFlags(ctx, depletionSourceIDs, []string{"DOC", "PULLET", "LAYER"}, "depletion source"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.reflowResetRecordingDepletionsOut(ctx, tx, existingDepletions, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -630,11 +653,16 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
if err := s.ensureDepletionWithinPopulation(ctx, tx, recordingEntity.ProjectFlockKandangId, sumDepletionQty(mappedDepletions), sumDepletionQty(existingDepletions)); err != nil {
|
||||
return err
|
||||
}
|
||||
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceProjectFlockKandangID := recordingEntity.ProjectFlockKandangId
|
||||
for i := range mappedDepletions {
|
||||
mappedDepletions[i].SourceProjectFlockKandangId = &sourceProjectFlockKandangID
|
||||
if mappedDepletions[i].SourceProductWarehouseId != nil && *mappedDepletions[i].SourceProductWarehouseId != 0 {
|
||||
continue
|
||||
}
|
||||
sourceWarehouseID, err := s.resolvePopulationWarehouseID(ctx, recordingEntity.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mappedDepletions[i].SourceProductWarehouseId = &sourceWarehouseID
|
||||
}
|
||||
}
|
||||
@@ -700,7 +728,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return err
|
||||
}
|
||||
|
||||
mappedEggs := recordingutil.MapEggs(recordingEntity.Id, recordingEntity.CreatedBy, req.Eggs)
|
||||
mappedEggs := recordingutil.MapEggs(recordingEntity.Id, recordingEntity.ProjectFlockKandangId, recordingEntity.CreatedBy, req.Eggs)
|
||||
if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil {
|
||||
s.Log.Errorf("Failed to update eggs: %+v", err)
|
||||
return err
|
||||
@@ -1476,6 +1504,9 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v
|
||||
if dep.ProductWarehouseId != 0 {
|
||||
idSet[dep.ProductWarehouseId] = struct{}{}
|
||||
}
|
||||
if dep.SourceProductWarehouseId != nil && *dep.SourceProductWarehouseId != 0 {
|
||||
idSet[*dep.SourceProductWarehouseId] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, egg := range eggs {
|
||||
if egg.ProductWarehouseId != 0 {
|
||||
@@ -2500,12 +2531,17 @@ func (s *recordingService) allocatePopulationForDepletion(
|
||||
}
|
||||
|
||||
var projectFlockKandangID uint
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("recordings").
|
||||
Select("project_flock_kandangs_id").
|
||||
Where("id = ?", depletion.RecordingId).
|
||||
Scan(&projectFlockKandangID).Error; err != nil {
|
||||
return err
|
||||
if depletion.SourceProjectFlockKandangId != nil {
|
||||
projectFlockKandangID = *depletion.SourceProjectFlockKandangId
|
||||
}
|
||||
if projectFlockKandangID == 0 {
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("recordings").
|
||||
Select("project_flock_kandangs_id").
|
||||
Where("id = ?", depletion.RecordingId).
|
||||
Scan(&projectFlockKandangID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if projectFlockKandangID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak ditemukan untuk depletion")
|
||||
|
||||
@@ -9,8 +9,9 @@ type (
|
||||
}
|
||||
|
||||
Depletion struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
SourceProductWarehouseId *uint `json:"source_product_warehouse_id,omitempty" validate:"omitempty,number,min=1"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
Egg struct {
|
||||
|
||||
@@ -53,17 +53,18 @@ type ProductRelationDTOFixed struct {
|
||||
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||
}
|
||||
|
||||
func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64, agingMap map[int]int) []RepportMarketingItemDTO {
|
||||
func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppByDelivery map[uint]float64, categoryByDelivery map[uint]string, agingMap map[int]int) []RepportMarketingItemDTO {
|
||||
items := make([]RepportMarketingItemDTO, 0, len(mdps))
|
||||
|
||||
for _, mdp := range mdps {
|
||||
hppPerKg := float64(0)
|
||||
category := ""
|
||||
if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
|
||||
if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists {
|
||||
hppPerKg = hpp
|
||||
hppPerKg := hppByDelivery[mdp.Id]
|
||||
category := categoryByDelivery[mdp.Id]
|
||||
if category == "" {
|
||||
if projectFlockKandang := mdp.AttributedProjectFlockKandang; projectFlockKandang != nil {
|
||||
category = projectFlockKandang.ProjectFlock.Category
|
||||
} else if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
|
||||
category = projectFlockKandang.ProjectFlock.Category
|
||||
}
|
||||
category = projectFlockKandang.ProjectFlock.Category
|
||||
}
|
||||
|
||||
soDate := time.Time{}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
@@ -215,39 +216,95 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
|
||||
}
|
||||
}
|
||||
|
||||
projectFlockIDMap := make(map[uint]bool)
|
||||
hppMap := make(map[uint]float64)
|
||||
deliveryIDs := make([]uint, 0, len(deliveryProducts))
|
||||
for _, delivery := range deliveryProducts {
|
||||
deliveryIDs = append(deliveryIDs, delivery.Id)
|
||||
}
|
||||
|
||||
for _, dp := range deliveryProducts {
|
||||
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
|
||||
projectFlockID := projectFlockKandang.ProjectFlockId
|
||||
if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] {
|
||||
projectFlockIDMap[projectFlockID] = true
|
||||
attributionRows, err := s.MarketingDeliveryRepo.GetAttributionRowsByDeliveryProductIDs(c.Context(), deliveryIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
category := projectFlockKandang.ProjectFlock.Category
|
||||
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryLaying {
|
||||
if s.HppSvc != nil {
|
||||
hppCost, err := s.HppSvc.CalculateHppCost(projectFlockID, nil)
|
||||
if err != nil {
|
||||
hppMap[projectFlockID] = 0.0
|
||||
} else if hppCost != nil {
|
||||
hppMap[projectFlockID] = hppCost.Real.HargaKg
|
||||
} else {
|
||||
hppMap[projectFlockID] = 0.0
|
||||
}
|
||||
} else {
|
||||
hppMap[projectFlockID] = 0.0
|
||||
}
|
||||
} else {
|
||||
hppByDelivery := buildMarketingHppByDelivery(c.Context(), s.HppSvc, attributionRows)
|
||||
categoryByDelivery := buildMarketingCategoryByDelivery(deliveryProducts, attributionRows)
|
||||
|
||||
hppMap[projectFlockID] = 0.0
|
||||
items := dto.ToMarketingReportItems(deliveryProducts, hppByDelivery, categoryByDelivery, agingMap)
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func buildMarketingHppByDelivery(
|
||||
ctx context.Context,
|
||||
hppSvc approvalService.HppService,
|
||||
attributionRows []commonRepo.MarketingDeliveryAttributionRow,
|
||||
) map[uint]float64 {
|
||||
if len(attributionRows) == 0 {
|
||||
return map[uint]float64{}
|
||||
}
|
||||
|
||||
hppByKandang := make(map[uint]float64)
|
||||
weightedByDelivery := make(map[uint]float64)
|
||||
totalQtyByDelivery := make(map[uint]float64)
|
||||
|
||||
for _, row := range attributionRows {
|
||||
if row.MarketingDeliveryProductID == 0 || row.ProjectFlockKandangID == 0 || row.AllocatedQty <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hppPerKg, exists := hppByKandang[row.ProjectFlockKandangID]
|
||||
if !exists {
|
||||
hppPerKg = 0
|
||||
if hppSvc != nil && utils.ProjectFlockCategory(row.ProjectFlockCategory) == utils.ProjectFlockCategoryLaying {
|
||||
if hppCost, err := hppSvc.CalculateHppCost(row.ProjectFlockKandangID, nil); err == nil && hppCost != nil {
|
||||
hppPerKg = hppCost.Real.HargaKg
|
||||
}
|
||||
}
|
||||
hppByKandang[row.ProjectFlockKandangID] = hppPerKg
|
||||
}
|
||||
|
||||
weightedByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty * hppPerKg
|
||||
totalQtyByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty
|
||||
}
|
||||
|
||||
result := make(map[uint]float64, len(totalQtyByDelivery))
|
||||
for deliveryID, totalQty := range totalQtyByDelivery {
|
||||
if totalQty <= 0 {
|
||||
continue
|
||||
}
|
||||
result[deliveryID] = weightedByDelivery[deliveryID] / totalQty
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func buildMarketingCategoryByDelivery(
|
||||
deliveryProducts []entity.MarketingDeliveryProduct,
|
||||
attributionRows []commonRepo.MarketingDeliveryAttributionRow,
|
||||
) map[uint]string {
|
||||
result := make(map[uint]string, len(deliveryProducts))
|
||||
for _, row := range attributionRows {
|
||||
if row.MarketingDeliveryProductID == 0 || strings.TrimSpace(row.ProjectFlockCategory) == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := result[row.MarketingDeliveryProductID]; !exists {
|
||||
result[row.MarketingDeliveryProductID] = row.ProjectFlockCategory
|
||||
}
|
||||
}
|
||||
|
||||
items := dto.ToMarketingReportItems(deliveryProducts, hppMap, agingMap)
|
||||
return items, total, nil
|
||||
for _, delivery := range deliveryProducts {
|
||||
if _, exists := result[delivery.Id]; exists {
|
||||
continue
|
||||
}
|
||||
if delivery.AttributedProjectFlockKandang != nil {
|
||||
result[delivery.Id] = delivery.AttributedProjectFlockKandang.ProjectFlock.Category
|
||||
continue
|
||||
}
|
||||
if delivery.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||
result[delivery.Id] = delivery.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) {
|
||||
|
||||
@@ -31,44 +31,65 @@ func MapDepletions(recordingID uint, items []validation.Depletion) []entity.Reco
|
||||
return nil
|
||||
}
|
||||
|
||||
aggregate := make(map[uint]float64, len(items))
|
||||
type depletionKey struct {
|
||||
ProductWarehouseID uint
|
||||
SourceProductWarehouseID uint
|
||||
}
|
||||
|
||||
aggregate := make(map[depletionKey]float64, len(items))
|
||||
for _, item := range items {
|
||||
if item.ProductWarehouseId == 0 || item.Qty == 0 {
|
||||
continue
|
||||
}
|
||||
aggregate[item.ProductWarehouseId] += item.Qty
|
||||
key := depletionKey{ProductWarehouseID: item.ProductWarehouseId}
|
||||
if item.SourceProductWarehouseId != nil {
|
||||
key.SourceProductWarehouseID = *item.SourceProductWarehouseId
|
||||
}
|
||||
aggregate[key] += item.Qty
|
||||
}
|
||||
if len(aggregate) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingDepletion, 0, len(aggregate))
|
||||
for warehouseID, qty := range aggregate {
|
||||
for key, qty := range aggregate {
|
||||
if qty == 0 {
|
||||
continue
|
||||
}
|
||||
var sourceWarehouseID *uint
|
||||
if key.SourceProductWarehouseID != 0 {
|
||||
sourceWarehouseID = new(uint)
|
||||
*sourceWarehouseID = key.SourceProductWarehouseID
|
||||
}
|
||||
result = append(result, entity.RecordingDepletion{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: warehouseID,
|
||||
Qty: qty,
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: key.ProductWarehouseID,
|
||||
SourceProductWarehouseId: sourceWarehouseID,
|
||||
Qty: qty,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.RecordingEgg {
|
||||
func MapEggs(recordingID uint, projectFlockKandangID uint, createdBy uint, items []validation.Egg) []entity.RecordingEgg {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingEgg, 0, len(items))
|
||||
for _, item := range items {
|
||||
var sourceProjectFlockKandangID *uint
|
||||
if projectFlockKandangID != 0 {
|
||||
sourceProjectFlockKandangID = new(uint)
|
||||
*sourceProjectFlockKandangID = projectFlockKandangID
|
||||
}
|
||||
result = append(result, entity.RecordingEgg{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
Qty: item.Qty,
|
||||
Weight: item.Weight,
|
||||
CreatedBy: createdBy,
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
ProjectFlockKandangId: sourceProjectFlockKandangID,
|
||||
Qty: item.Qty,
|
||||
Weight: item.Weight,
|
||||
CreatedBy: createdBy,
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -79,6 +100,11 @@ type EggTotals struct {
|
||||
Weight float64
|
||||
}
|
||||
|
||||
type DepletionRoute struct {
|
||||
ProductWarehouseId uint
|
||||
SourceProductWarehouseId uint
|
||||
}
|
||||
|
||||
func StockUsageByWarehouse(items []entity.RecordingStock) map[uint]float64 {
|
||||
return TotalsByWarehouse(items, func(stock entity.RecordingStock) (uint, float64) {
|
||||
var usage float64
|
||||
@@ -121,6 +147,19 @@ func EggTotalsEqual(a, b map[uint]EggTotals) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func DepletionRouteMapsEqual(a, b map[DepletionRoute]float64) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for key, value := range a {
|
||||
other, ok := b[key]
|
||||
if !ok || !floatNearlyEqual(value, other) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func floatNearlyEqual(a, b float64) bool {
|
||||
return a-b <= 0.000001 && b-a <= 0.000001
|
||||
}
|
||||
@@ -134,6 +173,19 @@ func TotalsByWarehouse[T any](items []T, get func(T) (uint, float64)) map[uint]f
|
||||
return result
|
||||
}
|
||||
|
||||
func DepletionTotalsByRoute[T any](items []T, get func(T) (uint, *uint, float64)) map[DepletionRoute]float64 {
|
||||
result := make(map[DepletionRoute]float64)
|
||||
for _, item := range items {
|
||||
productWarehouseID, sourceProductWarehouseID, qty := get(item)
|
||||
key := DepletionRoute{ProductWarehouseId: productWarehouseID}
|
||||
if sourceProductWarehouseID != nil {
|
||||
key.SourceProductWarehouseId = *sourceProductWarehouseID
|
||||
}
|
||||
result[key] += qty
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func EggTotalsByWarehouse[T any](items []T, get func(T) (uint, int, *float64)) map[uint]EggTotals {
|
||||
result := make(map[uint]EggTotals)
|
||||
for _, item := range items {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package recording
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||
)
|
||||
|
||||
func TestMapDepletionsKeepsSourceWarehouseRoutes(t *testing.T) {
|
||||
sourceA := uint(11)
|
||||
sourceB := uint(12)
|
||||
|
||||
got := MapDepletions(99, []validation.Depletion{
|
||||
{ProductWarehouseId: 21, SourceProductWarehouseId: &sourceA, Qty: 3},
|
||||
{ProductWarehouseId: 21, SourceProductWarehouseId: &sourceA, Qty: 2},
|
||||
{ProductWarehouseId: 21, SourceProductWarehouseId: &sourceB, Qty: 4},
|
||||
})
|
||||
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 depletion routes, got %d", len(got))
|
||||
}
|
||||
|
||||
routeQty := DepletionTotalsByRoute(got, func(item entity.RecordingDepletion) (uint, *uint, float64) {
|
||||
return item.ProductWarehouseId, item.SourceProductWarehouseId, item.Qty
|
||||
})
|
||||
|
||||
if routeQty[DepletionRoute{ProductWarehouseId: 21, SourceProductWarehouseId: sourceA}] != 5 {
|
||||
t.Fatalf("expected source A qty 5, got %.2f", routeQty[DepletionRoute{ProductWarehouseId: 21, SourceProductWarehouseId: sourceA}])
|
||||
}
|
||||
if routeQty[DepletionRoute{ProductWarehouseId: 21, SourceProductWarehouseId: sourceB}] != 4 {
|
||||
t.Fatalf("expected source B qty 4, got %.2f", routeQty[DepletionRoute{ProductWarehouseId: 21, SourceProductWarehouseId: sourceB}])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapEggsSetsProjectFlockKandangID(t *testing.T) {
|
||||
got := MapEggs(77, 44, 9, []validation.Egg{
|
||||
{ProductWarehouseId: 88, Qty: 10},
|
||||
})
|
||||
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected 1 egg row, got %d", len(got))
|
||||
}
|
||||
if got[0].ProjectFlockKandangId == nil || *got[0].ProjectFlockKandangId != 44 {
|
||||
t.Fatalf("expected project flock kandang id 44, got %+v", got[0].ProjectFlockKandangId)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user