From d6d92ece3f1e1e59d2db0edcbdabb6ea6a793bc0 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:37:54 +0500 Subject: [PATCH] =?UTF-8?q?fix(ct):=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=B7=20=D0=9A=D0=9F=20?= =?UTF-8?q?=D0=BF=D0=B8=D1=88=D1=83=D1=82=D1=81=D1=8F=20=D0=B2=20SP=20?= =?UTF-8?q?=D1=81=D1=80=D0=B0=D0=B7=D1=83=20=D0=BF=D1=80=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B5,=20=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=20=C2=AB=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Предложения писались внутри цикла по позициям и только если поставщика уже определил Syntex. Syntex отключён, поставщика теперь называет поток — узнаём его после разбора всех позиций, поэтому при обработке КП в «Оценка поставщиков» не попадало ничего: закрыл вкладку без «Сохранить» — и разобранные цены пропали. Теперь записи строятся после определения поставщика (buildKpOffersToSave), а сбои потока и записи идут в консоль вместо пустого catch. --- .../kpOfferRecords.test.ts | 74 +++++++++++++++++++ .../Step2CommercialOffers/kpOfferRecords.ts | 45 +++++++++++ .../Step2CommercialOffers/useKpProcessing.ts | 28 +++---- 3 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.test.ts create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.ts diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.test.ts new file mode 100644 index 0000000..6de1a69 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.test.ts @@ -0,0 +1,74 @@ +import { buildKpOffersToSave } from './kpOfferRecords'; +import { FlowPositionResult } from './kpTypes'; +import { Position } from '../types/domain'; + +function makePosition(overrides: Partial): Position { + return { + id: 'pos_1', + spId: 1543, + requestId: 'req_1', + requestNumber: '051/26-08/0015', + materialName: 'Доска пробковая', + disciplineCode: '', + smrType: '', + unit: 'шт', + agskCode: '', + uniqueCode: '', + quantity: 4, + positionType: 'ТМЦ', + deadline: '', + ...overrides, + }; +} + +const POSITIONS = [ + makePosition({}), + makePosition({ id: 'pos_2', spId: 1544, materialName: 'Флипчарт', quantity: 3 }), +]; + +const RESULTS: FlowPositionResult[] = [ + { positionId: 'pos_1', positionSpId: 1543, flowResult: { position: 'Доска в раме', unit: 'шт.', quantity: 4, price: 28500, cost: 114000 } }, + { positionId: 'pos_2', positionSpId: 1544, flowResult: { price: 98000, cost: 294000 } }, +]; + +describe('buildKpOffersToSave', () => { + // Syntex отключён: поставщика называет поток, и узнаём его только после разбора всех позиций. + // Раньше записи строились внутри цикла и без поставщика от Syntex не строились вовсе. + it('строит записи по поставщику, определённому после разбора', () => { + const offers = buildKpOffersToSave(RESULTS, POSITIONS, 264, 283); + + expect(offers).toHaveLength(2); + expect(offers[0]).toEqual({ + positionSpId: 1543, + supplierSpId: 264, + offerSpId: 283, + supplierPosition: 'Доска в раме', + unit: 'шт.', + quantity: 4, + price: 28500, + summa: 114000, + }); + }); + + it('пустые поля потока берёт из позиции СТ', () => { + const offers = buildKpOffersToSave(RESULTS, POSITIONS, 264, 283); + + expect(offers[1]).toMatchObject({ supplierPosition: 'Флипчарт', unit: 'шт', quantity: 3, price: 98000 }); + }); + + it('без поставщика записей нет', () => { + expect(buildKpOffersToSave(RESULTS, POSITIONS, undefined, 283)).toEqual([]); + }); + + it('пропускает позицию без SP Id', () => { + const results: FlowPositionResult[] = [{ positionId: 'pos_x', flowResult: { price: 1 } }]; + + expect(buildKpOffersToSave(results, POSITIONS, 264, 283)).toEqual([]); + }); + + it('берёт SP Id из позиции, если в результате его нет', () => { + const results: FlowPositionResult[] = [{ positionId: 'pos_2', flowResult: { price: 5 } }]; + + expect(buildKpOffersToSave(results, POSITIONS, 264, 283)[0].positionSpId).toBe(1544); + }); +}); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.ts new file mode 100644 index 0000000..1243f43 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/kpOfferRecords.ts @@ -0,0 +1,45 @@ +import { FlowPositionResult } from './kpTypes'; +import { Position } from '../types/domain'; + +// Запись «Оценка поставщиков» по одной позиции из разобранного КП (вход CtService.saveKpOffer). +export interface KpOfferToSave { + positionSpId: number; + supplierSpId: number; + offerSpId: number; + supplierPosition: string; + unit: string; + quantity: number; + price: number; + summa: number; +} + +// Строит записи после разбора всего КП. Поставщик может прийти из ответа потока (Syntex отключён), +// а его узнаём только когда разобраны все позиции — поэтому записи не строим внутри цикла по позициям. +// Пустые поля потока добираем из позиции СТ. +export function buildKpOffersToSave( + results: FlowPositionResult[], + positions: Position[], + supplierSpId: number | undefined, + fileSpId: number, +): KpOfferToSave[] { + if (!supplierSpId) return []; + + const offers: KpOfferToSave[] = []; + for (const { positionId, positionSpId, flowResult } of results) { + const position = positions.find(p => p.id === positionId); + const spId = positionSpId ?? position?.spId; + if (!spId) continue; + + offers.push({ + positionSpId: spId, + supplierSpId, + offerSpId: fileSpId, + supplierPosition: flowResult.position ?? position?.materialName ?? '', + unit: flowResult.unit ?? position?.unit ?? '', + quantity: flowResult.quantity ?? position?.quantity ?? 0, + price: flowResult.price ?? 0, + summa: flowResult.cost ?? 0, + }); + } + return offers; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts index 9caf562..77aa295 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/useKpProcessing.ts @@ -3,6 +3,7 @@ import { CtService } from '../../services/CtService'; import { CTFormData } from '../hooks/useCtForm'; import { KpFile, FlowPositionResult, POLL_INTERVAL_MS, MAX_POLL_RETRIES } from './kpTypes'; import { buildKpSuppliersAndOffers } from './step2Persist'; +import { buildKpOffersToSave } from './kpOfferRecords'; let localIdCounter = 0; @@ -91,20 +92,9 @@ export function useKpProcessing( try { const result = await service.triggerFlow(fileName, fileSpId, pos.materialName); collectedResults.push({ positionId: pos.id, positionSpId: pos.spId, flowResult: result }); - if (pos.spId && supplierSpId && result) { - await service.saveKpOffer({ - positionSpId: pos.spId, - supplierSpId, - offerSpId: fileSpId, - supplierPosition: result.position ?? pos.materialName, - unit: result.unit ?? pos.unit, - quantity: result.quantity ?? pos.quantity, - price: result.price ?? 0, - summa: result.cost ?? 0, - }); - } - } catch { - // individual position failure doesn't stop others + } catch (err) { + // Сбой одной позиции не останавливает остальные, но и не теряется молча. + console.warn(`[КП] поток не разобрал позицию «${pos.materialName}»:`, err); } if (mountedRef.current) { setKpFiles(prev => prev.map(f => @@ -128,6 +118,16 @@ export function useKpProcessing( } } + // Предложения пишем после того, как поставщик определён: Syntex отключён, и поставщика чаще + // называет сам поток — внутри цикла по позициям его ещё нет, и записи раньше не создавались вовсе. + const offersToSave = buildKpOffersToSave(collectedResults, positions, resolvedSupplierSpId, fileSpId); + await Promise.all(offersToSave.map(offer => + service.saveKpOffer(offer).catch(err => { + // Не критично: «Сохранить» на Шаге 2/3 перезапишет предложения позиции целиком. + console.warn(`[КП] не удалось записать предложение по позиции ${offer.positionSpId}:`, err); + }) + )); + // БИН поставщика (для Adata) — берём первый результат из позиций, где он вообще пришёл const supplierBIN = collectedResults.find(r => r.flowResult.supplierBIN)?.flowResult.supplierBIN; if (supplierBIN && resolvedSupplierSpId) {