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 { Step3ComparisonTable } from './Step3ComparisonTable/Step3ComparisonTable';
|
||||||
import { Step4Approval } from './Step4Approval/Step4Approval';
|
import { Step4Approval } from './Step4Approval/Step4Approval';
|
||||||
import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns';
|
import { buildColumnsFromSpOffers, buildSupplierColumnId, findColumnForRecord } from './utils/supplierColumns';
|
||||||
|
import { buildOffersFromSpRecords } from './utils/spOffers';
|
||||||
import styles from './CtCreateForm.module.scss';
|
import styles from './CtCreateForm.module.scss';
|
||||||
import '@tnsu/ui-kit/styles.css';
|
import '@tnsu/ui-kit/styles.css';
|
||||||
|
|
||||||
@@ -124,23 +125,10 @@ const CtCreateForm: React.FC<ICtCreateFormProps> = ({ context, userDisplayName,
|
|||||||
|
|
||||||
// Build offers in domain format. Файл КП берём из самой записи (offerFileId) — карта из сессии
|
// Build offers in domain format. Файл КП берём из самой записи (offerFileId) — карта из сессии
|
||||||
// знает один файл на поставщика и при нескольких КП отдала бы обеим колонкам один и тот же.
|
// знает один файл на поставщика и при нескольких КП отдала бы обеим колонкам один и тот же.
|
||||||
const offers = rawOffers.map(o => {
|
const offers = buildOffersFromSpRecords(rawOffers, {
|
||||||
const offerIdNum = o.offerFileSpId ?? offerIdBySupplierSpId.get(o.supplierSpId);
|
columnIdOf,
|
||||||
const currencyCode = o.valutaSpId ? (rateById.get(o.valutaSpId)?.code ?? 'KZT') : 'KZT';
|
currencyCodeOf,
|
||||||
return {
|
fallbackOfferFileId: supplierSpId => offerIdBySupplierSpId.get(supplierSpId),
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reconstruct uploadedKpFiles for users without session (other users opening CT via link)
|
// 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;
|
positionSpId: number;
|
||||||
supplierSpId: number;
|
supplierSpId: number;
|
||||||
supplierName: string;
|
supplierName: string;
|
||||||
|
supplierPosition: string;
|
||||||
price: number;
|
price: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
@@ -799,7 +800,7 @@ export class CtService {
|
|||||||
// valutaId is selected directly; caller enriches currencyCode via loadCurrencyRatesByIds.
|
// valutaId is selected directly; caller enriches currencyCode via loadCurrencyRatesByIds.
|
||||||
const url =
|
const url =
|
||||||
`${this._li(LISTS.OFFERS)}` +
|
`${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` +
|
`&$expand=supplier` +
|
||||||
`&$filter=${filter}` +
|
`&$filter=${filter}` +
|
||||||
`&$top=2000`;
|
`&$top=2000`;
|
||||||
@@ -816,6 +817,7 @@ export class CtService {
|
|||||||
positionSpId: Number(item["positionId"]),
|
positionSpId: Number(item["positionId"]),
|
||||||
supplierSpId: Number(item["supplierId"]),
|
supplierSpId: Number(item["supplierId"]),
|
||||||
supplierName: String(sup?.["Title"] ?? ""),
|
supplierName: String(sup?.["Title"] ?? ""),
|
||||||
|
supplierPosition: String(item["supplierPosition"] ?? ""),
|
||||||
price: Number(item["price"] ?? 0),
|
price: Number(item["price"] ?? 0),
|
||||||
quantity: Number(item["quantity"] ?? 0),
|
quantity: Number(item["quantity"] ?? 0),
|
||||||
unit: String(item["unit"] ?? ""),
|
unit: String(item["unit"] ?? ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user