AURORA-1899: возврат тестирования — разрядность не трогает неденежные и текстовые колонки #308

Merged
aep merged 1 commits from feat/285-aurora-1899-non-money-fix into test 2026-11-11 15:15:47 +03:00
3 changed files with 170 additions and 10 deletions

View File

@ -3,7 +3,7 @@ import React, { useMemo, useCallback, useState } from 'react';
import { useCellLock } from '../../contexts/RealtimeContext'; import { useCellLock } from '../../contexts/RealtimeContext';
import { resolveCellTextColor } from '../../utils/cellTextColor'; import { resolveCellTextColor } from '../../utils/cellTextColor';
import { formatNumber } from '../../utils/numberFormat'; import { formatNumber } from '../../utils/numberFormat';
import { TEXT_COLUMN_IDS } from '../../utils/moneyUtils'; import { isTextColumn } from '../../utils/moneyUtils';
const BASE_CELL_STYLES = { const BASE_CELL_STYLES = {
width: '100%', width: '100%',
@ -164,7 +164,7 @@ const CellComponent = ({
const { rawValue, displayValue, isNumeric, isNegative } = useMemo(() => { const { rawValue, displayValue, isNumeric, isNegative } = useMemo(() => {
const value = cell.getValue(); const value = cell.getValue();
const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value)) && !TEXT_COLUMN_IDS.has(column.id); const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value)) && !isTextColumn(column);
let display = String(value || ''); let display = String(value || '');
let negative = false; let negative = false;
if (isNum) { if (isNum) {

View File

@ -41,9 +41,9 @@ export const TEXT_COLUMN_IDS = new Set([
'data.expense_item_code', 'data.expense_item_code',
]); ]);
// Последний сегмент accessorKey (после последней точки), который никогда не денежный: // Последний сегмент accessorKey (после последней точки) числовой, но не денежной колонки:
// количества, ставки, курсы, коды, идентификаторы и т.п. // количества, площади, ставки, курсы, суммы в валюте. Форматируются как числа, не масштабируются.
const NON_MONEY_LAST_SEGMENTS = new Set([ const NUMERIC_NON_MONEY_LAST_SEGMENTS = new Set([
'qty_q1', 'qty_q1',
'qty_q2', 'qty_q2',
'qty_q3', 'qty_q3',
@ -52,6 +52,13 @@ const NON_MONEY_LAST_SEGMENTS = new Set([
'vat_rate', 'vat_rate',
'exchange_rate', 'exchange_rate',
'amount_foreign', 'amount_foreign',
]);
const NUMERIC_NON_MONEY_SUFFIXES = ['_qty'];
// Последний сегмент accessorKey текстовой колонки: коды, идентификаторы и свободный текст.
// Не масштабируются и не форматируются как числа, даже если значение похоже на число
// (внутренний заказ 10123, комментарий «123»).
const TEXT_LAST_SEGMENTS = new Set([
'section', 'section',
'section_code', 'section_code',
'section_no', 'section_no',
@ -65,8 +72,70 @@ const NON_MONEY_LAST_SEGMENTS = new Set([
'pay_year', 'pay_year',
'order', 'order',
'expense_item_code', 'expense_item_code',
'id_stati',
// Свободный текст (комментарии, обоснования, реквизиты договоров, даты, флаги).
// Без них значение «123» в комментарии делилось на разрядность, а при разрядности
// не «тыс.» текстовый редактор закрывался как денежный (возврат AURORA-1899, 11.11).
// Комментарии с суффиксом *_comment ловит TEXT_SUFFIXES.
'comment',
'justification',
'note',
'name',
'counterparty',
'subject',
'reference',
'contract_ref',
'deadline',
'delivery_deadline',
'date',
'pay_date',
'contract_end_date',
'currency',
'act',
'pay_act',
'scheme',
'payment_scheme',
'rf_schedule',
'addenda',
'proc_plan',
'proc_method',
'procurement_plan',
'procurement_method',
'property',
'allocation_purpose',
'protocol',
'protocol_reference',
'approved',
'centralized_flag',
'object_type',
'unit',
'address',
'equipment_name',
]); ]);
const TEXT_SUFFIXES = ['_comment'];
const hasSuffix = (segment, suffixes) => suffixes.some((suffix) => segment.endsWith(suffix));
// И конфиг колонки, и MRT column: у последнего accessorKey и moneyUnit лежат в column.columnDef
// (возврат AURORA-1899, 11.11: без этого любая колонка считалась денежной и НДС/курс/площадь
// масштабировались).
const toColumnDef = (column) => column?.columnDef ?? column;
/**
* Текстовая колонка: значение показывается как есть, без числового форматирования
* и без масштабирования разрядностью.
*
* @param {{ accessorKey?: string, columnDef?: { accessorKey?: string } }} column
* @returns {boolean}
*/
export const isTextColumn = (column) => {
const accessorKey = toColumnDef(column)?.accessorKey;
if (TEXT_COLUMN_IDS.has(accessorKey)) return true;
const lastSegment = getLastSegment(accessorKey);
return TEXT_LAST_SEGMENTS.has(lastSegment) || hasSuffix(lastSegment, TEXT_SUFFIXES);
};
// Лист "АХР лимиты" (FORM_2/AHR_LIMIT) — нормативы хранятся в рублях, а не в тыс. руб. // Лист "АХР лимиты" (FORM_2/AHR_LIMIT) — нормативы хранятся в рублях, а не в тыс. руб.
// У листа нет селектора разрядности (SettingPanel), значения показываются как есть. // У листа нет селектора разрядности (SettingPanel), значения показываются как есть.
export const AHR_LIMIT_SHEET = 'AHR_LIMIT'; export const AHR_LIMIT_SHEET = 'AHR_LIMIT';
@ -97,16 +166,18 @@ const getLastSegment = (accessorKey) => {
* @param {string} [sheetName] * @param {string} [sheetName]
* @returns {'thousand'|'rub'|null} * @returns {'thousand'|'rub'|null}
*/ */
export const getMoneyUnit = (columnDef, sheetName) => { export const getMoneyUnit = (column, sheetName) => {
const columnDef = toColumnDef(column);
if (columnDef && Object.hasOwn(columnDef, 'moneyUnit')) { if (columnDef && Object.hasOwn(columnDef, 'moneyUnit')) {
return columnDef.moneyUnit; return columnDef.moneyUnit;
} }
const accessorKey = columnDef?.accessorKey; if (isTextColumn(columnDef)) return null;
if (TEXT_COLUMN_IDS.has(accessorKey)) return null;
const lastSegment = getLastSegment(accessorKey); const lastSegment = getLastSegment(columnDef?.accessorKey);
if (lastSegment.endsWith('_qty') || NON_MONEY_LAST_SEGMENTS.has(lastSegment)) return null; if (NUMERIC_NON_MONEY_LAST_SEGMENTS.has(lastSegment) || hasSuffix(lastSegment, NUMERIC_NON_MONEY_SUFFIXES)) {
return null;
}
if (sheetName === AHR_LIMIT_SHEET && AHR_LIMIT_RUB_LAST_SEGMENTS.has(lastSegment)) return 'rub'; if (sheetName === AHR_LIMIT_SHEET && AHR_LIMIT_RUB_LAST_SEGMENTS.has(lastSegment)) return 'rub';

View File

@ -7,6 +7,7 @@ import {
UNIT_IN_RUB, UNIT_IN_RUB,
getDisplayFactor, getDisplayFactor,
getMoneyUnit, getMoneyUnit,
isTextColumn,
resolveDenomination, resolveDenomination,
} from './moneyUtils.js'; } from './moneyUtils.js';
@ -54,6 +55,94 @@ test('getMoneyUnit respects an explicit moneyUnit override, including null', ()
assert.equal(getMoneyUnit({ accessorKey: 'data.qty_q1', moneyUnit: 'rub' }), 'rub'); assert.equal(getMoneyUnit({ accessorKey: 'data.qty_q1', moneyUnit: 'rub' }), 'rub');
}); });
// AURORA-1899, возврат тестирования 11.11: tableColumns передаёт в расчёт множителя
// MRT column (column.id + column.columnDef), у которого нет своего accessorKey.
test('getMoneyUnit reads accessorKey from an MRT column object via columnDef', () => {
const mrtColumn = (accessorKey) => ({ id: accessorKey, columnDef: { accessorKey } });
assert.equal(getMoneyUnit(mrtColumn('data.contract_detail.vat_rate'), 'OPER'), null);
assert.equal(getMoneyUnit(mrtColumn('data.contract_detail.exchange_rate'), 'OPER'), null);
assert.equal(getMoneyUnit(mrtColumn('data.contract_detail.amount_foreign'), 'OPER'), null);
assert.equal(getMoneyUnit(mrtColumn('data.header.internal_order'), 'OPER'), null);
assert.equal(getMoneyUnit(mrtColumn('data.rented_area'), 'AHR_RENT'), null);
assert.equal(getMoneyUnit(mrtColumn('data.plan.q1'), 'OPER'), 'thousand');
assert.equal(getMoneyUnit(mrtColumn('data.amount_q1'), 'AHR_LIMIT'), 'rub');
assert.equal(getMoneyUnit({ id: 'data.plan.q1', columnDef: { accessorKey: 'data.plan.q1', moneyUnit: null } }), null);
});
test('getMoneyUnit returns null for text columns: comments, justifications and other free text', () => {
for (const accessorKey of [
'data.plan.comment',
'data.q2.adj_comment',
'data.q2.base_correction_comment',
'data.q1.transfer_next_comment',
'data.q1.transfer_far_comment',
'data.seq_dfip.justification',
'data.header.justification',
'data.collegial.note',
'data.header.name',
'data.contract.counterparty',
'data.contract.subject',
'data.contract.reference',
'data.contract.deadline',
'data.contract.date',
'data.contract.currency',
'data.contract.act',
'data.contract.scheme',
'data.contract_detail.payment_scheme',
'data.contract_detail.rf_schedule',
'data.contract_detail.addenda',
'data.ckk.proc_plan',
'data.ckk.proc_method',
'data.allocation.property',
'data.collegial.protocol',
'data.collegial.approved',
'data.contract_summary.centralized_flag',
'data.contract_end_date',
'data.object_type',
'data.unit',
'data.address',
]) {
assert.equal(getMoneyUnit({ accessorKey }, 'OPER'), null, accessorKey);
}
});
// Возврат 11.11: внутренний заказ 10123 и комментарий «123» ожидаются как исходный текст,
// а не «10 123,0» / «123,0»; ставки и количества остаются числами.
test('isTextColumn marks codes and free text as text, numeric non-money columns as numbers', () => {
const mrtColumn = (accessorKey) => ({ id: accessorKey, columnDef: { accessorKey } });
for (const accessorKey of [
'data.header.internal_order',
'data.header.section_code',
'data.header.item_id',
'data.plan.comment',
'data.q2.adj_comment',
'data.seq_dfip.justification',
'data.contract.counterparty',
'data.article_id',
]) {
assert.equal(isTextColumn({ accessorKey }), true, accessorKey);
assert.equal(isTextColumn(mrtColumn(accessorKey)), true, `mrt ${accessorKey}`);
}
for (const accessorKey of [
'data.contract_detail.vat_rate',
'data.contract_detail.exchange_rate',
'data.contract_detail.amount_foreign',
'data.rented_area',
'data.qty_q1',
'data.acquired_qty',
'data.plan.q1',
]) {
assert.equal(isTextColumn({ accessorKey }), false, accessorKey);
}
});
test('getMoneyUnit keeps money columns whose names resemble text ones as thousand', () => {
assert.equal(getMoneyUnit({ accessorKey: 'data.collegial.amount' }, 'OPER'), 'thousand');
assert.equal(getMoneyUnit({ accessorKey: 'data.q1.transfer_q2_delay_acts' }, 'OPER'), 'thousand');
assert.equal(getMoneyUnit({ accessorKey: 'data.approved.q1' }, 'OPER'), 'thousand');
assert.equal(getMoneyUnit({ accessorKey: 'data.q1.pay_amount' }, 'OPER'), 'thousand');
});
test('resolveDenomination falls back to thousand when nothing is stored', () => { test('resolveDenomination falls back to thousand when nothing is stored', () => {
assert.equal(resolveDenomination(null), DEFAULT_DENOMINATION); assert.equal(resolveDenomination(null), DEFAULT_DENOMINATION);
assert.equal(resolveDenomination(undefined), 'thousand'); assert.equal(resolveDenomination(undefined), 'thousand');