fix(ct): по ссылке и без сессии «Наименование ТМЦ» берётся из КП, а не из заявки
При открытии СТ по ссылке (согласующий) или в новой вкладке предложения собирались из SP без поля supplierPosition, и шаг 3 подставлял вместо названия из КП название позиции заявки. Согласующий не видел, что поставщик предложил другую модель или аналог, а автор, сохранив шаг 3 без сессии, записывал в SP название заявки поверх названия поставщика. Теперь название читается из SP и переносится (сборка офферов вынесена в buildOffersFromSpRecords).
This commit is contained in:
@@ -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<ICtCreateFormProps> = ({ 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)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { SpOfferRecord, buildOffersFromSpRecords } from './spOffers';
|
||||
|
||||
function makeRecord(overrides: Partial<SpOfferRecord>): 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');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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"] ?? ""),
|
||||
|
||||
Reference in New Issue
Block a user