From 2bf50884c090c2e49ca9669e850909773c201286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Thu, 1 Oct 2026 10:59:11 +0500 Subject: [PATCH] =?UTF-8?q?fix(oid):=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=BD=D0=B0=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=9A=D0=9F=20=D0=BF=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D1=89=D0=B8=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8F=D0=B5=D1=82=20=D0=B5=D0=B3=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B6=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB?= =?UTF-8?q?=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F,=20=D0=B0=20=D0=BD=D0=B5?= =?UTF-8?q?=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=D1=80=D1=83=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Шаг 2 ОИД писал предложения нового КП рядом с прежними записями поставщика. Шаг 3 всегда грузит предложения из SP, поэтому позиция выглядела «комплектом» из одинаковых строк, а автосохранение Шага 3 закрепляло это в SP. Теперь по повторному КП прежние записи поставщика по тем же позициям уходят в корзину — после записи новых, чтобы при сбое не потерять данные. Дедуп при загрузке, как в СТ, в ОИД не делаем: номер строки комплекта ОИД в Title не хранит, и такой дедуп удалил бы настоящие строки комплекта. --- .../Step2CommercialOffers/useKpProcessing.ts | 5 ++- .../forms/oid/services/OidService.ts | 42 +++++++++++++++++++ .../shared/offers/offerRecords.test.ts | 25 ++++++++++- .../shared/offers/offerRecords.ts | 12 ++++++ 4 files changed, 82 insertions(+), 2 deletions(-) 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); +} + // Какие записи вытеснила только что сохранённая: прежние записи того же поставщика по той же позиции, // включая строки комплекта — новый КП поставщика заменяет его прежнее предложение целиком. // Записи новее сохранённой не трогаем: их могла создать параллельная обработка.