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_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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user