fix report doc initial count logic

This commit is contained in:
Alberto-Audrix committed 2026-09-02 14:59:48 +07:00
1 parent 53316cfd49
commit c45b6656ae
3 files changed
+107 -17

No files matched your search

@@ -321,7 +321,10 @@ class Command(BaseCommand):
harvest_day_accum += harvest_today * age harvest_day_accum += harvest_today * age
harvest_wt_accum += harvest_today * harvest_wt_today 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 # Running averages: flock weight before the first harvest, then the
# harvest-weighted running mean carried forward on idle days. # harvest-weighted running mean carried forward on idle days.
if harvest_today: if harvest_today:
@@ -334,11 +337,9 @@ class Command(BaseCommand):
avg_hday = harvest_day_accum / harvest_total avg_hday = harvest_day_accum / harvest_total
avg_hwt = harvest_wt_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 lot_weight_g = life * weight if weight else 0.0
denom = harvest_weight_total + lot_weight_g denom = harvest_weight_total + lot_weight_g
fcr = (feed_total * 50_000) / denom if denom > 0 else 0.0 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: if avg_hday > 0 and fcr > 0:
eef = life_pct * (avg_hwt / 1000.0) / (fcr * avg_hday) * 100.0 eef = life_pct * (avg_hwt / 1000.0) / (fcr * avg_hday) * 100.0
else: else:
@@ -349,8 +350,8 @@ class Command(BaseCommand):
date=day, date=day,
defaults={ defaults={
"age_manual": age, "age_manual": age,
"mortality_manual": MORTALITY[i], "mortality_manual": mort_today,
"mortality_manual_total": mort_total, "mortality_manual_total": mort_total_after,
"feed_in_manual": in_today, "feed_in_manual": in_today,
"feed_in_manual_total": feed_in_total, "feed_in_manual_total": feed_in_total,
"feed_use_manual": feed_today, "feed_use_manual": feed_today,
@@ -376,8 +377,8 @@ class Command(BaseCommand):
date=day, date=day,
defaults={ defaults={
"age": age, "age": age,
"mortality": MORTALITY[i], "mortality": mort_today,
"mortality_total": mort_total, "mortality_total": mort_total_after,
"feed": feed_today, "feed": feed_today,
"feed_total": feed_total, "feed_total": feed_total,
"harvest": harvest_today, "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 ------------- # --- IoT panel: Kandang 2 sensors with per-house jitter -------------
panel_count = 0 panel_count = 0
@@ -98,11 +98,82 @@ describe('buildInMemoryReport', () => {
expect(payload.upToDay).toBe(1); expect(payload.upToDay).toBe(1);
expect(payload.counting).toHaveLength(2); 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); const day1 = payload.counting.find((row) => row.day === 1);
expect(day1?.countBefore).toBe(20000);
expect(day1?.iotCount).toBe(19800); expect(day1?.iotCount).toBe(19800);
expect(day1?.mortality).toBe(12); expect(day1?.mortality).toBe(12);
const feed1 = payload.feedUse.find((row) => row.day === 1); const feed1 = payload.feedUse.find((row) => row.day === 1);
expect(feed1?.iotUse).toBe(8); expect(feed1?.iotUse).toBe(8);
expect(payload.fcr.find((row) => row.day === 1)?.iotFcr).toBe(1.2); 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);
});
}); });
+27 -9
View File
@@ -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; 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%); /** Feed accuracy: both manual and IoT reading zero means a perfect match (100%);
* if only one side has a reading, the match is 0%. */ * if only one side has a reading, the match is 0%. */
function feedAccuracy(iot: number | null, manual: number | null): number | null { 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) { for (const row of input.kpis) {
kpiByAge.set(row.age, row); kpiByAge.set(row.age, row);
} }
const manualByDay = new Map<number, ManualInput>();
for (const row of input.manuals) {
manualByDay.set(cycleDayForDate(cycle.start_date, row.date), row);
}
const manualWeightByDay = new Map<number, number>(); const manualWeightByDay = new Map<number, number>();
const manualFeedInByDay = new Map<number, number>(); const manualFeedInByDay = new Map<number, number>();
const manualFeedUseByDay = new Map<number, number>(); const manualFeedUseByDay = new Map<number, number>();
@@ -84,23 +93,32 @@ export function buildInMemoryReport(input: BuildInMemoryReportInput): DashboardR
manualFeedUseByDay.set(day, row.feed_use_manual); manualFeedUseByDay.set(day, row.feed_use_manual);
manualFeedOutByDay.set(day, row.feed_out_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 counting = days.map((day) => {
const iot = countingByDay.get(day) ?? null; const iot = countingByDay.get(day) ?? null;
const kpi = kpiByAge.get(day) ?? null; const kpi = kpiByAge.get(day) ?? null;
const manual = manualByDay.get(day) ?? null;
const iotCount = iot?.total_count ?? 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 { return {
date: addDate(cycle.start_date, day), date: addDate(cycle.start_date, day),
day, day,
countBefore: after == null ? null : after + (kpi?.mortality ?? iot?.mortality_count ?? 0), countBefore,
mortality: kpi?.mortality ?? iot?.mortality_count ?? latestManual?.mortality_manual ?? null, mortality,
harvest: kpi?.harvest ?? null, harvest,
harvestWeight: kpi?.harvest_weight ?? null, harvestWeight,
countAfter: after, countAfter,
iotCount, iotCount,
accuracy: accuracy(iotCount, after), accuracy: countingAccuracy(iotCount, countAfter),
}; };
}); });