Compare commits

...

4 Commits

Author SHA1 Message Date
Тимур Городецкий 5f5f0e7f81 fix(ct):пофиксил дублирование позиций в таблице а также убрал добавление поставщика безКП в таблицу 2026-09-23 16:06:26 +05:00
Тимур Городецкий 64973a2232 fix(ct): поставщики без КП с шага 2 не попадают в таблицу сравнения
Они выводились на шаге 3 колонкой с нулевыми ценами, хотя сравнивать у них
нечего. Остаются в списке шага 2 и в «Не получены КП»; ручной ввод цен
сохранён за поставщиками, добавленными на самом шаге 3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:56:14 +05:00
Тимур Городецкий e4b6582e3c feat(ct): выбор валюты у уже загруженных КП на шаге 2
После возврата с шага 3 очередь загрузки пуста, и КП, по умолчанию
сохранённое в тенге, нельзя было перевести в валюту без перезагрузки файла.
Теперь валюта меняется в «Загруженные КП»: обновляются supplier.currency
и пересчёт офферов в тенге; в режиме просмотра показывается код валюты.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:32:43 +05:00
Тимур Городецкий a45807f669 fix(ct): список загруженных КП пропадал при открытии СТ без сессии
Лукап offer в «Оценке поставщиков» смотрит на FileLeafRef библиотеки КП,
а SP не отдаёт такой лукап через REST (500), поэтому loadKpFilesForOffers
всегда возвращал пусто и «Загруженные КП» видел только автор в своей сессии.
Id файла теперь дублируется в числовое поле offerFileId и читается оттуда.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:30:36 +05:00
11 changed files with 166 additions and 52 deletions
@@ -65,7 +65,8 @@ const CtCreateForm: React.FC<ICtCreateFormProps> = ({ context, userDisplayName,
const rawOffers = await service.loadFullOffersForPositions(positionSpIds); const rawOffers = await service.loadFullOffersForPositions(positionSpIds);
// Build supplierSpId → offerId map from session uploadedKpFiles. // Build supplierSpId → offerId map from session uploadedKpFiles.
// offerId cannot be read back via SP REST (500), so we restore it from session. // Лукап offerId через REST не читается (500), поэтому сначала берём из сессии, а без сессии —
// из числового дубля offerFileId (см. CtService.loadKpFilesForOffers).
// formData here is the initial mount value (session state before this update). // formData here is the initial mount value (session state before this update).
const offerIdBySupplierSpId = new Map<number, number>(); const offerIdBySupplierSpId = new Map<number, number>();
for (const kpFile of (formData.uploadedKpFiles ?? [])) { for (const kpFile of (formData.uploadedKpFiles ?? [])) {
@@ -120,7 +121,7 @@ const CtCreateForm: React.FC<ICtCreateFormProps> = ({ context, userDisplayName,
// Build offers in domain format, restoring offerId from session // Build offers in domain format, restoring offerId from session
const offers = rawOffers.map(o => { const offers = rawOffers.map(o => {
const offerIdNum = offerIdBySupplierSpId.get(o.supplierSpId); const offerIdNum = offerIdBySupplierSpId.get(o.supplierSpId) ?? o.offerFileSpId;
const currencyCode = o.valutaSpId ? (rateById.get(o.valutaSpId)?.code ?? 'KZT') : 'KZT'; const currencyCode = o.valutaSpId ? (rateById.get(o.valutaSpId)?.code ?? 'KZT') : 'KZT';
return { return {
id: '', id: '',
@@ -109,6 +109,30 @@
} }
} }
// Размеры совпадают с селектом валюты в очереди загрузки (FileUploadArea), чтобы строки выглядели одинаково.
.kpCurrencySelect {
font-size: 11px;
padding: 2px 6px;
border: 1px solid $border;
border-radius: 3px;
background: $surface;
color: $text;
font-family: inherit;
cursor: pointer;
outline: none;
flex-shrink: 0;
height: 26px;
&:focus { border-color: $accent; }
}
.kpCurrencyCode {
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
color: $muted;
}
.noOfferBadge { .noOfferBadge {
background: $danger-bg; background: $danger-bg;
color: $danger; color: $danger;
@@ -11,6 +11,7 @@ import { useKpProcessing } from './useKpProcessing';
import { persistStep2 } from './step2Persist'; import { persistStep2 } from './step2Persist';
import { SuppliersWithoutKp } from './SuppliersWithoutKp'; import { SuppliersWithoutKp } from './SuppliersWithoutKp';
import { UploadedKpList } from './UploadedKpList'; import { UploadedKpList } from './UploadedKpList';
import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency';
import styles from './Step2CommercialOffers.module.scss'; import styles from './Step2CommercialOffers.module.scss';
interface Props { interface Props {
@@ -72,6 +73,23 @@ export const Step2CommercialOffers: React.FC<Props> = ({
onSave({ suppliers: [...withOffer, ...withoutOffer] }); onSave({ suppliers: [...withOffer, ...withoutOffer] });
}; };
// Смена валюты уже загруженного КП (список «Загруженные КП»). Если файл ещё в очереди этой сессии,
// синхронизируем и её: иначе persistStep2 пересоберёт поставщика из kpFiles со старой валютой.
const handleUploadedCurrencyChange = (fileSpId: number, currencyCode: string): void => {
const uploaded = formData.uploadedKpFiles.find(u => u.spId === fileSpId);
if (!uploaded?.supplierName) return;
onSave(applySupplierCurrency(
formData.suppliers ?? [],
formData.offers ?? [],
uploaded.supplierName,
currencyCode,
currencyRates,
));
kp.kpFiles
.filter(f => f.supplierName === uploaded.supplierName)
.forEach(f => kp.handleCurrencyChange(f.localId, currencyCode));
};
const handleSave = async (): Promise<void> => { const handleSave = async (): Promise<void> => {
setIsSaving(true); setIsSaving(true);
setSaveError(undefined); setSaveError(undefined);
@@ -133,8 +151,11 @@ export const Step2CommercialOffers: React.FC<Props> = ({
<UploadedKpList <UploadedKpList
files={formData.uploadedKpFiles} files={formData.uploadedKpFiles}
service={service} service={service}
currencyOptions={currencyOptions}
getCurrency={supplierName => getSupplierCurrency(formData.suppliers ?? [], supplierName)}
onCurrencyChange={handleUploadedCurrencyChange}
onRemove={(spId) => onSave({ uploadedKpFiles: formData.uploadedKpFiles.filter(u => u.spId !== spId) })} onRemove={(spId) => onSave({ uploadedKpFiles: formData.uploadedKpFiles.filter(u => u.spId !== spId) })}
disabled={isReadOnly} disabled={isNotificationLink || isReadOnly}
/> />
)} )}
@@ -7,12 +7,24 @@ import styles from './Step2CommercialOffers.module.scss';
interface Props { interface Props {
files: CTFormData['uploadedKpFiles']; files: CTFormData['uploadedKpFiles'];
service: CtService; service: CtService;
currencyOptions: string[];
getCurrency: (supplierName: string | undefined) => string;
onCurrencyChange: (spId: number, currencyCode: string) => void;
onRemove: (spId: number) => void; onRemove: (spId: number) => void;
disabled?: boolean; disabled?: boolean;
} }
// Секция «Загруженные КП» (постоянный список из formData): ссылка на файл + удаление из списка. // Секция «Загруженные КП» (постоянный список из formData): ссылка на файл, валюта КП и удаление.
export const UploadedKpList: React.FC<Props> = ({ files, service, onRemove, disabled }) => ( // Валюту здесь можно поправить и после возврата с Шага 3 — очередь загрузки к этому моменту уже пуста.
export const UploadedKpList: React.FC<Props> = ({
files,
service,
currencyOptions,
getCurrency,
onCurrencyChange,
onRemove,
disabled,
}) => (
<section className={styles.section}> <section className={styles.section}>
<h3 className={styles.sectionTitle}>Загруженные КП</h3> <h3 className={styles.sectionTitle}>Загруженные КП</h3>
<ul className={styles.noOfferList}> <ul className={styles.noOfferList}>
@@ -27,12 +39,27 @@ export const UploadedKpList: React.FC<Props> = ({ files, service, onRemove, disa
> >
{f.name} {f.name}
</a> </a>
{disabled ? (
<span className={styles.kpCurrencyCode}>{getCurrency(f.supplierName)}</span>
) : (
<>
<select
className={styles.kpCurrencySelect}
value={getCurrency(f.supplierName)}
onChange={e => onCurrencyChange(f.spId, e.target.value)}
// Валюта хранится на поставщике — без него менять нечего.
disabled={!f.supplierName}
aria-label={`Валюта КП ${f.name}`}
>
{currencyOptions.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<IconButton <IconButton
icon="trash" icon="trash"
title="Удалить КП из списка" title="Удалить КП из списка"
onClick={() => onRemove(f.spId)} onClick={() => onRemove(f.spId)}
disabled={disabled}
/> />
</>
)}
</li> </li>
))} ))}
</ul> </ul>
@@ -0,0 +1,38 @@
import { CurrencyRate, Supplier, SupplierOffer } from '../types/domain';
const BASE_CURRENCY = 'KZT';
// Валюта хранится на поставщике (Шаг 3 читает supplier.currency), а офферы несут цену в валюте КП
// и её пересчёт в тенге. При смене валюты уже загруженного КП обновляем и то и другое, чтобы
// таблица сравнения и сохранение офферов в SP (valutaId) видели одну и ту же валюту.
export function applySupplierCurrency(
suppliers: Supplier[],
offers: SupplierOffer[],
supplierName: string,
currencyCode: string,
currencyRates: CurrencyRate[],
): { suppliers: Supplier[]; offers: SupplierOffer[] } {
const supplierIds = new Set(
suppliers.filter(s => s.hasOffer && s.name === supplierName).map(s => s.id),
);
const rate = currencyCode === BASE_CURRENCY
? 1
: (currencyRates.find(r => r.code === currencyCode)?.rateToKzt ?? 1);
const nextSuppliers = suppliers.map(s =>
supplierIds.has(s.id) ? { ...s, currency: currencyCode } : s,
);
const nextOffers = offers.map(o => {
if (!supplierIds.has(o.supplierId)) return o;
const pricePerUnitKzt = o.pricePerUnit * rate;
const totalPriceKzt = pricePerUnitKzt * o.quantity;
return { ...o, currencyCode, pricePerUnitKzt, totalPriceKzt, totalWithExtraKzt: totalPriceKzt };
});
return { suppliers: nextSuppliers, offers: nextOffers };
}
export function getSupplierCurrency(suppliers: Supplier[], supplierName: string | undefined): string {
const supplier = suppliers.find(s => s.hasOffer && s.name === supplierName);
return supplier?.currency ?? BASE_CURRENCY;
}
@@ -11,15 +11,32 @@ export interface PersistStep2Args {
noOfferSuppliers: NoOfferEntry[]; noOfferSuppliers: NoOfferEntry[];
} }
// Несколько КП одного поставщика (повторная загрузка, тот же КП под другим именем файла) получают
// один и тот же supplierSpId — saveSupplierWithOffer находит поставщика по имени. Без отбора
// в таблице сравнения появляется дубль колонки с тем же id, а офферы обоих файлов складываются
// в одну ячейку (задвоенные строки позиций). Оставляем последний загруженный файл поставщика,
// но обработанный файл не вытесняем тем, что ещё в процессе.
function pickLatestFilePerSupplier(kpFiles: KpFile[]): KpFile[] {
const fileBySupplier = new Map<string, KpFile>();
kpFiles.forEach(f => {
if (!f.supplierName) return;
const supplierKey = f.supplierSpId !== undefined ? String(f.supplierSpId) : f.supplierName;
const existing = fileBySupplier.get(supplierKey);
const canReplace = !existing || f.status === 'done' || existing.status !== 'done';
if (canReplace) fileBySupplier.set(supplierKey, f);
});
return Array.from(fileBySupplier.values());
}
// Строит поставщиков с КП и их офферы из результатов потока (Syntex). Вынесено из persistStep2, // Строит поставщиков с КП и их офферы из результатов потока (Syntex). Вынесено из persistStep2,
// чтобы ту же логику использовал инкрементальный persist в useKpProcessing (поставщик должен // чтобы ту же логику использовал инкрементальный persist в useKpProcessing (поставщик должен
// доходить до Шага 3 даже без кнопки «Сохранить и продолжить»). // доходить до Шага 3 даже без кнопки «Сохранить и продолжить»).
export function buildKpSuppliersAndOffers( export function buildKpSuppliersAndOffers(
kpFiles: KpFile[], allKpFiles: KpFile[],
currencyRates: CurrencyRate[], currencyRates: CurrencyRate[],
): { suppliers: Supplier[]; offers: SupplierOffer[] } { ): { suppliers: Supplier[]; offers: SupplierOffer[] } {
const kpFiles = pickLatestFilePerSupplier(allKpFiles);
const suppliers: Supplier[] = kpFiles const suppliers: Supplier[] = kpFiles
.filter(f => f.supplierName)
.map(f => ({ .map(f => ({
id: f.supplierSpId ? String(f.supplierSpId) : f.localId, id: f.supplierSpId ? String(f.supplierSpId) : f.localId,
spId: f.supplierSpId, spId: f.supplierSpId,
@@ -80,7 +80,6 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat
</div> </div>
<div className={styles.supplierMetaRow}> <div className={styles.supplierMetaRow}>
<AdataIndicator supplier={s} state={adata.adataMap[s.id]} onCheck={() => void adata.checkSupplier(s.id)} /> <AdataIndicator supplier={s} state={adata.adataMap[s.id]} onCheck={() => void adata.checkSupplier(s.id)} />
{s.isNoOffer && <span className={styles.noOfferTag}>Без КП</span>}
{currency !== 'KZT' && <span className={styles.currencyTag}>{currency}</span>} {currency !== 'KZT' && <span className={styles.currencyTag}>{currency}</span>}
</div> </div>
<div className={styles.supplierControlsRow}> <div className={styles.supplierControlsRow}>
@@ -74,7 +74,7 @@ export function MatrixRowView({ pos, posIndex, matrix, currencyRates, isRestrict
} }
const rate = isForeign ? (currencyRates.find(r => r.code === currency)?.rateToKzt ?? 1) : 1; const rate = isForeign ? (currencyRates.find(r => r.code === currency)?.rateToKzt ?? 1) : 1;
const qty = item.quantity ?? ((s.isNoOffer || s.isExtraSupplier) && pos.quantity ? pos.quantity : undefined); const qty = item.quantity ?? (s.isExtraSupplier && pos.quantity ? pos.quantity : undefined);
const price = item.pricePerUnit; const price = item.pricePerUnit;
const totalInCurrency = (price ?? 0) * (qty ?? 0); const totalInCurrency = (price ?? 0) * (qty ?? 0);
const priceKzt = (price ?? 0) * rate; const priceKzt = (price ?? 0) * rate;
@@ -213,16 +213,6 @@
white-space: nowrap; white-space: nowrap;
} }
.noOfferTag {
font-size: 11px;
background: $danger-bg;
color: $danger;
padding: 1px 7px;
border-radius: 10px;
white-space: nowrap;
font-weight: 600;
}
.deleteSupplierBtn { .deleteSupplierBtn {
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -290,9 +290,13 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
}, []); }, []);
const suppliersWithOffer = useMemo(() => [ const suppliersWithOffer = useMemo(() => [
...suppliers.map(s => ({ ...s, isExtraSupplier: false as const, isNoOffer: !s.hasOffer })), // Поставщики без КП с Шага 2 (hasOffer:false) в таблицу сравнения не попадают — сравнивать
// у них нечего. Ручной ввод цен остаётся за поставщиками, добавленными на самом Шаге 3 (extras).
...suppliers
.filter(s => s.hasOffer !== false)
.map(s => ({ ...s, isExtraSupplier: false as const })),
...extraSuppliers.map(es => ({ ...extraSuppliers.map(es => ({
id: es.id, name: es.name, hasOffer: true as const, isExtraSupplier: true as const, isNoOffer: false as const, id: es.id, name: es.name, hasOffer: true as const, isExtraSupplier: true as const,
// spId/bin известны сразу для поставщиков, добавленных из «База данных поставщиков» // spId/bin известны сразу для поставщиков, добавленных из «База данных поставщиков»
// (см. handleAddSupplierFromDatabase) — без этого useAdataCheck не увидит их БИН. // (см. handleAddSupplierFromDatabase) — без этого useAdataCheck не увидит их БИН.
spId: es.spId, bin: es.bin, spId: es.spId, bin: es.bin,
@@ -343,10 +347,10 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
const rate = currency !== 'KZT' const rate = currency !== 'KZT'
? (currencyRates.find(r => r.code === currency)?.rateToKzt ?? 1) ? (currencyRates.find(r => r.code === currency)?.rateToKzt ?? 1)
: 1; : 1;
// For noOffer/extra suppliers quantity may be undefined in edits (user only entered price). // For extra suppliers quantity may be undefined in edits (user only entered price).
// Fall back to the position's own quantity so totalWithExtraKzt stays non-zero and the // Fall back to the position's own quantity so totalWithExtraKzt stays non-zero and the
// offer is included in filteredComputed → persisted in offersForSession → restored on remount. // offer is included in filteredComputed → persisted in offersForSession → restored on remount.
const posQtyFallback = (s.isNoOffer || s.isExtraSupplier) ? (pos.quantity || undefined) : undefined; const posQtyFallback = s.isExtraSupplier ? (pos.quantity || undefined) : undefined;
items.forEach(item => result.push(computeOffer( items.forEach(item => result.push(computeOffer(
{ {
...item, ...item,
@@ -694,20 +698,6 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
extraOfferIds[es.id] = offerId; extraOfferIds[es.id] = offerId;
} }
const noOfferSupSpIds: Record<string, number> = {};
const noOfferOfferIds: Record<string, number> = {};
for (const s of suppliers.filter(sup => !sup.hasOffer)) {
const hasData = computedOffers.some(o => o.supplierId === s.id && o.totalWithExtraKzt > 0);
if (!hasData) continue;
if (s.spId) {
noOfferSupSpIds[s.id] = s.spId;
continue;
}
const { spId, offerId } = await service.saveManualSupplierWithKp(s.name);
noOfferSupSpIds[s.id] = spId;
noOfferOfferIds[s.id] = offerId;
}
const extraPosSpIds: Record<string, number> = {}; const extraPosSpIds: Record<string, number> = {};
for (const ep of extraPositions) { for (const ep of extraPositions) {
if (ep.spId) { if (ep.spId) {
@@ -735,7 +725,7 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
const offersToSave = computedOffers const offersToSave = computedOffers
.filter(o => o.totalWithExtraKzt > 0) .filter(o => o.totalWithExtraKzt > 0)
.map(o => { .map(o => {
const supSpId = extraSupSpIds[o.supplierId] ?? noOfferSupSpIds[o.supplierId]; const supSpId = extraSupSpIds[o.supplierId];
const posSpId = extraPosSpIds[o.positionId]; const posSpId = extraPosSpIds[o.positionId];
const existingSup = suppliers.find(s => s.id === o.supplierId); const existingSup = suppliers.find(s => s.id === o.supplierId);
const existingPos = selectedPositions.find(p => p.id === o.positionId); const existingPos = selectedPositions.find(p => p.id === o.positionId);
@@ -761,7 +751,6 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
: (existingPos?.spId ? String(existingPos.spId) : o.positionId), : (existingPos?.spId ? String(existingPos.spId) : o.positionId),
offerId: originalOffer?.offerId offerId: originalOffer?.offerId
?? (extraOfferIds[o.supplierId] !== undefined ? String(extraOfferIds[o.supplierId]) : undefined) ?? (extraOfferIds[o.supplierId] !== undefined ? String(extraOfferIds[o.supplierId]) : undefined)
?? (noOfferOfferIds[o.supplierId] !== undefined ? String(noOfferOfferIds[o.supplierId]) : undefined)
?? (kpFileOfferId !== undefined ? String(kpFileOfferId) : undefined), ?? (kpFileOfferId !== undefined ? String(kpFileOfferId) : undefined),
...(kmVal !== undefined ? { deliveryDistanceKm: kmVal } : {}), ...(kmVal !== undefined ? { deliveryDistanceKm: kmVal } : {}),
}; };
@@ -784,7 +773,6 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
...suppliers.map(s => ({ ...suppliers.map(s => ({
...s, ...s,
status: supplierStatuses[s.id] ?? s.status, status: supplierStatuses[s.id] ?? s.status,
...(noOfferSupSpIds[s.id] !== undefined ? { spId: noOfferSupSpIds[s.id] } : {}),
})), })),
// Move extras into suppliers (with React ID kept as `id` — required so formData.offers // Move extras into suppliers (with React ID kept as `id` — required so formData.offers
// entries with React IDs still match sup.id on remount). Skip extras that are already in // entries with React IDs still match sup.id on remount). Skip extras that are already in
@@ -543,7 +543,11 @@ export class CtService {
if (!isNaN(valId)) body.valutaId = valId; if (!isNaN(valId)) body.valutaId = valId;
} }
const resolvedOfferId = o.offerId ? parseInt(o.offerId, 10) : undefined; const resolvedOfferId = o.offerId ? parseInt(o.offerId, 10) : undefined;
if (resolvedOfferId && !isNaN(resolvedOfferId)) body.offerId = resolvedOfferId; if (resolvedOfferId && !isNaN(resolvedOfferId)) {
body.offerId = resolvedOfferId;
// Дубль лукапа числом — только его можно прочитать обратно (см. loadKpFilesForOffers).
body.offerFileId = resolvedOfferId;
}
const res = await this.http.post(this._li(LISTS.OFFERS), SPHttpClient.configurations.v1, { const res = await this.http.post(this._li(LISTS.OFFERS), SPHttpClient.configurations.v1, {
headers: { "Content-Type": "application/json;odata=nometadata" }, headers: { "Content-Type": "application/json;odata=nometadata" },
@@ -777,6 +781,7 @@ export class CtService {
currencyCode: string; currencyCode: string;
valutaSpId?: number; valutaSpId?: number;
deliveryDistanceKm?: number; deliveryDistanceKm?: number;
offerFileSpId?: number;
}> }>
> { > {
if (positionSpIds.length === 0) return []; if (positionSpIds.length === 0) return [];
@@ -787,7 +792,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=positionId,supplierId,price,quantity,unit,priceTenge,summaTenge,valutaId,deliveryDistanceKm,supplier/Id,supplier/Title` + `?$select=positionId,supplierId,price,quantity,unit,priceTenge,summaTenge,valutaId,deliveryDistanceKm,offerFileId,supplier/Id,supplier/Title` +
`&$expand=supplier` + `&$expand=supplier` +
`&$filter=${filter}` + `&$filter=${filter}` +
`&$top=2000`; `&$top=2000`;
@@ -810,6 +815,7 @@ export class CtService {
currencyCode: "KZT", // enriched by caller via loadCurrencyRatesByIds currencyCode: "KZT", // enriched by caller via loadCurrencyRatesByIds
valutaSpId: valutaSpId && !isNaN(valutaSpId) ? valutaSpId : undefined, valutaSpId: valutaSpId && !isNaN(valutaSpId) ? valutaSpId : undefined,
deliveryDistanceKm: item["deliveryDistanceKm"] != null ? Number(item["deliveryDistanceKm"]) : undefined, deliveryDistanceKm: item["deliveryDistanceKm"] != null ? Number(item["deliveryDistanceKm"]) : undefined,
offerFileSpId: item["offerFileId"] ? Number(item["offerFileId"]) : undefined,
}; };
}); });
} }
@@ -826,11 +832,13 @@ export class CtService {
const filter = positionSpIds const filter = positionSpIds
.map((id) => `positionId eq ${id}`) .map((id) => `positionId eq ${id}`)
.join(" or "); .join(" or ");
// offerId — обычное числовое поле (SP-Id строки файла КП), НЕ лукап. Раньше в $select // Лукап offer (КП) смотрит на FileLeafRef библиотеки — SP не отдаёт такой лукап через REST
// ошибочно был offerIdId (raw-ID суффикс лукапа) — такого поля нет, и SP валил запрос 400. // ни $select=offerId, ни $expand (500 «типы полей установлены неправильно»), и список КП
// всегда был пустым для всех, кроме автора в его сессии. Поэтому id файла дублируется
// в числовое поле offerFileId при записи и читается отсюда.
const url = const url =
`${this._li(LISTS.OFFERS)}` + `${this._li(LISTS.OFFERS)}` +
`?$select=supplierId,offerId` + `?$select=supplierId,offerFileId` +
`&$filter=${filter}` + `&$filter=${filter}` +
`&$top=2000`; `&$top=2000`;
const res = await this.http.get(url, SPHttpClient.configurations.v1); const res = await this.http.get(url, SPHttpClient.configurations.v1);
@@ -841,7 +849,7 @@ export class CtService {
const pairs: Array<{ supplierSpId: number; offerSpId: number }> = []; const pairs: Array<{ supplierSpId: number; offerSpId: number }> = [];
for (const item of data.value ?? []) { for (const item of data.value ?? []) {
const supplierSpId = Number(item["supplierId"]); const supplierSpId = Number(item["supplierId"]);
const offerSpId = Number(item["offerId"] ?? 0); const offerSpId = Number(item["offerFileId"] ?? 0);
if (!offerSpId || !supplierSpId || seen.has(supplierSpId)) continue; if (!offerSpId || !supplierSpId || seen.has(supplierSpId)) continue;
seen.add(supplierSpId); seen.add(supplierSpId);
pairs.push({ supplierSpId, offerSpId }); pairs.push({ supplierSpId, offerSpId });
@@ -1566,6 +1574,7 @@ export class CtService {
positionId: params.positionSpId, positionId: params.positionSpId,
supplierId: params.supplierSpId, supplierId: params.supplierSpId,
offerId: params.offerSpId, offerId: params.offerSpId,
offerFileId: params.offerSpId,
supplierPosition: params.supplierPosition, supplierPosition: params.supplierPosition,
unit: params.unit, unit: params.unit,
quantity: Number(params.quantity) || 0, quantity: Number(params.quantity) || 0,