Merge pull request 'AURORA-1899: возврат тестирования — разрядность не трогает неденежные и текстовые колонки' (#308) from feat/285-aurora-1899-non-money-fix into test
This commit is contained in:
commit
dd40b9f341
@ -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) {
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user