Починить Hm-прогон ассистента по НДС, остаткам и MCP-ответам

This commit is contained in:
2026-05-25 22:51:15 +03:00
parent 36f7b4df03
commit c577ac6b00
37 changed files with 2066 additions and 371 deletions
@@ -549,9 +549,33 @@ function needsVatPurchaseDateAnchorDisclosure(userMessage) {
}
return /(?:дата|дату|дате|момент)\s+(?:покуп|закуп)|(?:покуп|закуп)\S*\s+(?:дат|момент)|purchase\s+date|date\s+of\s+purchase/iu.test(text);
}
function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
if (!periodWindowLabel || !needsVatPurchaseDateAnchorDisclosure(options.userMessage)) {
return null;
function vatPurchaseDateBasisLabel(basis, hasMultiplePurchaseDates) {
if (!hasMultiplePurchaseDates) {
return "подтвержденную дату закупки";
}
if (basis === "last_confirmed_purchase") {
return "последнюю подтвержденную дату закупки";
}
return "первую подтвержденную дату закупки";
}
function buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel) {
const bridge = options.purchaseDateBridge ?? null;
const selectedPurchaseDate = normalizeIsoDateOnly(bridge?.selectedPurchaseDate);
const firstPurchaseDate = normalizeIsoDateOnly(bridge?.firstPurchaseDate);
const lastPurchaseDate = normalizeIsoDateOnly(bridge?.lastPurchaseDate);
const hasMultiplePurchaseDates = Boolean(bridge?.hasMultiplePurchaseDates || (firstPurchaseDate && lastPurchaseDate && firstPurchaseDate !== lastPurchaseDate));
if (!periodWindowLabel || (!selectedPurchaseDate && !needsVatPurchaseDateAnchorDisclosure(options.userMessage))) {
return [];
}
if (selectedPurchaseDate) {
const basisLabel = vatPurchaseDateBasisLabel(bridge?.basis, hasMultiplePurchaseDates);
const lines = [
`- Якорь периода: для расчета я использую ${basisLabel} ${formatDateRu(selectedPurchaseDate)}; налоговый период ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
];
if (hasMultiplePurchaseDates && firstPurchaseDate && lastPurchaseDate) {
lines.push(`- Важно: у позиции несколько подтвержденных дат закупки (${formatDateRu(firstPurchaseDate)}..${formatDateRu(lastPurchaseDate)}); это расчет по выбранному якорю, а не единственная возможная дата.`);
}
return lines;
}
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
@@ -565,9 +589,13 @@ function buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel) {
toTs !== null &&
asOfTs >= fromTs &&
asOfTs <= toTs) {
return `- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`;
return [
`- Якорь периода: дата покупки ${formatDateRu(asOfDate)} попадает в налоговый период ${periodWindowLabel}; поэтому расчет ниже взят из книг продаж/покупок за это окно.`
];
}
return `- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`;
return [
`- Якорь периода: дата покупки из вопроса/контекста использована для выбора налогового периода ${periodWindowLabel}; сам расчет ниже взят из книг продаж/покупок за это окно.`
];
}
function detectRankingLimit(userMessage, fallback = 20) {
const text = normalizeQuestionText(userMessage);
@@ -996,12 +1024,72 @@ function hasInventoryAccountPrefix(value, prefix) {
.replace(",", ".");
return normalized === prefix || normalized.startsWith(`${prefix}.`) || normalized.startsWith(prefix);
}
function isInventoryOnHandSnapshotRow(row) {
const registrator = String(row.registrator ?? "").trim().toLowerCase();
return /(?:остатки\s+на\s+дату|stock\s*on\s*hand|balance\s+as\s+of)/iu.test(registrator);
}
function isInventoryPurchaseMovement(row) {
return hasInventoryAccountPrefix(row.account_dt, "41.01");
return hasInventoryAccountPrefix(row.account_dt, "41.01") && !isInventoryOnHandSnapshotRow(row);
}
function isInventorySaleMovement(row) {
return hasInventoryAccountPrefix(row.account_kt, "41.01");
}
function inventoryAgingKeyPart(value) {
return normalizeEntityToken(value) ?? "";
}
function buildInventoryOnHandAgingScope(rows) {
const scope = {
itemKeys: new Set(),
itemOrgKeys: new Set(),
fullKeys: new Set(),
orgKeys: new Set()
};
for (const row of rows) {
if (!isInventoryOnHandSnapshotRow(row)) {
continue;
}
const quantity = extractInventoryQuantity(row);
if (quantity === null || quantity <= 0) {
continue;
}
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
if (!itemKey) {
continue;
}
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
scope.itemKeys.add(itemKey);
if (organizationKey) {
scope.orgKeys.add(organizationKey);
scope.itemOrgKeys.add(`${itemKey}|${organizationKey}`);
}
if (organizationKey || warehouseKey) {
scope.fullKeys.add(`${itemKey}|${warehouseKey}|${organizationKey}`);
}
}
return scope;
}
function inventoryPurchaseMatchesOnHandAgingScope(row, scope) {
if (scope.itemKeys.size === 0) {
return true;
}
const itemKey = inventoryAgingKeyPart(extractInventoryItemName(row));
if (!itemKey || !scope.itemKeys.has(itemKey)) {
return false;
}
const organizationKey = inventoryAgingKeyPart(extractInventoryOrganizationName(row));
const warehouseKey = inventoryAgingKeyPart(extractInventoryWarehouseName(row));
if (scope.fullKeys.has(`${itemKey}|${warehouseKey}|${organizationKey}`)) {
return true;
}
if (organizationKey && scope.itemOrgKeys.has(`${itemKey}|${organizationKey}`)) {
return true;
}
if (organizationKey && scope.orgKeys.size > 0) {
return false;
}
return true;
}
function looksLikeInventoryTraceDocumentToken(value) {
const normalized = String(value ?? "").trim();
if (!normalized) {
@@ -1162,7 +1250,11 @@ function formatCounterpartyItemFlowRows(rows, limit = 12) {
function buildInventoryAgingByItemAggregate(rows, asOfDate) {
const byItem = new Map();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
const onHandScope = buildInventoryOnHandAgingScope(rows);
for (const row of rows) {
if (!isInventoryPurchaseMovement(row) || !inventoryPurchaseMatchesOnHandAgingScope(row, onHandScope)) {
continue;
}
const item = extractInventoryItemName(row);
if (!item) {
continue;
@@ -1245,19 +1337,11 @@ function formatInventoryAgingRows(items, asOfDate, limit = 10) {
const parts = [
`${index + 1}. ${item.item}`,
`первая закупка: ${inventoryTraceDateLabel(item.firstPurchasePeriod)}`,
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`,
`документов: ${formatNumberWithDots(item.documentCount)}`,
`операций: ${formatNumberWithDots(item.operations)}`
`последняя закупка: ${inventoryTraceDateLabel(item.lastPurchasePeriod)}`
];
if (item.ageDays !== null) {
parts.push(`возраст следа на ${formatDateRu(asOfDate)}: ${formatNumberWithDots(item.ageDays)} дн.`);
}
if (item.warehouse) {
parts.push(`склад: ${item.warehouse}`);
}
if (item.organization) {
parts.push(`организация: ${item.organization}`);
}
if (item.counterparties.length > 0) {
parts.push(`поставщики: ${item.counterparties.slice(0, 3).join("; ")}`);
}
@@ -3060,15 +3144,27 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
const purchaseDateAnchorLine = buildVatPurchaseDateAnchorDisclosureLine(options, periodWindowLabel);
const purchaseDateAnchorLines = buildVatPurchaseDateAnchorDisclosureLines(options, periodWindowLabel);
const selectedPurchaseDate = normalizeIsoDateOnly(options.purchaseDateBridge?.selectedPurchaseDate);
const hasMultiplePurchaseDates = Boolean(options.purchaseDateBridge?.hasMultiplePurchaseDates ||
(normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) &&
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate) &&
normalizeIsoDateOnly(options.purchaseDateBridge?.firstPurchaseDate) !==
normalizeIsoDateOnly(options.purchaseDateBridge?.lastPurchaseDate)));
const purchaseBasisLabel = vatPurchaseDateBasisLabel(options.purchaseDateBridge?.basis, hasMultiplePurchaseDates);
const directVatLine = selectedPurchaseDate
? hasMultiplePurchaseDates
? `Коротко: если брать ${purchaseBasisLabel} ${formatDateRu(selectedPurchaseDate)}, подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
: `Коротко: по дате покупки ${formatDateRu(selectedPurchaseDate)} подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`
: `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`;
const lines = [
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
directVatLine,
"Расчет сделан по книгам продаж и покупок.",
"",
"Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
...(purchaseDateAnchorLine ? [purchaseDateAnchorLine] : []),
...purchaseDateAnchorLines,
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
`- Нетто НДС (книга продаж - книга покупок): ${formatConfirmedMoney(netVat)}.`
@@ -460,6 +460,23 @@ function resolveYearFromFilters(filters) {
function hasRelativeYearHint(text) {
return /(?:эт(?:от|ого)(?:\s+же)?\s+год|этого\s+же\s+года|того\s+же\s+года|this\s+year|same\s+year|that\s+year)/iu.test(String(text ?? ""));
}
function resolveMonthPeriodFromIsoDate(isoDate) {
const match = String(isoDate ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const year = Number(match[1]);
const month = Number(match[2]);
if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
return null;
}
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return {
period_from: `${match[1]}-${match[2]}-01`,
period_to: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`,
as_of_date: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, "0")}`
};
}
function resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext) {
if (!followupContext || !isInventoryRootFrameIntent(followupContext.root_intent)) {
return null;
@@ -629,6 +646,34 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
return (/(?:ндс|vat)/iu.test(normalized) &&
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
}
function hasInventoryPurchaseDateVatBridgeContinuationCue(text) {
const normalized = textWithRepairedVariant(String(text ?? ""))
.trim()
.toLowerCase()
.replace(/ё/g, "е");
if (!normalized) {
return false;
}
const mentionsPurchase = /(?:покупк|закупк|purchase)/iu.test(normalized);
const mentionsFirstOrLast = /(?:перв|последн|earliest|oldest|latest|last)/iu.test(normalized);
return mentionsPurchase && mentionsFirstOrLast;
}
function purchaseDateBridgeBasisFromMessage(userMessage) {
const normalized = textWithRepairedVariant(String(userMessage ?? ""))
.trim()
.toLowerCase()
.replace(/ё/g, "е");
if (!normalized) {
return null;
}
if (/(?:последн|latest|last)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
return "last_confirmed_purchase";
}
if (/(?:перв|earliest|oldest)/iu.test(normalized) && /(?:покупк|закупк|purchase)/iu.test(normalized)) {
return "first_confirmed_purchase";
}
return null;
}
function hasInventoryMarginRankingFollowupCue(text) {
const normalized = textWithRepairedVariant(String(text ?? ""))
.toLowerCase()
@@ -745,6 +790,31 @@ function isShortDebtRoleMirrorFollowup(intent, text, followupContext) {
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const merged = { ...current };
const reasons = [];
const clearInventoryAgingOrganizationAliasItem = (organizationHint) => {
if (intent !== "inventory_aging_by_purchase_date") {
return;
}
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
const agingItem = toNonEmptyString(merged.item);
const agingOrganization = toNonEmptyString(merged.organization) ?? organizationHint;
const normalizedAgingItem = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(agingItem ?? "");
const normalizedAgingOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(agingOrganization ?? "");
const itemLooksLikeOrganizationScope = Boolean(agingItem &&
agingOrganization &&
((0, assistantOrganizationMatcher_1.organizationsLikelySameEntity)(agingItem, agingOrganization) ||
(normalizedAgingItem &&
normalizedAgingOrganization &&
(normalizedAgingOrganization.includes(normalizedAgingItem) ||
normalizedAgingItem.includes(normalizedAgingOrganization)))));
if (agingItem && itemLooksLikeOrganizationScope) {
delete merged.item;
reasons.push("item_cleared_as_organization_scope_alias_for_stock_aging");
}
else if (agingItem && !explicitItemMention) {
delete merged.item;
reasons.push("item_cleared_for_stock_slice_aging");
}
};
if (!followupContext) {
if ((intent === "list_open_contracts" ||
intent === "open_contracts_confirmed_as_of_date" ||
@@ -760,6 +830,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
: "as_of_date_derived_from_period_for_open_contracts");
}
}
clearInventoryAgingOrganizationAliasItem(null);
return { filters: merged, reasons };
}
const previous = followupContext.previous_filters ?? {};
@@ -773,6 +844,12 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const previousAsOfDate = toNonEmptyString(previous.as_of_date);
const previousPeriodFrom = toNonEmptyString(previous.period_from);
const previousPeriodTo = toNonEmptyString(previous.period_to);
const previousPurchaseDateBridgeSelected = toNonEmptyString(previous.purchase_date_bridge_selected);
const previousPurchaseDateBridgeFirst = toNonEmptyString(previous.purchase_date_bridge_first);
const previousPurchaseDateBridgeLast = toNonEmptyString(previous.purchase_date_bridge_last);
const previousPurchaseDateBridgeBasis = toNonEmptyString(previous.purchase_date_bridge_basis);
const previousPurchaseDateBridgeHasMultiple = previous.purchase_date_bridge_has_multiple === true ||
String(previous.purchase_date_bridge_has_multiple ?? "").trim().toLowerCase() === "true";
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
const allTimeRequested = hasAllTimeHint(userMessage);
@@ -790,6 +867,53 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
merged.organization = previousOrganization;
reasons.push("organization_from_followup_context");
}
if (intent === "vat_liability_confirmed_for_tax_period" && previousPurchaseDateBridgeSelected) {
const requestedBridgeBasis = purchaseDateBridgeBasisFromMessage(userMessage);
const shouldCarryPurchaseDateBridge = !currentHasExplicitTemporalScope &&
(Boolean(requestedBridgeBasis) ||
hasInventoryPurchaseDateVatBridgeCue(userMessage) ||
hasInventoryPurchaseDateVatBridgeContinuationCue(userMessage));
if (!shouldCarryPurchaseDateBridge) {
delete merged.purchase_date_bridge_selected;
delete merged.purchase_date_bridge_first;
delete merged.purchase_date_bridge_last;
delete merged.purchase_date_bridge_basis;
delete merged.purchase_date_bridge_has_multiple;
reasons.push(currentHasExplicitTemporalScope
? "purchase_date_bridge_suppressed_by_explicit_temporal_scope"
: "purchase_date_bridge_not_reused_without_bridge_cue");
}
else {
const selectedBridgeDate = requestedBridgeBasis === "last_confirmed_purchase"
? previousPurchaseDateBridgeLast ?? previousPurchaseDateBridgeSelected
: requestedBridgeBasis === "first_confirmed_purchase"
? previousPurchaseDateBridgeFirst ?? previousPurchaseDateBridgeSelected
: previousPurchaseDateBridgeSelected;
merged.purchase_date_bridge_selected = selectedBridgeDate;
const selectedBridgeWindow = resolveMonthPeriodFromIsoDate(selectedBridgeDate);
if (selectedBridgeWindow) {
merged.period_from = selectedBridgeWindow.period_from;
merged.period_to = selectedBridgeWindow.period_to;
merged.as_of_date = selectedBridgeWindow.as_of_date;
reasons.push("period_from_purchase_date_bridge_followup_context");
reasons.push("period_to_purchase_date_bridge_followup_context");
}
if (previousPurchaseDateBridgeFirst) {
merged.purchase_date_bridge_first = previousPurchaseDateBridgeFirst;
}
if (previousPurchaseDateBridgeLast) {
merged.purchase_date_bridge_last = previousPurchaseDateBridgeLast;
}
const effectiveBridgeBasis = requestedBridgeBasis ?? previousPurchaseDateBridgeBasis;
if (effectiveBridgeBasis) {
merged.purchase_date_bridge_basis = effectiveBridgeBasis;
}
if (previousPurchaseDateBridgeHasMultiple) {
merged.purchase_date_bridge_has_multiple = true;
}
reasons.push("purchase_date_bridge_from_followup_context");
}
}
if (intent === "inventory_on_hand_as_of_date" &&
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
followupContext.target_intent === "inventory_on_hand_as_of_date" &&
@@ -1099,11 +1223,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
reasons.push("period_derived_from_inventory_root_frame_year");
}
if (intent === "inventory_aging_by_purchase_date") {
const explicitItemMention = /(?:^|[\s,.;:!?()\-\u2014])(?:товар(?:у|а|ом)?|позици(?:и|я|ю)|item|row|line)(?=$|[\s,.;:!?()\-\u2014])/iu.test(String(userMessage ?? ""));
if (toNonEmptyString(merged.item) && !explicitItemMention) {
delete merged.item;
reasons.push("item_cleared_for_stock_slice_aging");
}
clearInventoryAgingOrganizationAliasItem(previousOrganization);
}
if (!sameDateRequested &&
hasFollowupSignalForConfirmed &&
@@ -1317,7 +1437,12 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
return detectedIntent;
}
const normalizedMessage = String(userMessage ?? "");
const hasFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
const genericFollowupSignal = hasAddressFollowupContextSignal(normalizedMessage);
const previousFilters = followupContext.previous_filters ?? {};
const purchaseBridgeContinuationSignal = followupContext.previous_intent === "vat_liability_confirmed_for_tax_period" &&
Boolean(toNonEmptyString(previousFilters.purchase_date_bridge_selected)) &&
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
const hasFollowupSignal = genericFollowupSignal || purchaseBridgeContinuationSignal;
if (!hasFollowupSignal) {
return detectedIntent;
}
@@ -1326,7 +1451,6 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
if (!sourceIntent && !fallbackIntent) {
return detectedIntent;
}
const previousFilters = followupContext.previous_filters ?? {};
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
const previousContract = toNonEmptyString(previousFilters.contract);
@@ -1340,6 +1464,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
const samePeriodVatFollowup = isVatFollowup &&
hasSamePeriodHint(normalizedMessage) &&
Boolean(previousPeriodFrom || previousPeriodTo);
const previousPurchaseDateBridgeSelected = toNonEmptyString(previousFilters.purchase_date_bridge_selected);
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
const inventoryLineageActive = previousIsInventoryFamily ||
@@ -1361,7 +1486,11 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
sourceIntent === "open_items_by_counterparty_or_contract");
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
const vatPurchaseDateBridgeContinuation = sourceIntent === "vat_liability_confirmed_for_tax_period" &&
Boolean(previousPurchaseDateBridgeSelected) &&
hasInventoryPurchaseDateVatBridgeContinuationCue(normalizedMessage);
const inventoryPurchaseDateVatBridge = (inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage)) ||
vatPurchaseDateBridgeContinuation;
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
@@ -1382,6 +1511,7 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
if (inventoryPurchaseDateVatBridge &&
(detectedIntent.intent === "unknown" ||
detectedIntent.intent === sourceIntent ||
detectedIntent.intent === "account_balance_snapshot" ||
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
detectedIntent.intent === "vat_payable_forecast")) {
return {
@@ -57,6 +57,16 @@ function inventoryRequestedPartyMatches(requested, actualParties) {
function inventoryPartyListOrUnknown(parties) {
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
}
function normalizeInventoryReplyEntityToken(value) {
const normalized = String(value ?? "")
.trim()
.toLowerCase()
.replace(/ё/gu, "е")
.replace(/[^a-zа-я0-9]+/giu, " ")
.replace(/\s+/gu, " ")
.trim();
return normalized || null;
}
function sumInventoryRowAmount(rows) {
return rows.reduce((sum, row) => sum + (typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0), 0);
}
@@ -380,29 +390,47 @@ function composeInventoryReply(intent, rows, options, deps) {
}
if (intent === "inventory_aging_by_purchase_date") {
const asOfDate = deps.resolvePayablesAsOfDate(options);
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
const agingItems = deps.buildInventoryAgingByItemAggregate(rows, asOfDate);
const agingItemTokens = new Set(agingItems
.map((item) => normalizeInventoryReplyEntityToken(item.item))
.filter((item) => Boolean(item)));
const purchaseRows = rows.filter((row) => {
if (!deps.isInventoryPurchaseMovement(row)) {
return false;
}
if (agingItemTokens.size === 0) {
return true;
}
const item = deps.summarizeInventoryTraceRows([row]).item;
const itemToken = normalizeInventoryReplyEntityToken(item);
return Boolean(itemToken && agingItemTokens.has(itemToken));
});
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
const agingItems = deps.buildInventoryAgingByItemAggregate(purchaseRows, asOfDate);
const oldestPurchaseDate = agingItems[0]?.firstPurchasePeriod ?? summary.firstPeriod;
const oldestPurchaseAgeDays = agingItems[0]?.ageDays ?? null;
const organizationLabel = agingItems.find((item) => item.organization)?.organization ?? null;
const oldestAnswerPreview = agingItems
.slice(0, 3)
.map((item) => `${item.item} (${deps.inventoryTraceDateLabel(item.firstPurchasePeriod)})`)
.join("; ");
const directAnswerLine = agingItems.length > 0
? `К самым старым закупкам в текущем подтвержденном срезе относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
: "По доступному закупочному следу позиции со старыми закупками не материализованы.";
? `Среди фактических положительных остатков есть давно закупавшиеся позиции: ${oldestAnswerPreview}.`
: "В фактическом положительном остатке не найдено позиций с подтвержденным старым закупочным следом.";
const lines = [directAnswerLine];
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
const summaryLines = [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
];
if (organizationLabel) {
summaryLines.splice(1, 0, `Организация: ${organizationLabel}.`);
}
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", summaryLines);
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
"Берем только позиции с положительным остатком на дату среза; без партионного учета это возраст закупочного следа по номенклатуре, а не доказанный возраст конкретной партии."
]);
if (oldestPurchaseAgeDays !== null) {
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
@@ -411,11 +439,11 @@ function composeInventoryReply(intent, rows, options, deps) {
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
}
if (agingItems.length > 0) {
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 5));
}
else {
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", [
"- В доступных данных не найдено закупочных движений для выбранного среза."
"- В доступных данных не найдено закупочных движений по позициям, которые есть в положительном остатке на дату среза."
]);
}
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));