refactor(planning): use S53A Option B link (PR→POLine→LotLine→Lot) instead of PR→Lot
All 3 planning query methods (getOverview, getProcurementTimeline, getPositionsTimelineByCategory) now traverse via poLines relation instead of the unreliable pr.lots path (which misses lots with requirementId=NULL). Removes the orphan "PO-XXX — поставки" lane and the 2nd-pass PO→lots workaround. Lot events are deduplicated globally per category to prevent N× multiplication across PRs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4ad965bb03
commit
69aeab3626
@ -110,11 +110,10 @@ export class PlanningOverviewService {
|
||||
where: { orderId: { in: orderIds }, archived: false, status: { not: 'CANCELLED' } },
|
||||
select: {
|
||||
id: true, orderId: true, materialCategory: true, quantity: true, originalQuantity: true,
|
||||
lots: {
|
||||
where: { status: { notIn: ['CANCELLED'] } },
|
||||
poLines: {
|
||||
select: {
|
||||
id: true, lotNumber: true, quantity: true,
|
||||
plannedDate: true, deliveryDate: true, status: true,
|
||||
id: true,
|
||||
purchaseOrderId: true,
|
||||
purchaseOrder: {
|
||||
select: {
|
||||
id: true, orderCode: true, status: true,
|
||||
@ -122,7 +121,15 @@ export class PlanningOverviewService {
|
||||
},
|
||||
},
|
||||
lotLines: {
|
||||
select: { poLineId: true, quantity: true },
|
||||
select: {
|
||||
quantity: true,
|
||||
lot: {
|
||||
select: {
|
||||
id: true, lotNumber: true, quantity: true,
|
||||
plannedDate: true, deliveryDate: true, status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -253,20 +260,16 @@ export class PlanningOverviewService {
|
||||
|
||||
const deliveredLotsByMaterial = new Map<string, number>();
|
||||
for (const pr of prs) {
|
||||
for (const lot of pr.lots) {
|
||||
if (lot.status !== 'DELIVERED') continue;
|
||||
let lotHasRecs = false;
|
||||
for (const ll of lot.lotLines ?? []) {
|
||||
if (recs.some(r => r.poLine.id === ll.poLineId)) {
|
||||
lotHasRecs = true;
|
||||
break;
|
||||
for (const polLine of pr.poLines ?? []) {
|
||||
const hasRecs = recs.some(r => r.poLine.id === polLine.id);
|
||||
if (hasRecs) continue;
|
||||
for (const ll of polLine.lotLines ?? []) {
|
||||
if (ll.lot.status === 'DELIVERED') {
|
||||
const mat = pr.materialCategory as string;
|
||||
const prev = deliveredLotsByMaterial.get(mat) ?? 0;
|
||||
deliveredLotsByMaterial.set(mat, prev + Number(ll.quantity));
|
||||
}
|
||||
}
|
||||
if (!lotHasRecs) {
|
||||
const mat = pr.materialCategory as string;
|
||||
const prev = deliveredLotsByMaterial.get(mat) ?? 0;
|
||||
deliveredLotsByMaterial.set(mat, prev + Number(lot.quantity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,10 +290,10 @@ export class PlanningOverviewService {
|
||||
const poIds = new Set<number>();
|
||||
let defaultSupplier: string | null = null;
|
||||
for (const pr of catPrs) {
|
||||
for (const lot of pr.lots) {
|
||||
if (lot.purchaseOrder) {
|
||||
poIds.add(lot.purchaseOrder.id);
|
||||
if (!defaultSupplier) defaultSupplier = lot.purchaseOrder.supplier?.companyName ?? null;
|
||||
for (const polLine of pr.poLines ?? []) {
|
||||
if (polLine.purchaseOrder) {
|
||||
poIds.add(polLine.purchaseOrder.id);
|
||||
if (!defaultSupplier) defaultSupplier = polLine.purchaseOrder.supplier?.companyName ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -361,31 +364,38 @@ export class PlanningOverviewService {
|
||||
label: 'Создан',
|
||||
});
|
||||
|
||||
const seenLotIdsTimeline = new Set<number>();
|
||||
for (const pr of prs) {
|
||||
for (const lot of pr.lots) {
|
||||
if (lot.plannedDate) {
|
||||
events.push({
|
||||
type: 'lot_planned',
|
||||
date: new Date(lot.plannedDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber}`,
|
||||
category: pr.materialCategory,
|
||||
});
|
||||
}
|
||||
if (lot.status === 'DELIVERED' && lot.deliveryDate) {
|
||||
events.push({
|
||||
type: 'reception_complete',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Получено Лот ${lot.lotNumber} (${Number(lot.quantity)})`,
|
||||
color: '#389e0d',
|
||||
category: pr.materialCategory,
|
||||
});
|
||||
} else if (lot.deliveryDate) {
|
||||
events.push({
|
||||
type: 'lot_delivered',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber} доставка`,
|
||||
category: pr.materialCategory,
|
||||
});
|
||||
for (const polLine of pr.poLines ?? []) {
|
||||
for (const ll of polLine.lotLines ?? []) {
|
||||
const lot = ll.lot;
|
||||
if (seenLotIdsTimeline.has(lot.id)) continue;
|
||||
seenLotIdsTimeline.add(lot.id);
|
||||
|
||||
if (lot.plannedDate) {
|
||||
events.push({
|
||||
type: 'lot_planned',
|
||||
date: new Date(lot.plannedDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber}`,
|
||||
category: pr.materialCategory,
|
||||
});
|
||||
}
|
||||
if (lot.status === 'DELIVERED' && lot.deliveryDate) {
|
||||
events.push({
|
||||
type: 'reception_complete',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Получено Лот ${lot.lotNumber} (${Number(lot.quantity)})`,
|
||||
color: '#389e0d',
|
||||
category: pr.materialCategory,
|
||||
});
|
||||
} else if (lot.deliveryDate) {
|
||||
events.push({
|
||||
type: 'lot_delivered',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber} доставка`,
|
||||
category: pr.materialCategory,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -444,32 +454,33 @@ export class PlanningOverviewService {
|
||||
where: { orderId, archived: false, status: { not: 'CANCELLED' } },
|
||||
select: {
|
||||
id: true, materialCategory: true, createdAt: true, status: true, quantity: true, originalQuantity: true,
|
||||
lots: {
|
||||
where: { status: { notIn: ['CANCELLED'] } },
|
||||
poLines: {
|
||||
select: {
|
||||
id: true, lotNumber: true, quantity: true,
|
||||
plannedDate: true, deliveryDate: true, status: true,
|
||||
id: true,
|
||||
purchaseOrderId: true,
|
||||
purchaseOrder: { select: { orderCode: true, id: true } },
|
||||
purchaseOrder: { select: { id: true, orderCode: true, status: true } },
|
||||
lotLines: {
|
||||
select: {
|
||||
poLineId: true,
|
||||
poLine: {
|
||||
quantity: true,
|
||||
lot: {
|
||||
select: {
|
||||
receptions: {
|
||||
where: { status: { not: 'CANCELLED' } },
|
||||
select: { id: true, receivedQty: true, receivedAt: true },
|
||||
},
|
||||
id: true, lotNumber: true, quantity: true,
|
||||
plannedDate: true, deliveryDate: true, status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
receptions: {
|
||||
where: { status: { not: 'CANCELLED' as any } },
|
||||
select: { id: true, receivedQty: true, receivedAt: true, lotId: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const catMap = new Map<string, Array<any>>();
|
||||
const globalSeenLotIds = new Set<number>();
|
||||
for (const pr of prs) {
|
||||
const cat = pr.materialCategory;
|
||||
const events: any[] = [];
|
||||
@ -482,66 +493,58 @@ export class PlanningOverviewService {
|
||||
color: '#1890ff',
|
||||
});
|
||||
|
||||
for (const lot of pr.lots) {
|
||||
if (lot.plannedDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-planned`,
|
||||
type: 'lot_planned',
|
||||
date: new Date(lot.plannedDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber}`,
|
||||
color: '#faad14',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
}
|
||||
for (const polLine of pr.poLines) {
|
||||
const poCode = polLine.purchaseOrder?.orderCode;
|
||||
const poId = polLine.purchaseOrder?.id;
|
||||
|
||||
if (lot.purchaseOrderId) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-ordered`,
|
||||
type: 'lot_ordered',
|
||||
date: new Date(lot.plannedDate ?? pr.createdAt).toISOString(),
|
||||
label: `${lot.purchaseOrder?.orderCode ?? 'PO'} Лот ${lot.lotNumber}`,
|
||||
color: '#722ed1',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
});
|
||||
}
|
||||
for (const ll of polLine.lotLines) {
|
||||
const lot = ll.lot;
|
||||
if (globalSeenLotIds.has(lot.id)) continue;
|
||||
globalSeenLotIds.add(lot.id);
|
||||
|
||||
if (lot.status === 'DELIVERED' && lot.deliveryDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'reception_complete',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Получено Лот ${lot.lotNumber} (${Number(lot.quantity)})`,
|
||||
color: '#389e0d',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
} else if (lot.deliveryDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'lot_delivered',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Доставка Лот ${lot.lotNumber}`,
|
||||
color: '#52c41a',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
});
|
||||
}
|
||||
|
||||
for (const ll of lot.lotLines) {
|
||||
for (const rec of ll.poLine.receptions) {
|
||||
if (lot.plannedDate && lot.status !== 'DELIVERED' && lot.status !== 'CANCELLED') {
|
||||
events.push({
|
||||
id: `rec-${rec.id}`,
|
||||
type: 'reception',
|
||||
date: new Date(rec.receivedAt).toISOString(),
|
||||
label: `Приёмка ${Number(rec.receivedQty)}`,
|
||||
color: '#52c41a',
|
||||
quantity: Number(rec.receivedQty),
|
||||
id: `lot-${lot.id}-planned`,
|
||||
type: 'lot_planned',
|
||||
date: new Date(lot.plannedDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber}`,
|
||||
color: '#faad14',
|
||||
poNumber: poCode, poId,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
}
|
||||
|
||||
if (lot.status === 'DELIVERED' && lot.deliveryDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'reception_complete',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Получено Лот ${lot.lotNumber} (${Number(lot.quantity)})`,
|
||||
color: '#389e0d',
|
||||
poNumber: poCode, poId,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
} else if (lot.deliveryDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'lot_delivered',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Доставка Лот ${lot.lotNumber}`,
|
||||
color: '#52c41a',
|
||||
poNumber: poCode, poId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const rec of polLine.receptions) {
|
||||
events.push({
|
||||
id: `rec-${rec.id}`,
|
||||
type: 'reception',
|
||||
date: new Date(rec.receivedAt).toISOString(),
|
||||
label: `Приёмка ${Number(rec.receivedQty)}`,
|
||||
color: '#52c41a',
|
||||
quantity: Number(rec.receivedQty),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -550,33 +553,18 @@ export class PlanningOverviewService {
|
||||
catMap.set(cat, [...existing, ...events]);
|
||||
}
|
||||
|
||||
// PO-level events (independent of lots) + PO lots without PR link
|
||||
// PO-level events (creation + expected delivery) — no more lot duplication pass
|
||||
const pos = await this.prisma.purchaseOrder.findMany({
|
||||
where: { clientOrderId: orderId, status: { notIn: ['CANCELLED', 'DRAFT'] } },
|
||||
select: {
|
||||
id: true, orderCode: true, status: true, createdAt: true,
|
||||
expectedDeliveryDate: true, actualDeliveryDate: true,
|
||||
expectedDeliveryDate: true,
|
||||
lines: { select: { material: true } },
|
||||
lots: {
|
||||
where: { status: { notIn: ['CANCELLED'] } },
|
||||
select: {
|
||||
id: true, lotNumber: true, quantity: true,
|
||||
plannedDate: true, deliveryDate: true, status: true,
|
||||
requirementId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const prLinkedLotIds = new Set<number>();
|
||||
for (const pr of prs) {
|
||||
for (const lot of pr.lots) {
|
||||
prLinkedLotIds.add(lot.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const po of pos) {
|
||||
const categories = [...new Set(po.lines.map(l => l.material))];
|
||||
const categories = Array.from(new Set(po.lines.map(l => l.material)));
|
||||
for (const cat of categories) {
|
||||
const poEvents: any[] = [];
|
||||
poEvents.push({
|
||||
@ -600,45 +588,6 @@ export class PlanningOverviewService {
|
||||
});
|
||||
}
|
||||
|
||||
for (const lot of po.lots) {
|
||||
if (prLinkedLotIds.has(lot.id)) continue;
|
||||
|
||||
if (lot.plannedDate) {
|
||||
poEvents.push({
|
||||
id: `lot-${lot.id}-planned`,
|
||||
type: 'lot_planned',
|
||||
date: new Date(lot.plannedDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber} — ${po.orderCode}`,
|
||||
color: '#faad14',
|
||||
poNumber: po.orderCode,
|
||||
poId: po.id,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
}
|
||||
if (lot.status === 'DELIVERED' && lot.deliveryDate) {
|
||||
poEvents.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'reception_complete',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Получено Лот ${lot.lotNumber} (${Number(lot.quantity)}) — ${po.orderCode}`,
|
||||
color: '#389e0d',
|
||||
poNumber: po.orderCode,
|
||||
poId: po.id,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
} else if (lot.deliveryDate) {
|
||||
poEvents.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'lot_delivered',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Доставка Лот ${lot.lotNumber} — ${po.orderCode}`,
|
||||
color: '#52c41a',
|
||||
poNumber: po.orderCode,
|
||||
poId: po.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const existing = catMap.get(cat) ?? [];
|
||||
catMap.set(cat, [...existing, ...poEvents]);
|
||||
}
|
||||
@ -680,29 +629,34 @@ export class PlanningOverviewService {
|
||||
let lastReceptionDate: string | null = null;
|
||||
let lastPlannedOrDelivery: string | null = null;
|
||||
|
||||
for (const lot of pr.lots) {
|
||||
if (lot.plannedDate) physicalDates.push(new Date(lot.plannedDate).toISOString());
|
||||
if (lot.deliveryDate) physicalDates.push(new Date(lot.deliveryDate).toISOString());
|
||||
const critDate = lot.deliveryDate ?? lot.plannedDate;
|
||||
if (critDate) {
|
||||
const iso = new Date(critDate).toISOString();
|
||||
if (!lastPlannedOrDelivery || iso > lastPlannedOrDelivery) lastPlannedOrDelivery = iso;
|
||||
}
|
||||
let lotHasReceptions = false;
|
||||
for (const ll of lot.lotLines) {
|
||||
for (const rec of ll.poLine.receptions) {
|
||||
qtyReceived += Number(rec.receivedQty);
|
||||
physicalDates.push(new Date(rec.receivedAt).toISOString());
|
||||
const recIso = new Date(rec.receivedAt).toISOString();
|
||||
if (!lastReceptionDate || recIso > lastReceptionDate) lastReceptionDate = recIso;
|
||||
lotHasReceptions = true;
|
||||
for (const polLine of pr.poLines) {
|
||||
for (const ll of polLine.lotLines) {
|
||||
const lot = ll.lot;
|
||||
if (lot.plannedDate) physicalDates.push(new Date(lot.plannedDate).toISOString());
|
||||
if (lot.deliveryDate) physicalDates.push(new Date(lot.deliveryDate).toISOString());
|
||||
const critDate = lot.deliveryDate ?? lot.plannedDate;
|
||||
if (critDate) {
|
||||
const iso = new Date(critDate).toISOString();
|
||||
if (!lastPlannedOrDelivery || iso > lastPlannedOrDelivery) lastPlannedOrDelivery = iso;
|
||||
}
|
||||
}
|
||||
if (!lotHasReceptions && lot.status === 'DELIVERED') {
|
||||
qtyReceived += Number(lot.quantity);
|
||||
if (lot.deliveryDate) {
|
||||
const delIso = new Date(lot.deliveryDate).toISOString();
|
||||
if (!lastReceptionDate || delIso > lastReceptionDate) lastReceptionDate = delIso;
|
||||
|
||||
for (const rec of polLine.receptions) {
|
||||
qtyReceived += Number(rec.receivedQty);
|
||||
physicalDates.push(new Date(rec.receivedAt).toISOString());
|
||||
const recIso = new Date(rec.receivedAt).toISOString();
|
||||
if (!lastReceptionDate || recIso > lastReceptionDate) lastReceptionDate = recIso;
|
||||
}
|
||||
|
||||
if (polLine.receptions.length === 0) {
|
||||
for (const ll of polLine.lotLines) {
|
||||
if (ll.lot.status === 'DELIVERED') {
|
||||
qtyReceived += Number(ll.quantity);
|
||||
if (ll.lot.deliveryDate) {
|
||||
const delIso = new Date(ll.lot.deliveryDate).toISOString();
|
||||
if (!lastReceptionDate || delIso > lastReceptionDate) lastReceptionDate = delIso;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -790,25 +744,26 @@ export class PlanningOverviewService {
|
||||
where: { orderId, materialCategory: category, archived: false, status: { not: 'CANCELLED' } },
|
||||
select: {
|
||||
id: true, description: true, quantity: true, originalQuantity: true, unit: true, status: true, createdAt: true,
|
||||
lots: {
|
||||
where: { status: { notIn: ['CANCELLED'] } },
|
||||
poLines: {
|
||||
select: {
|
||||
id: true, lotNumber: true, quantity: true,
|
||||
plannedDate: true, deliveryDate: true, status: true,
|
||||
id: true,
|
||||
purchaseOrderId: true,
|
||||
purchaseOrder: { select: { orderCode: true, id: true } },
|
||||
purchaseOrder: { select: { id: true, orderCode: true, status: true } },
|
||||
lotLines: {
|
||||
select: {
|
||||
poLine: {
|
||||
quantity: true,
|
||||
lot: {
|
||||
select: {
|
||||
receptions: {
|
||||
where: { status: { not: 'CANCELLED' as any } },
|
||||
select: { id: true, receivedQty: true, receivedAt: true },
|
||||
},
|
||||
id: true, lotNumber: true, quantity: true,
|
||||
plannedDate: true, deliveryDate: true, status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
receptions: {
|
||||
where: { status: { not: 'CANCELLED' as any } },
|
||||
select: { id: true, receivedQty: true, receivedAt: true, lotId: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -819,7 +774,7 @@ export class PlanningOverviewService {
|
||||
orderId,
|
||||
category,
|
||||
positions: prs.map(pr => {
|
||||
const qtyReceived = this.computeReceivedQty(pr);
|
||||
const qtyReceived = this.computeReceivedQtyV2(pr);
|
||||
return {
|
||||
id: pr.id,
|
||||
description: pr.description,
|
||||
@ -827,30 +782,30 @@ export class PlanningOverviewService {
|
||||
unit: pr.unit,
|
||||
status: pr.status,
|
||||
qtyReceived,
|
||||
events: this.buildPositionEvents(pr, qtyReceived),
|
||||
events: this.buildPositionEventsV2(pr, qtyReceived),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private computeReceivedQty(pr: any): number {
|
||||
private computeReceivedQtyV2(pr: any): number {
|
||||
let total = 0;
|
||||
for (const lot of pr.lots) {
|
||||
let lotHasReceptions = false;
|
||||
for (const ll of lot.lotLines) {
|
||||
for (const rec of ll.poLine.receptions) {
|
||||
total += Number(rec.receivedQty);
|
||||
lotHasReceptions = true;
|
||||
}
|
||||
for (const polLine of pr.poLines) {
|
||||
for (const rec of polLine.receptions) {
|
||||
total += Number(rec.receivedQty);
|
||||
}
|
||||
if (!lotHasReceptions && lot.status === 'DELIVERED') {
|
||||
total += Number(lot.quantity);
|
||||
if (polLine.receptions.length === 0) {
|
||||
for (const ll of polLine.lotLines) {
|
||||
if (ll.lot.status === 'DELIVERED') {
|
||||
total += Number(ll.quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private buildPositionEvents(pr: any, qtyReceived: number): Array<any> {
|
||||
private buildPositionEventsV2(pr: any, qtyReceived: number): Array<any> {
|
||||
const events: any[] = [];
|
||||
const qtyOrdered = Number(pr.originalQuantity ?? pr.quantity);
|
||||
|
||||
@ -862,85 +817,65 @@ export class PlanningOverviewService {
|
||||
color: '#1890ff',
|
||||
});
|
||||
|
||||
for (const lot of pr.lots) {
|
||||
if (lot.plannedDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-planned`,
|
||||
type: 'lot_planned',
|
||||
date: new Date(lot.plannedDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber}`,
|
||||
color: '#faad14',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
}
|
||||
if (lot.purchaseOrderId) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-ordered`,
|
||||
type: 'lot_ordered',
|
||||
date: new Date(lot.plannedDate ?? pr.createdAt).toISOString(),
|
||||
label: `${lot.purchaseOrder?.orderCode ?? 'PO'} Лот ${lot.lotNumber}`,
|
||||
color: '#722ed1',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
});
|
||||
}
|
||||
let cumulativeReceived = 0;
|
||||
const allReceptions: Array<{ id: number; receivedQty: any; receivedAt: Date }> = [];
|
||||
for (const ll of lot.lotLines) {
|
||||
allReceptions.push(...ll.poLine.receptions);
|
||||
const seenLotIds = new Set<number>();
|
||||
|
||||
for (const polLine of pr.poLines) {
|
||||
const poCode = polLine.purchaseOrder?.orderCode;
|
||||
const poId = polLine.purchaseOrder?.id;
|
||||
|
||||
for (const ll of polLine.lotLines) {
|
||||
const lot = ll.lot;
|
||||
if (seenLotIds.has(lot.id)) continue;
|
||||
seenLotIds.add(lot.id);
|
||||
|
||||
if (lot.plannedDate && lot.status !== 'DELIVERED' && lot.status !== 'CANCELLED') {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-planned-${polLine.id}`,
|
||||
type: 'forecast_delivery',
|
||||
date: new Date(lot.plannedDate).toISOString(),
|
||||
label: `Лот ${lot.lotNumber} (${Number(ll.quantity)} ${pr.unit})`,
|
||||
color: '#fa8c16',
|
||||
poNumber: poCode, poId,
|
||||
quantity: Number(ll.quantity),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (allReceptions.length > 0) {
|
||||
if (lot.deliveryDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'lot_delivered',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Доставка Лот ${lot.lotNumber}`,
|
||||
color: '#52c41a',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
});
|
||||
}
|
||||
allReceptions.sort((a, b) => new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime());
|
||||
for (const rec of allReceptions) {
|
||||
cumulativeReceived += Number(rec.receivedQty);
|
||||
const isComplete = cumulativeReceived >= qtyOrdered && qtyOrdered > 0;
|
||||
events.push({
|
||||
id: `rec-${rec.id}`,
|
||||
type: isComplete ? 'reception_complete' : 'reception_partial',
|
||||
date: new Date(rec.receivedAt).toISOString(),
|
||||
label: isComplete
|
||||
? `Получено полностью (${Number(rec.receivedQty)})`
|
||||
: `Приёмка ${Number(rec.receivedQty)} (${Math.round(cumulativeReceived / qtyOrdered * 100)}%)`,
|
||||
color: isComplete ? '#52c41a' : '#52c41a',
|
||||
quantity: Number(rec.receivedQty),
|
||||
});
|
||||
}
|
||||
} else if (lot.status === 'DELIVERED' && lot.deliveryDate) {
|
||||
const sortedReceptions = [...polLine.receptions].sort(
|
||||
(a: any, b: any) => new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime()
|
||||
);
|
||||
let cumulative = 0;
|
||||
for (const rec of sortedReceptions) {
|
||||
cumulative += Number(rec.receivedQty);
|
||||
const isComplete = cumulative >= qtyOrdered && qtyOrdered > 0;
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'reception_complete',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Получено Лот ${lot.lotNumber} (${Number(lot.quantity)})`,
|
||||
color: '#389e0d',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
quantity: Number(lot.quantity),
|
||||
});
|
||||
} else if (lot.deliveryDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered`,
|
||||
type: 'lot_delivered',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Доставка Лот ${lot.lotNumber}`,
|
||||
id: `rec-${rec.id}`,
|
||||
type: isComplete ? 'reception_complete' : 'reception_partial',
|
||||
date: new Date(rec.receivedAt).toISOString(),
|
||||
label: isComplete
|
||||
? `Получено полностью (${Number(rec.receivedQty)} ${pr.unit})`
|
||||
: `Приёмка ${Number(rec.receivedQty)} ${pr.unit} (${Math.round((cumulative / qtyOrdered) * 100)}%)`,
|
||||
color: '#52c41a',
|
||||
poNumber: lot.purchaseOrder?.orderCode,
|
||||
poId: lot.purchaseOrder?.id,
|
||||
quantity: Number(rec.receivedQty),
|
||||
poNumber: poCode, poId,
|
||||
});
|
||||
}
|
||||
|
||||
for (const ll of polLine.lotLines) {
|
||||
const lot = ll.lot;
|
||||
const hasRecForThisLot = polLine.receptions.some((r: any) => r.lotId === lot.id);
|
||||
if (lot.status === 'DELIVERED' && !hasRecForThisLot && lot.deliveryDate) {
|
||||
events.push({
|
||||
id: `lot-${lot.id}-delivered-${polLine.id}`,
|
||||
type: 'reception_complete',
|
||||
date: new Date(lot.deliveryDate).toISOString(),
|
||||
label: `Получено Лот ${lot.lotNumber} (${Number(ll.quantity)} ${pr.unit})`,
|
||||
color: '#389e0d',
|
||||
poNumber: poCode, poId,
|
||||
quantity: Number(ll.quantity),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
events.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user