From 6ef39f8fc9bcbffb04dbf9b8c9068dfb8bbeabdb 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 14:28:32 +0500 Subject: [PATCH] =?UTF-8?q?fix(ct):=20=D0=BF=D0=BE=20=D1=81=D1=81=D1=8B?= =?UTF-8?q?=D0=BB=D0=BA=D0=B5=20=D0=B8=20=D0=B1=D0=B5=D0=B7=20=D1=81=D0=B5?= =?UTF-8?q?=D1=81=D1=81=D0=B8=D0=B8=20=C2=AB=D0=9D=D0=B0=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=A2=D0=9C=D0=A6?= =?UTF-8?q?=C2=BB=20=D0=B1=D0=B5=D1=80=D1=91=D1=82=D1=81=D1=8F=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=9A=D0=9F,=20=D0=B0=20=D0=BD=D0=B5=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При открытии СТ по ссылке (согласующий) или в новой вкладке предложения собирались из SP без поля supplierPosition, и шаг 3 подставлял вместо названия из КП название позиции заявки. Согласующий не видел, что поставщик предложил другую модель или аналог, а автор, сохранив шаг 3 без сессии, записывал в SP название заявки поверх названия поставщика. Теперь название читается из SP и переносится (сборка офферов вынесена в buildOffersFromSpRecords). --- .../forms/ct/components/CtCreateForm.tsx | 22 ++----- .../ct/components/utils/spOffers.test.ts | 66 +++++++++++++++++++ .../forms/ct/components/utils/spOffers.ts | 49 ++++++++++++++ .../forms/ct/services/CtService.ts | 4 +- 4 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.test.ts create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.ts diff --git a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx index 1e87c39..53c4a40 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/CtCreateForm.tsx @@ -9,6 +9,7 @@ import { Step2CommercialOffers } from './Step2CommercialOffers/Step2CommercialOf import { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTable'; import { Step4Approval } from './Step4Approval/Step4Approval'; import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns'; +import { buildOffersFromSpRecords } from './utils/spOffers'; import styles from './CtCreateForm.module.scss'; import '@tnsu/ui-kit/styles.css'; @@ -124,23 +125,10 @@ const CtCreateForm: React.FC = ({ context, userDisplayName, // Build offers in domain format. Файл КП берём из самой записи (offerFileId) — карта из сессии // знает один файл на поставщика и при нескольких КП отдала бы обеим колонкам один и тот же. - const offers = rawOffers.map(o => { - const offerIdNum = o.offerFileSpId ?? offerIdBySupplierSpId.get(o.supplierSpId); - const currencyCode = o.valutaSpId ? (rateById.get(o.valutaSpId)?.code ?? 'KZT') : 'KZT'; - return { - id: '', - supplierId: columnIdOf(o.supplierSpId, o.offerFileSpId), - positionId: String(o.positionSpId), - itemIndex: o.itemIndex, - unit: o.unit, - pricePerUnit: o.price, - quantity: o.quantity, - currencyCode, - pricePerUnitKzt: o.priceTenge || o.price, - totalPriceKzt: o.summaTenge, - totalWithExtraKzt: o.summaTenge, - offerId: offerIdNum !== undefined ? String(offerIdNum) : undefined, - }; + const offers = buildOffersFromSpRecords(rawOffers, { + columnIdOf, + currencyCodeOf, + fallbackOfferFileId: supplierSpId => offerIdBySupplierSpId.get(supplierSpId), }); // Reconstruct uploadedKpFiles for users without session (other users opening CT via link) diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.test.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.test.ts new file mode 100644 index 0000000..a489fe3 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.test.ts @@ -0,0 +1,66 @@ +import { SpOfferRecord, buildOffersFromSpRecords } from './spOffers'; + +function makeRecord(overrides: Partial): SpOfferRecord { + return { + positionSpId: 1543, + supplierSpId: 263, + supplierName: 'ТОО «Арлан Офис Снаб»', + supplierPosition: 'Доска пробковая в раме 90х120', + price: 28500, + quantity: 4, + unit: 'шт.', + priceTenge: 28500, + summaTenge: 114000, + currencyCode: 'KZT', + itemIndex: 0, + offerFileSpId: 277, + ...overrides, + }; +} + +const deps = { + columnIdOf: (supplierSpId: number, offerFileSpId?: number): string => `${supplierSpId}_${offerFileSpId}`, + currencyCodeOf: (): string => 'KZT', + fallbackOfferFileId: (): number | undefined => undefined, +}; + +describe('buildOffersFromSpRecords (открытие СТ по ссылке / без сессии)', () => { + // Раньше название из КП терялось: шаг 3 подставлял название из заявки, а «Сохранить» записывало + // его в SP поверх названия поставщика — согласующий не видел, что предложена другая модель. + it('сохраняет наименование ТМЦ из КП', () => { + const [offer] = buildOffersFromSpRecords([makeRecord({})], deps); + + expect(offer.supplierPosition).toBe('Доска пробковая в раме 90х120'); + }); + + it('пустое наименование из КП не подменяет названием из заявки', () => { + const [offer] = buildOffersFromSpRecords([makeRecord({ supplierPosition: '' })], deps); + + expect(offer.supplierPosition).toBeUndefined(); + }); + + it('переносит колонку, позицию, цены и файл КП', () => { + const [offer] = buildOffersFromSpRecords([makeRecord({})], deps); + + expect(offer).toMatchObject({ + supplierId: '263_277', + positionId: '1543', + itemIndex: 0, + unit: 'шт.', + pricePerUnit: 28500, + quantity: 4, + pricePerUnitKzt: 28500, + totalPriceKzt: 114000, + offerId: '277', + }); + }); + + it('без файла в записи берёт файл КП из сессии', () => { + const [offer] = buildOffersFromSpRecords( + [makeRecord({ offerFileSpId: undefined })], + { ...deps, fallbackOfferFileId: () => 300 }, + ); + + expect(offer.offerId).toBe('300'); + }); +}); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.ts b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.ts new file mode 100644 index 0000000..af87ce8 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/utils/spOffers.ts @@ -0,0 +1,49 @@ +import { SupplierOffer } from '../types/domain'; + +// Запись «Оценка поставщиков», прочитанная из SP (CtService.loadFullOffersForPositions). +export interface SpOfferRecord { + positionSpId: number; + supplierSpId: number; + supplierName: string; + supplierPosition: string; // наименование ТМЦ в КП поставщика + price: number; + quantity: number; + unit: string; + priceTenge: number; + summaTenge: number; + currencyCode: string; + itemIndex: number; + valutaSpId?: number; + offerFileSpId?: number; +} + +export interface BuildOffersDeps { + columnIdOf: (supplierSpId: number, offerFileSpId?: number) => string; + currencyCodeOf: (valutaSpId?: number) => string; + // Файл КП из сессии — для старых записей без offerFileId. + fallbackOfferFileId: (supplierSpId: number) => number | undefined; +} + +// Офферы формы из записей SP (открытие СТ по ссылке / без сессии). Наименование ТМЦ из КП переносим +// обязательно: без него шаг 3 подставляет название из заявки — согласующий не видит, что поставщик +// предложил другую модель, а «Сохранить» записывает название заявки в SP поверх названия поставщика. +export function buildOffersFromSpRecords(records: SpOfferRecord[], deps: BuildOffersDeps): SupplierOffer[] { + return records.map(r => { + const offerFileId = r.offerFileSpId ?? deps.fallbackOfferFileId(r.supplierSpId); + return { + id: '', + supplierId: deps.columnIdOf(r.supplierSpId, r.offerFileSpId), + positionId: String(r.positionSpId), + itemIndex: r.itemIndex, + supplierPosition: r.supplierPosition || undefined, + unit: r.unit, + pricePerUnit: r.price, + quantity: r.quantity, + currencyCode: deps.currencyCodeOf(r.valutaSpId), + pricePerUnitKzt: r.priceTenge || r.price, + totalPriceKzt: r.summaTenge, + totalWithExtraKzt: r.summaTenge, + offerId: offerFileId !== undefined ? String(offerFileId) : undefined, + }; + }); +} diff --git a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts index 52e8059..74d8a69 100644 --- a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts +++ b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts @@ -779,6 +779,7 @@ export class CtService { positionSpId: number; supplierSpId: number; supplierName: string; + supplierPosition: string; price: number; quantity: number; unit: string; @@ -799,7 +800,7 @@ export class CtService { // valutaId is selected directly; caller enriches currencyCode via loadCurrencyRatesByIds. const url = `${this._li(LISTS.OFFERS)}` + - `?$select=Id,Title,positionId,supplierId,price,quantity,unit,priceTenge,summaTenge,valutaId,deliveryDistanceKm,offerFileId,supplier/Id,supplier/Title` + + `?$select=Id,Title,positionId,supplierId,supplierPosition,price,quantity,unit,priceTenge,summaTenge,valutaId,deliveryDistanceKm,offerFileId,supplier/Id,supplier/Title` + `&$expand=supplier` + `&$filter=${filter}` + `&$top=2000`; @@ -816,6 +817,7 @@ export class CtService { positionSpId: Number(item["positionId"]), supplierSpId: Number(item["supplierId"]), supplierName: String(sup?.["Title"] ?? ""), + supplierPosition: String(item["supplierPosition"] ?? ""), price: Number(item["price"] ?? 0), quantity: Number(item["quantity"] ?? 0), unit: String(item["unit"] ?? ""),