Закрепить margin follow-up по выбранной позиции

This commit is contained in:
2026-06-02 00:23:36 +03:00
parent 88c92e316a
commit 908d011743
12 changed files with 731 additions and 15 deletions
@@ -1149,9 +1149,9 @@ function extractInventoryCounterpartyCandidates(row, excludedTokens = []) {
excludedComparableTokens.includes(comparable)) {
continue;
}
candidates.push(normalized);
candidates.push(collapseRepeatedInventoryPartyLabel(normalized));
}
const explicitCounterparty = normalizeCounterpartyDisplayLabel(row.counterparty);
const explicitCounterparty = collapseRepeatedInventoryPartyLabel(normalizeCounterpartyDisplayLabel(row.counterparty));
const explicitComparable = normalizeEntityToken(explicitCounterparty);
if (explicitCounterparty &&
explicitComparable &&
@@ -1163,6 +1163,50 @@ function extractInventoryCounterpartyCandidates(row, excludedTokens = []) {
}
return uniqueStrings(candidates);
}
function collapseRepeatedInventoryPartyLabel(value) {
const compact = String(value ?? "").replace(/\s+/gu, " ").trim();
if (!compact) {
return compact;
}
const tokens = compact.split(" ").filter(Boolean);
for (let size = 1; size <= Math.floor(tokens.length / 2); size += 1) {
const collapsedTokens = [];
let changed = false;
for (let index = 0; index < tokens.length;) {
const leftTokens = tokens.slice(index, index + size);
const rightTokens = tokens.slice(index + size, index + size * 2);
if (leftTokens.length === size &&
rightTokens.length === size &&
normalizeEntityToken(leftTokens.join(" ")) === normalizeEntityToken(rightTokens.join(" "))) {
collapsedTokens.push(...leftTokens);
index += size * 2;
changed = true;
}
else {
collapsedTokens.push(tokens[index]);
index += 1;
}
}
if (changed) {
return collapseRepeatedInventoryPartyLabel(collapsedTokens.join(" "));
}
}
return compact;
}
function uniqueCollapsedInventoryPartyLabels(values) {
const result = [];
const seen = new Set();
for (const value of values) {
const collapsed = collapseRepeatedInventoryPartyLabel(value);
const comparable = normalizeEntityToken(collapsed);
if (!collapsed || !comparable || seen.has(comparable)) {
continue;
}
seen.add(comparable);
result.push(collapsed);
}
return result;
}
function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
const items = uniqueStrings(rows
.map((row) => extractInventoryItemName(row))
@@ -1173,7 +1217,7 @@ function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
const organizations = uniqueStrings(rows
.map((row) => extractInventoryOrganizationName(row))
.filter((item) => Boolean(item)));
const counterparties = uniqueStrings(rows.flatMap((row) => extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens)));
const counterparties = uniqueCollapsedInventoryPartyLabels(rows.flatMap((row) => extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens)));
const documents = uniqueStrings(rows
.map((row) => String(row.registrator ?? "").trim())
.filter((item) => item.length > 0 && item !== "(без названия)"));
@@ -613,7 +613,7 @@ function hasInventorySupplierFollowupCue(text) {
}
function hasInventoryPurchaseDocumentsFollowupCue(text) {
const value = String(text ?? "");
return (/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value) ||
return (/(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|каким\s+документом\s+(?:это|его|её|ее|она|он|этот\s+товар|эта\s+позиция|эту\s+позицию)?\s*(?:подтвержден[оа]?|доказан[оа]?|закрыт[оа]?)|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(value) ||
/(?:(?:покажи|показать|выведи|дай)?[\s\S]{0,30}док(?:и|умент[а-яё]*)[\s\S]{0,80}(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару)|операци)|(?:по\s+(?:ним|ней|нему|этой\s+позиции|этому\s+товару))[\s\S]{0,80}док(?:и|умент[а-яё]*))/iu.test(value));
}
function hasInventoryProfitabilityFollowupCue(text) {
@@ -67,6 +67,61 @@ function normalizeInventoryReplyEntityToken(value) {
.trim();
return normalized || null;
}
function normalizeInventoryReplyLabelComparable(value) {
const normalized = String(value ?? "")
.trim()
.toLowerCase()
.replace(/ё/gu, "е")
.replace(/С‘/gu, "Рµ")
.replace(/[^\p{L}\p{N}]+/gu, " ")
.replace(/\s+/gu, " ")
.trim();
return normalized || null;
}
function collapseRepeatedInventoryReplyLabel(value) {
const compact = String(value ?? "").replace(/\s+/gu, " ").trim();
if (!compact) {
return compact;
}
const tokens = compact.split(/\s+/u).filter(Boolean);
for (let size = 1; size <= Math.floor(tokens.length / 2); size += 1) {
const collapsedTokens = [];
let changed = false;
for (let index = 0; index < tokens.length;) {
const leftTokens = tokens.slice(index, index + size);
const rightTokens = tokens.slice(index + size, index + size * 2);
if (leftTokens.length === size &&
rightTokens.length === size &&
normalizeInventoryReplyLabelComparable(leftTokens.join(" ")) === normalizeInventoryReplyLabelComparable(rightTokens.join(" "))) {
collapsedTokens.push(...leftTokens);
index += size * 2;
changed = true;
}
else {
collapsedTokens.push(tokens[index]);
index += 1;
}
}
if (changed) {
return collapseRepeatedInventoryReplyLabel(collapsedTokens.join(" "));
}
}
return compact;
}
function normalizeInventoryTraceSummaryCounterparties(summary) {
const normalized = [];
const seen = new Set();
for (const counterparty of summary.counterparties) {
const collapsed = collapseRepeatedInventoryReplyLabel(counterparty);
const comparable = normalizeInventoryReplyLabelComparable(collapsed);
if (!collapsed || !comparable || seen.has(comparable)) {
continue;
}
seen.add(comparable);
normalized.push(collapsed);
}
summary.counterparties = normalized;
}
function sumInventoryRowAmount(rows) {
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
}
@@ -112,6 +167,9 @@ function asksInventoryMarginAccount41Not01(userMessage) {
const text = String(userMessage ?? "").toLowerCase();
return /(?:\b41\b|41\s*сч|сч[её]т[ау]?\s*41)/iu.test(text) && /(?:\b01\b|не\s+ос|основн)/iu.test(text);
}
function asksForInventoryConfirmationDocument(userMessage) {
return /(?:каким\s+документом|чем\s+подтвержд|документ(?:ом|ами)?\s+подтвержд|покажи\s+закупочн(?:ый|ые)\s+документ|покажи\s+документ)/iu.test(String(userMessage ?? ""));
}
function inventoryRowItemLabel(row, deps) {
return deps.summarizeInventoryTraceRows([row]).item;
}
@@ -210,10 +268,15 @@ function composeInventoryReply(intent, rows, options, deps) {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
normalizeInventoryTraceSummaryCounterparties(summary);
const itemLabel = summary.item ?? "товар не определен";
const firstDocument = summary.documents[0] ?? null;
const confirmationDocumentFocus = asksForInventoryConfirmationDocument(options.userMessage);
const directAnswerLine = purchaseRows.length <= 0
? `По позиции ${itemLabel} подтвержденные документы закупки в доступных данных не найдены.`
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
: confirmationDocumentFocus && firstDocument
? `Документ подтверждения по позиции ${itemLabel}: ${firstDocument}.`
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
const lines = [directAnswerLine];
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
`Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`,
@@ -240,6 +303,7 @@ function composeInventoryReply(intent, rows, options, deps) {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
normalizeInventoryTraceSummaryCounterparties(summary);
const itemLabel = summary.item ?? "товар не определен";
const boundedAsOfLabel = asOfDate ? deps.formatDateRu(asOfDate) : null;
const purchaseDateActionFocus = deps.hasInventoryPurchaseDateActionFocus(options.userMessage);
@@ -330,6 +394,7 @@ function composeInventoryReply(intent, rows, options, deps) {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
normalizeInventoryTraceSummaryCounterparties(summary);
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
const unresolvedSupplierQuestion = /(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(String(options.userMessage ?? ""));
if (unresolvedSupplierQuestion) {
@@ -406,6 +471,7 @@ function composeInventoryReply(intent, rows, options, deps) {
return Boolean(itemToken && agingItemTokens.has(itemToken));
});
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
normalizeInventoryTraceSummaryCounterparties(summary);
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
@@ -454,6 +520,7 @@ function composeInventoryReply(intent, rows, options, deps) {
const requestedItemHint = String(options.itemHint ?? "").trim();
const provisionalExcludedTokens = requestedItemHint ? [requestedItemHint] : [];
const summary = deps.summarizeInventoryTraceRows(saleRows, provisionalExcludedTokens);
normalizeInventoryTraceSummaryCounterparties(summary);
const itemLabel = requestedItemHint || (summary.item ?? "товар не определен");
const excludedCounterpartyTokens = [itemLabel];
const directAnswerLine = summary.counterparties.length === 1
@@ -529,7 +596,7 @@ function composeInventoryReply(intent, rows, options, deps) {
"оплаты могут помочь сверить поступление денег, но сами по себе не подтверждают валовую прибыль по товарам;",
"строгий бухгалтерский расчет требует проводок реализации и себестоимости, а не только банковских движений."
]);
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", true));
}
if (confirmedEntries.length === 0) {
const costBaseRowsRequested = asksForInventoryCostBaseRows(options.userMessage);