fix(oid): повторная обработка КП поставщика заменяет его прежние предложения, а не дублирует
Шаг 2 ОИД писал предложения нового КП рядом с прежними записями поставщика. Шаг 3 всегда грузит предложения из SP, поэтому позиция выглядела «комплектом» из одинаковых строк, а автосохранение Шага 3 закрепляло это в SP. Теперь по повторному КП прежние записи поставщика по тем же позициям уходят в корзину — после записи новых, чтобы при сбое не потерять данные. Дедуп при загрузке, как в СТ, в ОИД не делаем: номер строки комплекта ОИД в Title не хранит, и такой дедуп удалил бы настоящие строки комплекта.
This commit is contained in:
+4
-1
@@ -146,7 +146,10 @@ export function useKpProcessing(service: OidService, formData: OIDFormData) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (offers.length > 0) {
|
if (offers.length > 0) {
|
||||||
service.saveOffers(offers).catch(() => undefined);
|
service.replaceSupplierOffers(supplierSpId, offers).catch((err) => {
|
||||||
|
// Не критично: Шаг 3 при сохранении перезаписывает предложения позиций целиком.
|
||||||
|
console.warn("[КП ОИД] не удалось записать предложения:", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
getRequestTypeIdByPageUrl,
|
getRequestTypeIdByPageUrl,
|
||||||
writeRoute,
|
writeRoute,
|
||||||
} from "../../../shared/approval";
|
} from "../../../shared/approval";
|
||||||
|
import { findSupplierOfferIds } from "../../../shared/offers/offerRecords";
|
||||||
|
|
||||||
const SITE_URL = "https://tnsukz.sharepoint.com/sites/sed";
|
const SITE_URL = "https://tnsukz.sharepoint.com/sites/sed";
|
||||||
|
|
||||||
@@ -786,6 +787,47 @@ export class OidService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Предложения нового КП поставщика (Шаг 2) заменяют его прежние по этим позициям. Раньше повторная
|
||||||
|
// обработка КП добавляла записи рядом с прежними, а Шаг 3 грузит всё из SP — позиция показывалась
|
||||||
|
// «комплектом» из одинаковых строк, и автосохранение Шага 3 закрепляло его в SP.
|
||||||
|
// Порядок «снимок → запись → удаление снимка»: упади запись — прежнее предложение останется.
|
||||||
|
async replaceSupplierOffers(supplierSpId: number, offers: SupplierOffer[]): Promise<void> {
|
||||||
|
if (offers.length === 0) return;
|
||||||
|
const positionSpIds = offers.map((o) => Number(o.positionId));
|
||||||
|
const filter =
|
||||||
|
`supplierId eq ${supplierSpId} and (` +
|
||||||
|
positionSpIds.map((id) => `positionId eq ${id}`).join(" or ") +
|
||||||
|
")";
|
||||||
|
const res = await this.sp.get(
|
||||||
|
`${listUrl(LISTS.OFFERS)}/items?$filter=${encodeURIComponent(filter)}&$select=Id,positionId,supplierId&$top=1000`,
|
||||||
|
SPHttpClient.configurations.v1,
|
||||||
|
);
|
||||||
|
const previous = res.ok
|
||||||
|
? ((await res.json()).value as Array<Record<string, unknown>>).map((item) => ({
|
||||||
|
id: Number(item.Id),
|
||||||
|
supplierSpId: Number(item.supplierId),
|
||||||
|
positionSpId: Number(item.positionId),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
await this.saveOffers(offers);
|
||||||
|
|
||||||
|
const staleIds = findSupplierOfferIds(previous, supplierSpId, positionSpIds);
|
||||||
|
if (staleIds.length === 0) return;
|
||||||
|
const results = await this.executeBatch(
|
||||||
|
staleIds.map((id) => ({
|
||||||
|
method: "POST" as const,
|
||||||
|
url: `${listUrl(LISTS.OFFERS)}/items(${id})/recycle()`,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.ok || r.status === 404) continue;
|
||||||
|
// SP webhook bug: item IS recycled but event receiver throws FormatException
|
||||||
|
if (r.status === 500 && r.body.includes("FormatException")) continue;
|
||||||
|
throw new Error(`Ошибка удаления прежнего предложения: ${r.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadOffersForPositions(
|
async loadOffersForPositions(
|
||||||
positionSpIds: number[],
|
positionSpIds: number[],
|
||||||
): Promise<SupplierOffer[]> {
|
): Promise<SupplierOffer[]> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IOfferRecordKey, findSupersededOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
import { IOfferRecordKey, findSupersededOfferIds, findSupplierOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
||||||
|
|
||||||
interface ITestRecord extends IOfferRecordKey {
|
interface ITestRecord extends IOfferRecordKey {
|
||||||
price: number;
|
price: number;
|
||||||
@@ -94,3 +94,26 @@ describe('findSupersededOfferIds', () => {
|
|||||||
expect(findSupersededOfferIds(existing, saved)).toEqual([]);
|
expect(findSupersededOfferIds(existing, saved)).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findSupplierOfferIds', () => {
|
||||||
|
// ОИД: перед записью нового КП поставщика запоминаем его прежние записи по этим позициям,
|
||||||
|
// чтобы после записи убрать их. Номера строк комплекта ОИД в Title не хранит — берём все.
|
||||||
|
it('возвращает все записи поставщика по переданным позициям, включая строки комплекта', () => {
|
||||||
|
const records = [
|
||||||
|
makeRecord({ id: 314, supplierSpId: 18, positionSpId: 121 }),
|
||||||
|
makeRecord({ id: 315, supplierSpId: 18, positionSpId: 121 }),
|
||||||
|
makeRecord({ id: 316, supplierSpId: 18, positionSpId: 122 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(findSupplierOfferIds(records, 18, [121, 122])).toEqual([314, 315, 316]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('не трогает другие позиции и других поставщиков', () => {
|
||||||
|
const records = [
|
||||||
|
makeRecord({ id: 314, supplierSpId: 18, positionSpId: 999 }),
|
||||||
|
makeRecord({ id: 315, supplierSpId: 19, positionSpId: 121 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(findSupplierOfferIds(records, 18, [121])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -31,6 +31,18 @@ export function keepLatestOfferRecords<T extends IOfferRecordKey>(records: T[]):
|
|||||||
return Array.from(latestByKey.values());
|
return Array.from(latestByKey.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Все записи поставщика по указанным позициям — снимок «до» записи нового КП, чтобы после записи
|
||||||
|
// убрать прежние. Для форм, где SP Id новой записи заранее не известен (ОИД пишет пакетом).
|
||||||
|
export function findSupplierOfferIds(
|
||||||
|
records: Array<Pick<IOfferRecordKey, 'id' | 'supplierSpId' | 'positionSpId'>>,
|
||||||
|
supplierSpId: number,
|
||||||
|
positionSpIds: number[],
|
||||||
|
): number[] {
|
||||||
|
return records
|
||||||
|
.filter(record => record.supplierSpId === supplierSpId && positionSpIds.includes(record.positionSpId))
|
||||||
|
.map(record => record.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Какие записи вытеснила только что сохранённая: прежние записи того же поставщика по той же позиции,
|
// Какие записи вытеснила только что сохранённая: прежние записи того же поставщика по той же позиции,
|
||||||
// включая строки комплекта — новый КП поставщика заменяет его прежнее предложение целиком.
|
// включая строки комплекта — новый КП поставщика заменяет его прежнее предложение целиком.
|
||||||
// Записи новее сохранённой не трогаем: их могла создать параллельная обработка.
|
// Записи новее сохранённой не трогаем: их могла создать параллельная обработка.
|
||||||
|
|||||||
Reference in New Issue
Block a user