Сжать долговые ответы и техничность net-flow

This commit is contained in:
dctouch 2026-05-22 09:05:58 +03:00
parent 151f2a26de
commit 1a94e72381
4 changed files with 62 additions and 129 deletions

View File

@ -2093,36 +2093,31 @@ function formatPayablesEvidenceSuffix(item) {
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
parts.push(`документ: ${item.documents[0]}${suffix}`);
}
if (item.sourceRefs.length > 0) {
parts.push(`source refs: ${item.sourceRefs.slice(0, 2).join("; ")}`);
}
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function formatDebtMirrorGroupLine(item) {
const details = [
item.account ? `счет ${item.account}` : null,
item.contract ? `договор/аналитика: ${item.contract}` : null,
item.organization ? `организация: ${item.organization}` : null
].filter((part) => Boolean(part));
const netText = Math.abs(item.netAmount) <= 0.005
? "чисто: 0 ₽"
: item.netAmount > 0
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
return `${item.name}: встречная часть ${formatMoneyRub(Math.min(item.debitAmount, item.creditAmount))}, ${netText}.`;
}
function debtMirrorCleanScopeLabel(kind) {
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
function debtMirrorCleanScopeLabel(kind, grammaticalCase) {
if (kind === "payables") {
return grammaticalCase === "genitive" ? "чистого долга к оплате" : "чистый долг к оплате";
}
return grammaticalCase === "genitive" ? "чистой дебиторки к получению" : "чистую дебиторку к получению";
}
function appendDebtMirrorCompactDisclosure(lines, snapshot, kind) {
if (snapshot.mirroredOffsetAmount <= 0.005) {
return;
}
lines.push("", "Для сверки:");
lines.push(`- Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`);
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она не включена в ${debtMirrorCleanScopeLabel(kind, "accusative")}.`);
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
if (leadingMirror) {
lines.push(`- Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
}
}
function appendDebtMirrorDisclosure(lines, snapshot, kind) {
@ -2131,8 +2126,11 @@ function appendDebtMirrorDisclosure(lines, snapshot, kind) {
}
lines.push("");
lines.push("Встречные остатки к сверке");
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`);
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind, "genitive")}.`);
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
if (leadingMirror) {
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
}
}
function deriveOperationalYearWindow(yearDocs, yearOps) {
const docsSeries = [...yearDocs].sort((a, b) => a.year - b.year);
@ -3381,8 +3379,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
: [`Коротко: на ${formatDateRu(payablesAsOfDate)} подтвержденных обязательств к оплате не найдено.`];
if (leading) {
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
}));
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
@ -3429,7 +3426,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push("Подтвержденные позиции к оплате");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
]));
if (lines[lines.length - 1] === "") {
@ -3477,8 +3474,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
: [`Коротко: на ${formatDateRu(receivablesAsOfDate)} подтвержденной дебиторской задолженности не найдено.`];
if (leading) {
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
}));
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
@ -3525,7 +3521,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push("Подтвержденные позиции к получению");
if (confirmedBalances.length > 0) {
lines.push(...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
]));
if (lines[lines.length - 1] === "") {
@ -3559,7 +3555,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const carryoverLine = asOfDate || periodFrom || periodTo
? "- В список могут попадать обязательства, возникшие раньше выбранного периода, если они потенциально оставались открытыми на дату среза."
: null;
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
const pushCategorySlice = (lines, title, items, limit) => {
if (items.length === 0) {
return;
@ -3656,7 +3652,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`,
"",
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
];
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
return {

View File

@ -215,7 +215,7 @@ function localizeLine(value) {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
}
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
}
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
@ -531,26 +531,6 @@ function businessOverviewInventoryLine(overview) {
].filter((item) => Boolean(item));
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
}
function rowCountText(value) {
const count = Number(value);
return Number.isFinite(count) ? String(count) : null;
}
function sideRowsText(side) {
const rowsWithAmount = rowCountText(side?.rows_with_amount);
const rowsMatched = rowCountText(side?.rows_matched);
if (rowsWithAmount && rowsMatched) {
return `${rowsWithAmount} из ${rowsMatched}`;
}
return rowsWithAmount ?? rowsMatched;
}
function sideDateText(side) {
const first = toNonEmptyString(side?.first_movement_date);
const latest = toNonEmptyString(side?.latest_movement_date);
if (first && latest) {
return first === latest ? `дата ${first}` : `даты ${first}..${latest}`;
}
return first ? `первая дата ${first}` : latest ? `последняя дата ${latest}` : null;
}
function bidirectionalNetLabel(direction) {
if (direction === "net_outgoing") {
return "нетто в сторону контрагента";
@ -586,28 +566,24 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
: "по выбранному контуру";
const period = toNonEmptyString(flow.period_scope);
const periodText = period ? ` за период ${period}` : " в проверенном окне";
const incomingRows = sideRowsText(incoming);
const outgoingRows = sideRowsText(outgoing);
const incomingDates = sideDateText(incoming);
const outgoingDates = sideDateText(outgoing);
const netLabel = bidirectionalNetLabel(flow.net_direction);
const lines = [
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
];
const basis = [];
if (incomingRows) {
basis.push(`входящих строк с суммой ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
if (incomingAmount) {
basis.push("входящие платежи");
}
if (outgoingRows) {
basis.push(`исходящих строк с суммой ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
if (outgoingAmount) {
basis.push("исходящие платежи");
}
if (basis.length > 0) {
lines.push(`Основа: ${basis.join("; ")}.`);
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`);
}
if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
}
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
lines.push("Метод: нетто рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи; это не полное бухгалтерское сальдо вне проверенного окна.");
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
if (fallbackNextStep) {
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
@ -673,21 +649,17 @@ function buildPreviousCounterpartyValueFlowSummary(flow, separateSubject, docume
const lead = `; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` +
`${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`;
const basis = [];
const incomingRows = sideRowsText(incoming);
const outgoingRows = sideRowsText(outgoing);
const incomingDates = sideDateText(incoming);
const outgoingDates = sideDateText(outgoing);
if (incomingRows) {
basis.push(`входящие строки ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
if (incomingAmount) {
basis.push("входящие платежи");
}
if (outgoingRows) {
basis.push(`исходящие строки ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
if (outgoingAmount) {
basis.push("исходящие платежи");
}
const documents = previousDocumentSummaryLine(documentBundle, counterparty);
if (documents) {
basis.push(documents);
}
const basisText = basis.length > 0 ? ` Основа: ${basis.join("; ")}.` : "";
const basisText = basis.length > 0 ? ` Основа: проверенные ${basis.join(" и ")}.` : "";
return {
lead,
line: `Отдельно по контрагенту ${counterparty}: подтверждено получили ${incomingAmount ?? "0 руб."}, ` +

View File

@ -2723,29 +2723,24 @@ function formatPayablesEvidenceSuffix(item: PayablesConfirmedBalanceAggregate):
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
parts.push(`документ: ${item.documents[0]}${suffix}`);
}
if (item.sourceRefs.length > 0) {
parts.push(`source refs: ${item.sourceRefs.slice(0, 2).join("; ")}`);
}
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function formatDebtMirrorGroupLine(item: DebtMirrorBalanceGroup): string {
const details = [
item.account ? `счет ${item.account}` : null,
item.contract ? `договор/аналитика: ${item.contract}` : null,
item.organization ? `организация: ${item.organization}` : null
].filter((part): part is string => Boolean(part));
const netText =
Math.abs(item.netAmount) <= 0.005
? "чисто: 0 ₽"
: item.netAmount > 0
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
return `${item.name}: встречная часть ${formatMoneyRub(Math.min(item.debitAmount, item.creditAmount))}, ${netText}.`;
}
function debtMirrorCleanScopeLabel(kind: "payables" | "receivables"): string {
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
function debtMirrorCleanScopeLabel(kind: "payables" | "receivables", grammaticalCase: "accusative" | "genitive"): string {
if (kind === "payables") {
return grammaticalCase === "genitive" ? "чистого долга к оплате" : "чистый долг к оплате";
}
return grammaticalCase === "genitive" ? "чистой дебиторки к получению" : "чистую дебиторку к получению";
}
function appendDebtMirrorCompactDisclosure(
@ -2758,11 +2753,11 @@ function appendDebtMirrorCompactDisclosure(
}
lines.push("", "Для сверки:");
lines.push(
`- Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она не включена в ${debtMirrorCleanScopeLabel(kind, "accusative")}.`
);
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
if (leadingMirror) {
lines.push(`- Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
}
}
@ -2777,9 +2772,12 @@ function appendDebtMirrorDisclosure(
lines.push("");
lines.push("Встречные остатки к сверке");
lines.push(
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind, "genitive")}.`
);
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
if (leadingMirror) {
lines.push(`- Крупнейший встречный контрагент: ${formatDebtMirrorGroupLine(leadingMirror)}`);
}
}
function deriveOperationalYearWindow(
@ -4341,8 +4339,7 @@ function composeFactualReplyBody(
if (leading) {
compactLines.push(
...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
})
);
if (confirmedBalances.length > 5) {
@ -4396,7 +4393,7 @@ function composeFactualReplyBody(
if (confirmedBalances.length > 0) {
lines.push(
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
])
);
@ -4454,8 +4451,7 @@ function composeFactualReplyBody(
if (leading) {
compactLines.push(
...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.).`;
})
);
if (confirmedBalances.length > 5) {
@ -4509,7 +4505,7 @@ function composeFactualReplyBody(
if (confirmedBalances.length > 0) {
lines.push(
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
""
])
);
@ -4548,7 +4544,7 @@ function composeFactualReplyBody(
: null;
const formatHeuristicItem = (item: PayablesCounterpartyRiskAggregate, index: number): string =>
`${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
`${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
const pushCategorySlice = (
lines: string[],
@ -4665,7 +4661,7 @@ function composeFactualReplyBody(
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
...confirmedBalances.slice(0, 10).map(
(item, index) =>
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
];
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");

View File

@ -262,7 +262,7 @@ function localizeLine(value: string): string {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
}
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
return "Нетто денежного потока рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи в 1С.";
}
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
@ -614,29 +614,6 @@ function businessOverviewInventoryLine(overview: Record<string, unknown>): strin
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
}
function rowCountText(value: unknown): string | null {
const count = Number(value);
return Number.isFinite(count) ? String(count) : null;
}
function sideRowsText(side: Record<string, unknown> | null): string | null {
const rowsWithAmount = rowCountText(side?.rows_with_amount);
const rowsMatched = rowCountText(side?.rows_matched);
if (rowsWithAmount && rowsMatched) {
return `${rowsWithAmount} из ${rowsMatched}`;
}
return rowsWithAmount ?? rowsMatched;
}
function sideDateText(side: Record<string, unknown> | null): string | null {
const first = toNonEmptyString(side?.first_movement_date);
const latest = toNonEmptyString(side?.latest_movement_date);
if (first && latest) {
return first === latest ? `дата ${first}` : `даты ${first}..${latest}`;
}
return first ? `первая дата ${first}` : latest ? `последняя дата ${latest}` : null;
}
function bidirectionalNetLabel(direction: unknown): string {
if (direction === "net_outgoing") {
return "нетто в сторону контрагента";
@ -678,29 +655,25 @@ function buildCompactBidirectionalValueFlowReply(
: "по выбранному контуру";
const period = toNonEmptyString(flow.period_scope);
const periodText = period ? ` за период ${period}` : " в проверенном окне";
const incomingRows = sideRowsText(incoming);
const outgoingRows = sideRowsText(outgoing);
const incomingDates = sideDateText(incoming);
const outgoingDates = sideDateText(outgoing);
const netLabel = bidirectionalNetLabel(flow.net_direction);
const lines = [
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
];
const basis: string[] = [];
if (incomingRows) {
basis.push(`входящих строк с суммой ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
if (incomingAmount) {
basis.push("входящие платежи");
}
if (outgoingRows) {
basis.push(`исходящих строк с суммой ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
if (outgoingAmount) {
basis.push("исходящие платежи");
}
if (basis.length > 0) {
lines.push(`Основа: ${basis.join("; ")}.`);
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`);
}
if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
}
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
lines.push("Метод: нетто рассчитано как подтвержденные входящие платежи минус подтвержденные исходящие платежи; это не полное бухгалтерское сальдо вне проверенного окна.");
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
if (fallbackNextStep) {
@ -785,21 +758,17 @@ function buildPreviousCounterpartyValueFlowSummary(
`; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` +
`${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`;
const basis: string[] = [];
const incomingRows = sideRowsText(incoming);
const outgoingRows = sideRowsText(outgoing);
const incomingDates = sideDateText(incoming);
const outgoingDates = sideDateText(outgoing);
if (incomingRows) {
basis.push(`входящие строки ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`);
if (incomingAmount) {
basis.push("входящие платежи");
}
if (outgoingRows) {
basis.push(`исходящие строки ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`);
if (outgoingAmount) {
basis.push("исходящие платежи");
}
const documents = previousDocumentSummaryLine(documentBundle, counterparty);
if (documents) {
basis.push(documents);
}
const basisText = basis.length > 0 ? ` Основа: ${basis.join("; ")}.` : "";
const basisText = basis.length > 0 ? ` Основа: проверенные ${basis.join(" и ")}.` : "";
return {
lead,
line: