fix report doc initial count logic
This commit is contained in:
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_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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<number, ManualInput>();
|
||||
for (const row of input.manuals) {
|
||||
manualByDay.set(cycleDayForDate(cycle.start_date, row.date), row);
|
||||
}
|
||||
const manualWeightByDay = new Map<number, number>();
|
||||
const manualFeedInByDay = 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);
|
||||
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),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user