From c45b6656ae15f24d77810892c1d5fcacddc6e712 Mon Sep 17 00:00:00 2001 From: Alberto-Audrix Date: Wed, 2 Sep 2026 14:59:48 +0700 Subject: [PATCH] fix report doc initial count logic --- .../commands/seed_sukawarna_kandang4.py | 17 ++--- utils/__tests__/buildInMemoryReport.test.ts | 71 +++++++++++++++++++ utils/buildInMemoryReport.ts | 36 +++++++--- 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py b/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py index 061d662..09b687d 100644 --- a/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py +++ b/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py @@ -321,7 +321,10 @@ class Command(BaseCommand): harvest_day_accum += harvest_today * age harvest_wt_accum += harvest_today * harvest_wt_today - # --- manual input ---------------------------------------------- + mort_today = MORTALITY[i] + mort_total_after = mort_total + mort_today + life = DOC_IN_COUNT - mort_total_after - harvest_total + life_pct = life / DOC_IN_COUNT * 100.0 if DOC_IN_COUNT else 0.0 # Running averages: flock weight before the first harvest, then the # harvest-weighted running mean carried forward on idle days. if harvest_today: @@ -334,11 +337,9 @@ class Command(BaseCommand): avg_hday = harvest_day_accum / harvest_total avg_hwt = harvest_wt_accum / harvest_total - life = DOC_IN_COUNT - mort_total - harvest_total lot_weight_g = life * weight if weight else 0.0 denom = harvest_weight_total + lot_weight_g fcr = (feed_total * 50_000) / denom if denom > 0 else 0.0 - life_pct = (DOC_IN_COUNT - mort_total) / DOC_IN_COUNT * 100.0 if avg_hday > 0 and fcr > 0: eef = life_pct * (avg_hwt / 1000.0) / (fcr * avg_hday) * 100.0 else: @@ -349,8 +350,8 @@ class Command(BaseCommand): date=day, defaults={ "age_manual": age, - "mortality_manual": MORTALITY[i], - "mortality_manual_total": mort_total, + "mortality_manual": mort_today, + "mortality_manual_total": mort_total_after, "feed_in_manual": in_today, "feed_in_manual_total": feed_in_total, "feed_use_manual": feed_today, @@ -376,8 +377,8 @@ class Command(BaseCommand): date=day, defaults={ "age": age, - "mortality": MORTALITY[i], - "mortality_total": mort_total, + "mortality": mort_today, + "mortality_total": mort_total_after, "feed": feed_today, "feed_total": feed_total, "harvest": harvest_today, @@ -398,7 +399,7 @@ class Command(BaseCommand): }, ) - mort_total += MORTALITY[i] + mort_total = mort_total_after # --- IoT panel: Kandang 2 sensors with per-house jitter ------------- panel_count = 0 diff --git a/utils/__tests__/buildInMemoryReport.test.ts b/utils/__tests__/buildInMemoryReport.test.ts index 6f34189..18b6da5 100644 --- a/utils/__tests__/buildInMemoryReport.test.ts +++ b/utils/__tests__/buildInMemoryReport.test.ts @@ -98,11 +98,82 @@ describe('buildInMemoryReport', () => { expect(payload.upToDay).toBe(1); expect(payload.counting).toHaveLength(2); + const day0 = payload.counting.find((row) => row.day === 0); + expect(day0?.countBefore).toBe(20000); + expect(day0?.countAfter).toBe(20000); const day1 = payload.counting.find((row) => row.day === 1); + expect(day1?.countBefore).toBe(20000); expect(day1?.iotCount).toBe(19800); expect(day1?.mortality).toBe(12); const feed1 = payload.feedUse.find((row) => row.day === 1); expect(feed1?.iotUse).toBe(8); expect(payload.fcr.find((row) => row.day === 1)?.iotFcr).toBe(1.2); }); + + it('uses doc_in_count for day-0 stok awal instead of reverse-calculating from stok akhir', () => { + const countings: ChickenCounting[] = [ + { + id: 1, + cycle: 1, + kandang: 9, + flock_id: 1, + date: '2026-05-01', + total_count: 36900, + mortality_count: 42, + created_at: '', + updated_at: '', + }, + ]; + const kpis: KPI[] = [ + { + id: 1, + cycle: 1, + kandang: 9, + flock_id: 1, + date: '2026-05-01', + age: 0, + mortality: 42, + mortality_total: 42, + feed: 0, + feed_total: 0, + harvest: 0, + harvest_total: 0, + harvest_weight: 0, + harvest_weight_total: 0, + chicken_life: 45000, + chicken_life_percentage: 100, + iot_weight: 0, + average_harvest_day: 0, + average_harvest_weight: 0, + fcr: 0, + eef: 0, + cc: 1, + cw: 1, + fs: 1, + manual: 1, + created_at: '', + updated_at: '', + }, + ]; + + const payload = buildInMemoryReport({ + cycle: { ...cycle, doc_in_count: 45000, current_day: 0, start_date: '2026-05-01' }, + kandangId: 9, + kandangName: 'Kandang 1', + area: 'Sukawarna', + reportType: 'daily', + countings, + manuals: [], + weights: [], + karungs: [], + kpis, + }); + + const day0 = payload.counting.find((row) => row.day === 0); + expect(day0?.countBefore).toBe(45000); + expect(day0?.mortality).toBe(42); + expect(day0?.countAfter).toBe(45000); + expect(day0?.iotCount).toBe(36900); + expect(day0?.accuracy).toBe(82); + }); }); diff --git a/utils/buildInMemoryReport.ts b/utils/buildInMemoryReport.ts index 6a9ce76..973b890 100644 --- a/utils/buildInMemoryReport.ts +++ b/utils/buildInMemoryReport.ts @@ -30,6 +30,11 @@ function accuracy(iot: number | null, manual: number | null): number | null { return Math.round((1 - Math.abs(iot - manual) / Math.abs(manual)) * 1000) / 10; } +function countingAccuracy(iot: number | null, stockAkhir: number | null): number | null { + if (iot == null || stockAkhir == null || stockAkhir <= 0) return null; + return Math.round((Math.min(iot, stockAkhir) / Math.max(iot, stockAkhir)) * 1000) / 10; +} + /** Feed accuracy: both manual and IoT reading zero means a perfect match (100%); * if only one side has a reading, the match is 0%. */ function feedAccuracy(iot: number | null, manual: number | null): number | null { @@ -73,6 +78,10 @@ export function buildInMemoryReport(input: BuildInMemoryReportInput): DashboardR for (const row of input.kpis) { kpiByAge.set(row.age, row); } + const manualByDay = new Map(); + for (const row of input.manuals) { + manualByDay.set(cycleDayForDate(cycle.start_date, row.date), row); + } const manualWeightByDay = new Map(); const manualFeedInByDay = new Map(); const manualFeedUseByDay = new Map(); @@ -84,23 +93,32 @@ export function buildInMemoryReport(input: BuildInMemoryReportInput): DashboardR manualFeedUseByDay.set(day, row.feed_use_manual); manualFeedOutByDay.set(day, row.feed_out_manual); } - const latestManual = input.manuals[0] ?? null; - + let previousStockAkhir: number | null = null; const counting = days.map((day) => { const iot = countingByDay.get(day) ?? null; const kpi = kpiByAge.get(day) ?? null; + const manual = manualByDay.get(day) ?? null; const iotCount = iot?.total_count ?? null; - const after = kpi?.chicken_life ?? iotCount; + const mortality = + manual?.mortality_manual ?? kpi?.mortality ?? iot?.mortality_count ?? null; + const harvest = manual?.harvest_manual ?? kpi?.harvest ?? null; + const harvestWeight = manual?.harvest_weight_manual ?? kpi?.harvest_weight ?? null; + const mati = mortality ?? 0; + const panen = harvest ?? 0; + const countBefore = day === 0 ? cycle.doc_in_count || 0 : (previousStockAkhir ?? 0); + const countAfter = kpi?.chicken_life ?? Math.max(0, countBefore - mati - panen); + previousStockAkhir = countAfter; + return { date: addDate(cycle.start_date, day), day, - countBefore: after == null ? null : after + (kpi?.mortality ?? iot?.mortality_count ?? 0), - mortality: kpi?.mortality ?? iot?.mortality_count ?? latestManual?.mortality_manual ?? null, - harvest: kpi?.harvest ?? null, - harvestWeight: kpi?.harvest_weight ?? null, - countAfter: after, + countBefore, + mortality, + harvest, + harvestWeight, + countAfter, iotCount, - accuracy: accuracy(iotCount, after), + accuracy: countingAccuracy(iotCount, countAfter), }; });