diff --git a/src/webparts/ctFormsPortal/forms/oid/components/Step2CommercialOffers/useKpProcessing.ts b/src/webparts/ctFormsPortal/forms/oid/components/Step2CommercialOffers/useKpProcessing.ts index e1f92f1..733d975 100644 --- a/src/webparts/ctFormsPortal/forms/oid/components/Step2CommercialOffers/useKpProcessing.ts +++ b/src/webparts/ctFormsPortal/forms/oid/components/Step2CommercialOffers/useKpProcessing.ts @@ -146,7 +146,10 @@ export function useKpProcessing(service: OidService, formData: OIDFormData) { }; }); if (offers.length > 0) { - service.saveOffers(offers).catch(() => undefined); + service.replaceSupplierOffers(supplierSpId, offers).catch((err) => { + // Не критично: Шаг 3 при сохранении перезаписывает предложения позиций целиком. + console.warn("[КП ОИД] не удалось записать предложения:", err); + }); } } diff --git a/src/webparts/ctFormsPortal/forms/oid/services/OidService.ts b/src/webparts/ctFormsPortal/forms/oid/services/OidService.ts index dab6b41..576940c 100644 --- a/src/webparts/ctFormsPortal/forms/oid/services/OidService.ts +++ b/src/webparts/ctFormsPortal/forms/oid/services/OidService.ts @@ -25,6 +25,7 @@ import { getRequestTypeIdByPageUrl, writeRoute, } from "../../../shared/approval"; +import { findSupplierOfferIds } from "../../../shared/offers/offerRecords"; 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 { + 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>).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( positionSpIds: number[], ): Promise { diff --git a/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts b/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts index bf09e2a..60d71a2 100644 --- a/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts +++ b/src/webparts/ctFormsPortal/shared/offers/offerRecords.test.ts @@ -1,4 +1,4 @@ -import { IOfferRecordKey, findSupersededOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords'; +import { IOfferRecordKey, findSupersededOfferIds, findSupplierOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords'; interface ITestRecord extends IOfferRecordKey { price: number; @@ -94,3 +94,26 @@ describe('findSupersededOfferIds', () => { 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([]); + }); +}); diff --git a/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts b/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts index 700d2dc..4e26977 100644 --- a/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts +++ b/src/webparts/ctFormsPortal/shared/offers/offerRecords.ts @@ -31,6 +31,18 @@ export function keepLatestOfferRecords(records: T[]): return Array.from(latestByKey.values()); } +// Все записи поставщика по указанным позициям — снимок «до» записи нового КП, чтобы после записи +// убрать прежние. Для форм, где SP Id новой записи заранее не известен (ОИД пишет пакетом). +export function findSupplierOfferIds( + records: Array>, + supplierSpId: number, + positionSpIds: number[], +): number[] { + return records + .filter(record => record.supplierSpId === supplierSpId && positionSpIds.includes(record.positionSpId)) + .map(record => record.id); +} + // Какие записи вытеснила только что сохранённая: прежние записи того же поставщика по той же позиции, // включая строки комплекта — новый КП поставщика заменяет его прежнее предложение целиком. // Записи новее сохранённой не трогаем: их могла создать параллельная обработка.