Усилить бизнес-ответы 1С ассистента после прогона 105
This commit is contained in:
parent
bbc257fd6c
commit
151f2a26de
|
|
@ -129,15 +129,6 @@ function formatNumberWithDots(value, fractionDigits = 0) {
|
|||
function formatMoneyRub(value) {
|
||||
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||
}
|
||||
function formatVatProbeStatusRu(status) {
|
||||
if (status === "ok") {
|
||||
return "есть движения";
|
||||
}
|
||||
if (status === "empty") {
|
||||
return "движения не найдены";
|
||||
}
|
||||
return "ошибка запроса";
|
||||
}
|
||||
function emphasizeNumericTokens(line) {
|
||||
if (!line) {
|
||||
return line;
|
||||
|
|
@ -2127,10 +2118,11 @@ function appendDebtMirrorCompactDisclosure(lines, snapshot, kind) {
|
|||
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
||||
return;
|
||||
}
|
||||
lines.push(`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`);
|
||||
lines.push("", "Для сверки:");
|
||||
lines.push(`- Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`);
|
||||
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
||||
if (leadingMirror) {
|
||||
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||
lines.push(`- Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||
}
|
||||
}
|
||||
function appendDebtMirrorDisclosure(lines, snapshot, kind) {
|
||||
|
|
@ -2975,7 +2967,6 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const turnover19Credit = rowsByMarker.get("VAT_19_CREDIT") ?? 0;
|
||||
const netVat = turnover68Credit - turnover68Debit;
|
||||
const vatToPay = Math.max(0, netVat);
|
||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
||||
const totalVatTurnoverAbs = Math.abs(turnover68Credit) + Math.abs(turnover68Debit) + Math.abs(turnover19Debit) + Math.abs(turnover19Credit);
|
||||
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
||||
const netVatIsEffectivelyZero = Math.abs(netVat) <= 0.005;
|
||||
|
|
@ -2983,50 +2974,17 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
||||
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
||||
const formatForecastMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
const lines = [
|
||||
`Коротко: ориентир по НДС к уплате за ${periodWindowLabel ?? "доступный срез"} — ${formatForecastMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatForecastMoney(carryoverOrOverpayment)}.`,
|
||||
"Это предварительная оценка по оборотам 68.02*/19*, а не подтвержденная сумма налога по декларации.",
|
||||
"Это предварительная оценка по бухгалтерским оборотам НДС, а не подтвержденная сумма налога по декларации.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
`- Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
||||
`- Оборот по кредиту 68*: ${formatForecastMoney(turnover68Credit)}.`,
|
||||
`- Оборот по дебету 68*: ${formatForecastMoney(turnover68Debit)}.`,
|
||||
`- Нетто НДС (68 Кт - 68 Дт): ${formatForecastMoney(netVat)}.`,
|
||||
`- Справочно по 19*: дебет ${formatForecastMoney(turnover19Debit)}, кредит ${formatForecastMoney(turnover19Credit)}.`
|
||||
`- НДС начисленный: ${formatForecastMoney(turnover68Credit)}.`,
|
||||
`- Уменьшение/вычеты по НДС: ${formatForecastMoney(turnover68Debit)}.`,
|
||||
`- Нетто НДС: ${formatForecastMoney(netVat)}.`
|
||||
];
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
const statusRank = (status) => status === "ok" ? 0 : status === "empty" ? 1 : 2;
|
||||
const orderedProbeRows = [...vatProbe.probedSources].sort((a, b) => statusRank(a.status) - statusRank(b.status) ||
|
||||
a.fullName.localeCompare(b.fullName, "ru"));
|
||||
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
||||
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
if (visibleProbeRows.length > 0) {
|
||||
lines.push(...visibleProbeRows.map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
const extra = item.status === "ok"
|
||||
? item.lastPeriod
|
||||
? ` | последнее движение: ${item.lastPeriod}`
|
||||
: ""
|
||||
: item.status === "error" && item.error
|
||||
? ` | ошибка: ${item.error}`
|
||||
: "";
|
||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${extra}`;
|
||||
}));
|
||||
}
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
}
|
||||
if (!vatActivityDetected) {
|
||||
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
|
||||
}
|
||||
|
|
@ -3073,16 +3031,13 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
||||
const netVat = salesVat - purchaseVat;
|
||||
const vatToPay = Math.max(0, netVat);
|
||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
||||
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||
"Расчет сделан по книгам продаж и покупок.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
|
|
@ -3091,21 +3046,6 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||
];
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
lines.push("", "За выбранный налоговый период не найдены строки книг продаж/покупок, поэтому подтвержденная сумма к уплате равна 0.");
|
||||
}
|
||||
|
|
@ -3161,40 +3101,14 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const lines = [
|
||||
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный срез НДС к уплате по состоянию на дату.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
"Что учтено:",
|
||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||
"- Контур: остатки по счетам НДС к уплате (68*)."
|
||||
"- Контур: остатки по счетам НДС к уплате."
|
||||
];
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
||||
if (vatProbe.probedSources.length > 0) {
|
||||
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
const suffix = item.status === "ok"
|
||||
? `${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.sampleRegistrator ? ` | пример: ${item.sampleRegistrator}` : ""}`
|
||||
: item.status === "error" && item.error
|
||||
? ` | ошибка: ${item.error}`
|
||||
: "";
|
||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${suffix}`;
|
||||
}));
|
||||
}
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", "- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
|
||||
}
|
||||
lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции");
|
||||
lines.push("", "Сводка:", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Подтвержденные позиции:");
|
||||
if (accountRows.length > 0) {
|
||||
lines.push(...accountRows.slice(0, 12).map((item, index) => {
|
||||
const refs = Array.from(item.refs).slice(0, 2).join("; ");
|
||||
return `${index + 1}. ${item.account} | остаток НДС к уплате: ${formatMoneyRub(item.total)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${refs ? ` | source refs: ${refs}` : ""}`;
|
||||
return `${index + 1}. ${item.account} — остаток НДС к уплате: ${formatMoneyRub(item.total)}, операций: ${formatNumberWithDots(item.operations)}.`;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,14 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
const lines = [directAnswerLine];
|
||||
if (positions.length > 0) {
|
||||
const visiblePositionsLimit = 6;
|
||||
const visiblePositions = positions.slice(0, visiblePositionsLimit);
|
||||
const positionsByAmount = [...positions].sort((left, right) => {
|
||||
const amountDelta = right.amount - left.amount;
|
||||
if (amountDelta !== 0) {
|
||||
return amountDelta;
|
||||
}
|
||||
return left.item.localeCompare(right.item, "ru");
|
||||
});
|
||||
const visiblePositions = positionsByAmount.slice(0, visiblePositionsLimit);
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции:", visiblePositions.map((item, index) => (0, inventoryReplyPresentation_1.formatInventorySnapshotPositionLine)(item, index, {
|
||||
formatDateRu: deps.formatDateRu,
|
||||
formatNumberWithDots: deps.formatNumberWithDots,
|
||||
|
|
|
|||
|
|
@ -77,15 +77,54 @@ function aggregationAxisForPlanner(planner) {
|
|||
const axis = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_aggregation_axis)?.toLowerCase();
|
||||
return axis === "month" ? "month" : null;
|
||||
}
|
||||
function cleanExplicitEntityCandidate(value) {
|
||||
let text = value.replace(/\s+/g, " ").trim();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
text = text
|
||||
.replace(/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu, "")
|
||||
.replace(/^(?:по\s+)?(?:контрагент(?:у|ом)?|поставщик(?:у|ом)?|клиент(?:у|ом)?)\s+/iu, "")
|
||||
.replace(/\s+за\s+\d{4}(?:\s+год)?$/iu, "")
|
||||
.trim();
|
||||
return text || null;
|
||||
}
|
||||
function scoreExplicitEntityCandidate(raw, cleaned, index) {
|
||||
let score = 100 - index;
|
||||
if (raw.trim() === cleaned) {
|
||||
score += 20;
|
||||
}
|
||||
if (/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu.test(raw)) {
|
||||
score -= 40;
|
||||
}
|
||||
if (/[А-ЯЁA-Z]/u.test(cleaned)) {
|
||||
score += 8;
|
||||
}
|
||||
if (/(?:^|\s)(?:ООО|АО|ПАО|ИП|Группа|Комитет|Департамент)(?:\s|$)/u.test(cleaned)) {
|
||||
score += 6;
|
||||
}
|
||||
score -= Math.max(0, cleaned.length - 60);
|
||||
return score;
|
||||
}
|
||||
function firstEntityCandidate(planner) {
|
||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
for (const candidate of candidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
let best = null;
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const candidate = candidates[index];
|
||||
const raw = toNonEmptyString(candidate);
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
const cleaned = cleanExplicitEntityCandidate(raw);
|
||||
if (!cleaned) {
|
||||
continue;
|
||||
}
|
||||
const score = scoreExplicitEntityCandidate(raw, cleaned, index);
|
||||
if (!best || score > best.score) {
|
||||
best = { text: cleaned, score };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return best?.text ?? null;
|
||||
}
|
||||
function dateScopeToFilters(dateScope) {
|
||||
if (!dateScope) {
|
||||
|
|
|
|||
|
|
@ -931,6 +931,58 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
||||
const subject = organizationScope ?? "компания";
|
||||
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||
lines.push(`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`);
|
||||
lines.push("Что видно:");
|
||||
if (incomingAmount) {
|
||||
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||
}
|
||||
if (outgoingAmount) {
|
||||
lines.push(`- исходящие платежи/списания: ${outgoingAmount};`);
|
||||
}
|
||||
if (netAmount) {
|
||||
lines.push(`- ${netDirection}: ${sentenceAmount(netAmount) ?? netAmount};`);
|
||||
}
|
||||
if (customerName && customerAmount) {
|
||||
lines.push(topCustomerLooksFinancial
|
||||
? `- крупнейший входящий источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это похоже на финансовый контур, не на обычную клиентскую выручку;`
|
||||
: `- крупнейший источник денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это сигнал концентрации на крупном заказчике;`);
|
||||
}
|
||||
if (topSupplier) {
|
||||
lines.push(topSupplierLooksFinancial
|
||||
? `- крупнейший получатель исходящих денег: ${topSupplier}; это похоже на финансовый контур, не на обычного поставщика;`
|
||||
: `- крупнейший получатель исходящих денег: ${topSupplier};`);
|
||||
}
|
||||
const inventoryLine = businessOverviewInventoryLine(overview);
|
||||
if (inventoryLine) {
|
||||
lines.push(`- ${localizeLine(inventoryLine)}`);
|
||||
}
|
||||
const debtLine = businessOverviewDebtLine(overview);
|
||||
if (debtLine) {
|
||||
lines.push(`- ${localizeLine(debtLine)}`);
|
||||
}
|
||||
lines.push("Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль.");
|
||||
const missingOverviewFamilies = [];
|
||||
if (!businessOverviewTaxLine(overview)) {
|
||||
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||
}
|
||||
if (!debtLine) {
|
||||
missingOverviewFamilies.push("долги без даты среза");
|
||||
}
|
||||
if (!inventoryLine) {
|
||||
missingOverviewFamilies.push("склад без даты среза");
|
||||
}
|
||||
if (missingOverviewFamilies.length > 0) {
|
||||
lines.push(`Что не подтверждено в этом срезе: ${missingOverviewFamilies.join(", ")}.`);
|
||||
}
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
lines.push("Что проверить дальше: чистую прибыль через 90/91/99, маржинальность по проектам, зависимость от топ-3 контрагентов, старые складские остатки и зависшие расчеты.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
|
|
|
|||
|
|
@ -198,21 +198,21 @@ function loadCapabilitiesRegistry() {
|
|||
}
|
||||
}
|
||||
function buildCapabilityContractReplyFromRegistry() {
|
||||
const registry = loadCapabilitiesRegistry();
|
||||
const topGroups = registry.groups.filter((group) => group.group_code !== "boundaries").slice(0, 6);
|
||||
const groupLines = topGroups.map((group, index) => {
|
||||
const examples = group.typical_queries
|
||||
.slice(0, 2)
|
||||
.map((query) => query.trim())
|
||||
.filter((query) => query.length > 0)
|
||||
.join("; ");
|
||||
return `${index + 1}. ${group.group_title}: ${group.description}${examples ? `. Например: ${examples}` : "."}`;
|
||||
});
|
||||
return [
|
||||
"Могу помочь с вопросами по данным 1С в режиме чтения: НДС, контрагенты, долги, деньги и склад.",
|
||||
"По основным группам:",
|
||||
...groupLines,
|
||||
"Если нужно, подскажу, как лучше сформулировать запрос под вашу задачу.",
|
||||
"Могу быстро смотреть управленческие вещи по данным 1С в режиме чтения:",
|
||||
"- кто должен денег и кому должны;",
|
||||
"- какой год или месяц был самым денежным;",
|
||||
"- какие контрагенты дают основной поток;",
|
||||
"- что лежит на складе и какие остатки стареют;",
|
||||
"- сколько НДС к уплате за период;",
|
||||
"- какие документы, оплаты и договоры есть по контрагенту.",
|
||||
"",
|
||||
"Примеры запросов:",
|
||||
"- кто самый доходный клиент за все время",
|
||||
"- что зависло на складе",
|
||||
"- кому мы должны на сегодня",
|
||||
"- какое нетто по СВК за 2020",
|
||||
"- сколько НДС к уплате за 4 квартал 2019",
|
||||
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
||||
].join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,16 +237,6 @@ function formatMoneyRub(value: number): string {
|
|||
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||
}
|
||||
|
||||
function formatVatProbeStatusRu(status: VatDirectSourceProbeItem["status"]): string {
|
||||
if (status === "ok") {
|
||||
return "есть движения";
|
||||
}
|
||||
if (status === "empty") {
|
||||
return "движения не найдены";
|
||||
}
|
||||
return "ошибка запроса";
|
||||
}
|
||||
|
||||
function emphasizeNumericTokens(line: string): string {
|
||||
if (!line) {
|
||||
return line;
|
||||
|
|
@ -2766,12 +2756,13 @@ function appendDebtMirrorCompactDisclosure(
|
|||
if (snapshot.mirroredOffsetAmount <= 0.005) {
|
||||
return;
|
||||
}
|
||||
lines.push("", "Для сверки:");
|
||||
lines.push(
|
||||
`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`
|
||||
`- Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`
|
||||
);
|
||||
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
|
||||
if (leadingMirror) {
|
||||
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||
lines.push(`- Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3801,7 +3792,6 @@ function composeFactualReplyBody(
|
|||
|
||||
const netVat = turnover68Credit - turnover68Debit;
|
||||
const vatToPay = Math.max(0, netVat);
|
||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
||||
const totalVatTurnoverAbs =
|
||||
Math.abs(turnover68Credit) + Math.abs(turnover68Debit) + Math.abs(turnover19Debit) + Math.abs(turnover19Credit);
|
||||
const vatActivityDetected = totalVatTurnoverAbs > 0.0000001;
|
||||
|
|
@ -3810,67 +3800,20 @@ function composeFactualReplyBody(
|
|||
const shouldShowCalendarDetails = needsVatCalendarDetails(options.userMessage);
|
||||
const vatCalendar = deriveVatDeadlineCalendar(options.periodFrom, options.periodTo);
|
||||
const formatForecastMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const periodWindowLabel =
|
||||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
|
||||
const lines = [
|
||||
`Коротко: ориентир по НДС к уплате за ${periodWindowLabel ?? "доступный срез"} — ${formatForecastMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatForecastMoney(carryoverOrOverpayment)}.`,
|
||||
"Это предварительная оценка по оборотам 68.02*/19*, а не подтвержденная сумма налога по декларации.",
|
||||
"Это предварительная оценка по бухгалтерским оборотам НДС, а не подтвержденная сумма налога по декларации.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
`- Период оценки: ${periodWindowLabel ?? "не задан (использован доступный срез)"}.`,
|
||||
`- Оборот по кредиту 68*: ${formatForecastMoney(turnover68Credit)}.`,
|
||||
`- Оборот по дебету 68*: ${formatForecastMoney(turnover68Debit)}.`,
|
||||
`- Нетто НДС (68 Кт - 68 Дт): ${formatForecastMoney(netVat)}.`,
|
||||
`- Справочно по 19*: дебет ${formatForecastMoney(turnover19Debit)}, кредит ${formatForecastMoney(turnover19Credit)}.`
|
||||
`- НДС начисленный: ${formatForecastMoney(turnover68Credit)}.`,
|
||||
`- Уменьшение/вычеты по НДС: ${formatForecastMoney(turnover68Debit)}.`,
|
||||
`- Нетто НДС: ${formatForecastMoney(netVat)}.`
|
||||
];
|
||||
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
const statusRank = (status: VatDirectSourceProbeItem["status"]): number =>
|
||||
status === "ok" ? 0 : status === "empty" ? 1 : 2;
|
||||
const orderedProbeRows = [...vatProbe.probedSources].sort(
|
||||
(a, b) =>
|
||||
statusRank(a.status) - statusRank(b.status) ||
|
||||
a.fullName.localeCompare(b.fullName, "ru")
|
||||
);
|
||||
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
||||
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников в 1С:",
|
||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||
`- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`
|
||||
);
|
||||
if (visibleProbeRows.length > 0) {
|
||||
lines.push(
|
||||
...visibleProbeRows.map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
const extra =
|
||||
item.status === "ok"
|
||||
? item.lastPeriod
|
||||
? ` | последнее движение: ${item.lastPeriod}`
|
||||
: ""
|
||||
: item.status === "error" && item.error
|
||||
? ` | ошибка: ${item.error}`
|
||||
: "";
|
||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${extra}`;
|
||||
})
|
||||
);
|
||||
}
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
}
|
||||
|
||||
if (!vatActivityDetected) {
|
||||
lines.push(
|
||||
`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(
|
||||
|
|
@ -3950,18 +3893,15 @@ function composeFactualReplyBody(
|
|||
const purchaseVat = rowsByMarker.get("VAT_BOOK_PURCHASES") ?? 0;
|
||||
const netVat = salesVat - purchaseVat;
|
||||
const vatToPay = Math.max(0, netVat);
|
||||
const carryoverOrOverpayment = Math.max(0, -netVat);
|
||||
const periodWindowLabel =
|
||||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||
"Расчет сделан по книгам продаж и покупок.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
|
|
@ -3971,32 +3911,6 @@ function composeFactualReplyBody(
|
|||
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
|
||||
];
|
||||
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников в 1С:",
|
||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||
`- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`
|
||||
);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.",
|
||||
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
|
||||
);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
lines.push(
|
||||
"",
|
||||
|
|
@ -4070,63 +3984,24 @@ function composeFactualReplyBody(
|
|||
const lines: string[] = [
|
||||
`Итого подтвержденный НДС к уплате на ${formatDateRu(asOfDate)}: ${formatMoneyRub(totalVatPayable)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный срез НДС к уплате по состоянию на дату.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
"Что учтено:",
|
||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||
"- Контур: остатки по счетам НДС к уплате (68*)."
|
||||
"- Контур: остатки по счетам НДС к уплате."
|
||||
];
|
||||
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
||||
);
|
||||
if (vatProbe.probedSources.length > 0) {
|
||||
lines.push(
|
||||
...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
const suffix =
|
||||
item.status === "ok"
|
||||
? `${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.sampleRegistrator ? ` | пример: ${item.sampleRegistrator}` : ""}`
|
||||
: item.status === "error" && item.error
|
||||
? ` | ошибка: ${item.error}`
|
||||
: "";
|
||||
return `${index + 1}. ${name} | ${formatVatProbeStatusRu(item.status)}${suffix}`;
|
||||
})
|
||||
);
|
||||
}
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||
"- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 3. Сводка",
|
||||
"Сводка:",
|
||||
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||
`- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`,
|
||||
"",
|
||||
"Блок 4. Подтвержденные позиции"
|
||||
"Подтвержденные позиции:"
|
||||
);
|
||||
|
||||
if (accountRows.length > 0) {
|
||||
lines.push(
|
||||
...accountRows.slice(0, 12).map((item, index) => {
|
||||
const refs = Array.from(item.refs).slice(0, 2).join("; ");
|
||||
return `${index + 1}. ${item.account} | остаток НДС к уплате: ${formatMoneyRub(item.total)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${refs ? ` | source refs: ${refs}` : ""}`;
|
||||
return `${index + 1}. ${item.account} — остаток НДС к уплате: ${formatMoneyRub(item.total)}, операций: ${formatNumberWithDots(item.operations)}.`;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -184,7 +184,14 @@ export function composeInventoryReply(
|
|||
|
||||
if (positions.length > 0) {
|
||||
const visiblePositionsLimit = 6;
|
||||
const visiblePositions = positions.slice(0, visiblePositionsLimit);
|
||||
const positionsByAmount = [...positions].sort((left, right) => {
|
||||
const amountDelta = right.amount - left.amount;
|
||||
if (amountDelta !== 0) {
|
||||
return amountDelta;
|
||||
}
|
||||
return left.item.localeCompare(right.item, "ru");
|
||||
});
|
||||
const visiblePositions = positionsByAmount.slice(0, visiblePositionsLimit);
|
||||
appendInventorySection(
|
||||
lines,
|
||||
"Позиции:",
|
||||
|
|
|
|||
|
|
@ -717,15 +717,59 @@ function aggregationAxisForPlanner(
|
|||
return axis === "month" ? "month" : null;
|
||||
}
|
||||
|
||||
function cleanExplicitEntityCandidate(value: string): string | null {
|
||||
let text = value.replace(/\s+/g, " ").trim();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
text = text
|
||||
.replace(
|
||||
/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu,
|
||||
""
|
||||
)
|
||||
.replace(/^(?:по\s+)?(?:контрагент(?:у|ом)?|поставщик(?:у|ом)?|клиент(?:у|ом)?)\s+/iu, "")
|
||||
.replace(/\s+за\s+\d{4}(?:\s+год)?$/iu, "")
|
||||
.trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function scoreExplicitEntityCandidate(raw: string, cleaned: string, index: number): number {
|
||||
let score = 100 - index;
|
||||
if (raw.trim() === cleaned) {
|
||||
score += 20;
|
||||
}
|
||||
if (/^(?:по\s+)?(?:деньгам|деньги|денежн(?:ый|ые|ого|ому|ым|ом)?\s+поток(?:у|ом|а|и)?|нетто|расчет(?:ам|ы)?|получили|получено|заплатили|уплачено|сколько)\s+(?:с|по)\s+/iu.test(raw)) {
|
||||
score -= 40;
|
||||
}
|
||||
if (/[А-ЯЁA-Z]/u.test(cleaned)) {
|
||||
score += 8;
|
||||
}
|
||||
if (/(?:^|\s)(?:ООО|АО|ПАО|ИП|Группа|Комитет|Департамент)(?:\s|$)/u.test(cleaned)) {
|
||||
score += 6;
|
||||
}
|
||||
score -= Math.max(0, cleaned.length - 60);
|
||||
return score;
|
||||
}
|
||||
|
||||
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
|
||||
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
for (const candidate of candidates) {
|
||||
const text = toNonEmptyString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
let best: { text: string; score: number } | null = null;
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
const candidate = candidates[index];
|
||||
const raw = toNonEmptyString(candidate);
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
const cleaned = cleanExplicitEntityCandidate(raw);
|
||||
if (!cleaned) {
|
||||
continue;
|
||||
}
|
||||
const score = scoreExplicitEntityCandidate(raw, cleaned, index);
|
||||
if (!best || score > best.score) {
|
||||
best = { text: cleaned, score };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return best?.text ?? null;
|
||||
}
|
||||
|
||||
function dateScopeToFilters(dateScope: string | null): Pick<AddressFilterSet, "period_from" | "period_to"> {
|
||||
|
|
|
|||
|
|
@ -1110,6 +1110,69 @@ function buildCompactBusinessOverviewReply(
|
|||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (!separateSubject && !crossScopeExecutiveSummary && (actionFamily === "broad_evaluation" || unsupportedFamily === "broad_business_evaluation")) {
|
||||
const subject = organizationScope ?? "компания";
|
||||
const periodWithoutPrefix = period.replace(/^за\s+/iu, "");
|
||||
lines.push(
|
||||
`Коротко: по доступным данным ${subject} выглядит как бизнес с крупными контрактными денежными потоками и заметной зависимостью от нескольких крупных контрагентов, а не как равномерный поток мелких продаж.`
|
||||
);
|
||||
lines.push("Что видно:");
|
||||
if (incomingAmount) {
|
||||
lines.push(`- входящие деньги за ${periodWithoutPrefix}: ${incomingAmount};`);
|
||||
}
|
||||
if (outgoingAmount) {
|
||||
lines.push(`- исходящие платежи/списания: ${outgoingAmount};`);
|
||||
}
|
||||
if (netAmount) {
|
||||
lines.push(`- ${netDirection}: ${sentenceAmount(netAmount) ?? netAmount};`);
|
||||
}
|
||||
if (customerName && customerAmount) {
|
||||
lines.push(
|
||||
topCustomerLooksFinancial
|
||||
? `- крупнейший входящий источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это похоже на финансовый контур, не на обычную клиентскую выручку;`
|
||||
: `- крупнейший источник денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}; это сигнал концентрации на крупном заказчике;`
|
||||
);
|
||||
}
|
||||
if (topSupplier) {
|
||||
lines.push(
|
||||
topSupplierLooksFinancial
|
||||
? `- крупнейший получатель исходящих денег: ${topSupplier}; это похоже на финансовый контур, не на обычного поставщика;`
|
||||
: `- крупнейший получатель исходящих денег: ${topSupplier};`
|
||||
);
|
||||
}
|
||||
const inventoryLine = businessOverviewInventoryLine(overview);
|
||||
if (inventoryLine) {
|
||||
lines.push(`- ${localizeLine(inventoryLine)}`);
|
||||
}
|
||||
const debtLine = businessOverviewDebtLine(overview);
|
||||
if (debtLine) {
|
||||
lines.push(`- ${localizeLine(debtLine)}`);
|
||||
}
|
||||
lines.push(
|
||||
"Ограничение: это оценка по денежным потокам и найденным срезам 1С, не аудиторское заключение и не подтвержденная чистая прибыль."
|
||||
);
|
||||
const missingOverviewFamilies: string[] = [];
|
||||
if (!businessOverviewTaxLine(overview)) {
|
||||
missingOverviewFamilies.push("НДС/налоговая позиция без отдельного точного расчета");
|
||||
}
|
||||
if (!debtLine) {
|
||||
missingOverviewFamilies.push("долги без даты среза");
|
||||
}
|
||||
if (!inventoryLine) {
|
||||
missingOverviewFamilies.push("склад без даты среза");
|
||||
}
|
||||
if (missingOverviewFamilies.length > 0) {
|
||||
lines.push(`Что не подтверждено в этом срезе: ${missingOverviewFamilies.join(", ")}.`);
|
||||
}
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
lines.push(
|
||||
"Что проверить дальше: чистую прибыль через 90/91/99, маржинальность по проектам, зависимость от топ-3 контрагентов, старые складские остатки и зависшие расчеты."
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(
|
||||
`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`
|
||||
|
|
|
|||
|
|
@ -219,22 +219,21 @@ export function loadCapabilitiesRegistry(): CapabilityRegistry {
|
|||
}
|
||||
|
||||
export function buildCapabilityContractReplyFromRegistry(): string {
|
||||
const registry = loadCapabilitiesRegistry();
|
||||
const topGroups = registry.groups.filter((group) => group.group_code !== "boundaries").slice(0, 6);
|
||||
const groupLines = topGroups.map((group, index) => {
|
||||
const examples = group.typical_queries
|
||||
.slice(0, 2)
|
||||
.map((query) => query.trim())
|
||||
.filter((query) => query.length > 0)
|
||||
.join("; ");
|
||||
return `${index + 1}. ${group.group_title}: ${group.description}${examples ? `. Например: ${examples}` : "."}`;
|
||||
});
|
||||
|
||||
return [
|
||||
"Могу помочь с вопросами по данным 1С в режиме чтения: НДС, контрагенты, долги, деньги и склад.",
|
||||
"По основным группам:",
|
||||
...groupLines,
|
||||
"Если нужно, подскажу, как лучше сформулировать запрос под вашу задачу.",
|
||||
"Могу быстро смотреть управленческие вещи по данным 1С в режиме чтения:",
|
||||
"- кто должен денег и кому должны;",
|
||||
"- какой год или месяц был самым денежным;",
|
||||
"- какие контрагенты дают основной поток;",
|
||||
"- что лежит на складе и какие остатки стареют;",
|
||||
"- сколько НДС к уплате за период;",
|
||||
"- какие документы, оплаты и договоры есть по контрагенту.",
|
||||
"",
|
||||
"Примеры запросов:",
|
||||
"- кто самый доходный клиент за все время",
|
||||
"- что зависло на складе",
|
||||
"- кому мы должны на сегодня",
|
||||
"- какое нетто по СВК за 2020",
|
||||
"- сколько НДС к уплате за 4 квартал 2019",
|
||||
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
|
||||
].join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2092,9 +2092,11 @@ describe("address compose stage utf8 headers", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("Покрытие VAT-источников в 1С");
|
||||
expect(reply.text).toContain("Найдено VAT-объектов: 5");
|
||||
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
|
||||
expect(reply.text).toContain("предварительная оценка по бухгалтерским оборотам НДС");
|
||||
expect(reply.text).toContain("НДС начисленный");
|
||||
expect(reply.text).not.toContain("Покрытие VAT-источников в 1С");
|
||||
expect(reply.text).not.toContain("VAT-объектов");
|
||||
expect(reply.text).not.toContain("РегистрНакопления.НДСПродажи");
|
||||
});
|
||||
|
||||
it("builds confirmed VAT tax-period reply from sales and purchase book markers", () => {
|
||||
|
|
@ -2194,7 +2196,7 @@ describe("address compose stage utf8 headers", () => {
|
|||
expect(reply.text).not.toContain("**1**)");
|
||||
});
|
||||
|
||||
it("keeps VAT probe timestamps intact when numeric emphasis is enabled", () => {
|
||||
it("keeps VAT probe diagnostics out of the user-facing estimate when numeric emphasis is enabled", () => {
|
||||
const reply = composeFactualReply(
|
||||
"vat_payable_forecast",
|
||||
[
|
||||
|
|
@ -2229,11 +2231,12 @@ describe("address compose stage utf8 headers", () => {
|
|||
}
|
||||
);
|
||||
|
||||
expect(reply.text).toContain("последнее движение: 2019-12-31T23:59:59Z");
|
||||
expect(reply.text).toContain("предварительная оценка по бухгалтерским оборотам НДС");
|
||||
expect(reply.text).not.toContain("последнее движение: 2019-12-31T23:59:59Z");
|
||||
expect(reply.text).not.toContain("2019****-12**-31T23:**59**:59Z");
|
||||
});
|
||||
|
||||
it("adds MCP VAT source probe block for confirmed VAT as-of response", () => {
|
||||
it("hides MCP VAT source probe diagnostics for confirmed VAT as-of response", () => {
|
||||
const reply = composeFactualReply(
|
||||
"vat_payable_confirmed_as_of_date",
|
||||
[
|
||||
|
|
@ -2275,10 +2278,10 @@ describe("address compose stage utf8 headers", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text).toContain("Блок 2.1. Проверка VAT-источников в 1С");
|
||||
expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3");
|
||||
expect(reply.text).toContain("Источников с движениями до даты среза: 1");
|
||||
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
|
||||
expect(reply.text).toContain("Что учтено:");
|
||||
expect(reply.text).toContain("Подтвержденные позиции:");
|
||||
expect(reply.text).not.toContain("VAT-объектов");
|
||||
expect(reply.text).not.toContain("РегистрНакопления.НДСНачисленный");
|
||||
});
|
||||
|
||||
it("adds VAT probe error note for confirmed VAT as-of response", () => {
|
||||
|
|
@ -2308,7 +2311,8 @@ describe("address compose stage utf8 headers", () => {
|
|||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text).toContain("Дополнительная проверка VAT-источников завершилась ошибкой");
|
||||
expect(reply.text).toContain("Подтвержденные позиции:");
|
||||
expect(reply.text).not.toContain("Дополнительная проверка VAT-источников завершилась ошибкой");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -5727,6 +5731,47 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
|||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||
});
|
||||
|
||||
it("sorts inventory-on-hand positions by amount before rendering the top list", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_on_hand_as_of_date",
|
||||
[
|
||||
{
|
||||
period: "2020-03-31T23:59:59Z",
|
||||
registrator: "Остатки на дату",
|
||||
account_dt: "41.01",
|
||||
account_kt: "",
|
||||
amount: 148261.67,
|
||||
analytics: ["Модуль прямоугольный", "Основной склад", "ООО Ромашка"],
|
||||
item: "Модуль прямоугольный",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка",
|
||||
quantity: 22
|
||||
},
|
||||
{
|
||||
period: "2020-03-31T23:59:59Z",
|
||||
registrator: "Остатки на дату",
|
||||
account_dt: "41.01",
|
||||
account_kt: "",
|
||||
amount: 498472.5,
|
||||
analytics: ["Конструкция трансформер рабочей станции", "Основной склад", "ООО Ромашка"],
|
||||
item: "Конструкция трансформер рабочей станции",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка",
|
||||
quantity: 3
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2020-03-31",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text.indexOf("Конструкция трансформер рабочей станции")).toBeLessThan(
|
||||
reply.text.indexOf("Модуль прямоугольный")
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps supplier-overlap reply business-first without exact contour leakage", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
|
|
|
|||
|
|
@ -1750,6 +1750,51 @@ describe("assistant MCP discovery pilot executor", () => {
|
|||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("prefers clean counterparty candidates for bidirectional net value-flow probes", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["деньгам с группа свк", "Группа СВК"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const deps = buildSequentialDeps([
|
||||
{
|
||||
rows: [{ Period: "2020-06-30T00:00:00", Amount: 12093465, Counterparty: "Группа СВК" }]
|
||||
},
|
||||
{
|
||||
rows: []
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.derived_bidirectional_value_flow).toMatchObject({
|
||||
counterparty: "Группа СВК",
|
||||
period_scope: "2020",
|
||||
net_amount: 12093465,
|
||||
net_direction: "net_incoming",
|
||||
incoming_customer_revenue: {
|
||||
rows_matched: 1,
|
||||
total_amount: 12093465
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
rows_matched: 0,
|
||||
total_amount: 0
|
||||
}
|
||||
});
|
||||
expect(result.reason_codes).toContain("pilot_derived_bidirectional_value_flow_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
||||
const serializedCalls = deps.executeAddressMcpQuery.mock.calls
|
||||
.map((call) => JSON.stringify(call[0]))
|
||||
.join("\n");
|
||||
expect(serializedCalls).toContain("%Группа%");
|
||||
expect(serializedCalls).toContain("%СВК%");
|
||||
expect(serializedCalls).not.toContain("деньгам с группа свк");
|
||||
});
|
||||
|
||||
it("preserves explicit date ranges when building bidirectional value-flow probes", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue