ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Протянуть exact capability складских остатков товаров на дату

This commit is contained in:
2026-04-13 20:40:24 +03:00
parent c2ac0c610b
commit 2b48229312
27 changed files with 1781 additions and 95 deletions
@@ -26,6 +26,7 @@ export interface AddressCapabilityRouteDecision {
const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
"account_balance_snapshot",
"documents_forming_balance",
"inventory_on_hand_as_of_date",
"open_contracts_confirmed_as_of_date",
"payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date",
@@ -74,6 +75,9 @@ function defaultCapabilityId(intent: AddressIntent): string {
if (intent === "vat_liability_confirmed_for_tax_period") {
return "confirmed_vat_liability_for_tax_period";
}
if (intent === "inventory_on_hand_as_of_date") {
return "confirmed_inventory_on_hand_as_of_date";
}
if (intent === "list_payables_counterparties") {
return "payables_candidates_list";
}
@@ -134,6 +138,14 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
};
}
if (intent === "inventory_on_hand_as_of_date") {
return {
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
reason: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1
? "inventory_on_hand_route_enabled"
: "inventory_on_hand_route_disabled_by_flag"
};
}
if (intent === "list_payables_counterparties") {
return {
enabled: FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
@@ -927,6 +927,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
if (intent === "account_balance_snapshot" || intent === "documents_forming_balance") {
return ["account", "as_of_date"];
}
if (intent === "inventory_on_hand_as_of_date") {
return ["as_of_date"];
}
if (intent === "payables_confirmed_as_of_date") {
return ["as_of_date"];
}
@@ -957,6 +960,7 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
return (
intent === "inventory_on_hand_as_of_date" ||
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "open_contracts_confirmed_as_of_date" ||
@@ -1163,6 +1167,7 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
if (
(intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "inventory_on_hand_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date") &&
@@ -1542,6 +1542,22 @@ function hasAccountNumberAnchor(text: string): boolean {
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
}
function hasInventoryOnHandSignal(text: string): boolean {
const hasStockLexeme =
/(?:склад(?:е|у|ом|ы|ов)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text);
if (!hasStockLexeme) {
return false;
}
const hasGoodsLexeme =
/(?:товар(?:ы|ов|ом|а|ные)?|номенклатур|материал(?:ы|ов|а|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text);
const hasBalanceLexeme =
/(?:леж(?:ит|ат)|есть|числ(?:ит(?:ся|сь)|ятся)|остат(?:ок|ки)|срез|на\s+дат|по\s+состоянию|на\s+конец|today|now|current|as\s+of)/iu.test(
text
);
const hasRequestCue = /(?:покажи|показать|выведи|дай|какие|что|какой|сколько|show|list|which|what)/iu.test(text);
return (hasGoodsLexeme || hasBalanceLexeme) && (hasRequestCue || hasBalanceLexeme);
}
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
const text = String(userMessage ?? "").trim().toLowerCase();
@@ -1659,6 +1675,14 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (hasInventoryOnHandSignal(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "high",
reasons: ["inventory_on_hand_signal_detected"]
};
}
if (hasOpenContractsListSignal(text)) {
return {
intent: "open_contracts_confirmed_as_of_date",
@@ -100,6 +100,14 @@ const ADDRESS_ENTITY_TOKENS = [
"поступлени",
"списан",
"списани",
"склад",
"складе",
"складу",
"товар",
"товары",
"товарн",
"номенклат",
"материал",
"долг",
"должен",
"должны",
@@ -55,6 +55,10 @@ interface NormalizedAddressRow {
account_kt: string | null;
amount: number | null;
analytics: string[];
quantity?: number | null;
item?: string | null;
warehouse?: string | null;
organization?: string | null;
}
interface AddressTryHandleOptions {
@@ -321,6 +325,26 @@ function detectVatMetadataObjectType(fullName: string): VatMetadataObject["objec
return null;
}
function firstNonEmptyString(...values: unknown[]): string | null {
for (const value of values) {
const normalized = valueAsString(value).trim();
if (normalized) {
return normalized;
}
}
return null;
}
function firstFiniteNumber(...values: unknown[]): number | null {
for (const value of values) {
const parsed = parseFiniteNumber(value);
if (parsed !== null) {
return parsed;
}
}
return null;
}
function extractVatMetadataObjects(rows: Array<Record<string, unknown>>): VatMetadataObject[] {
const out: VatMetadataObject[] = [];
const seen = new Set<string>();
@@ -1086,6 +1110,17 @@ function collectAnalyticsStrings(row: Record<string, unknown>): string[] {
"Контрагент",
"Contract",
"Договор",
"Item",
"item",
"Номенклатура",
"НоменклатураПредставление",
"Warehouse",
"warehouse",
"Склад",
"СкладПредставление",
"Quantity",
"quantity",
"Количество",
"Organization",
"Организация",
"ОрганизацияПредставление",
@@ -1108,6 +1143,10 @@ function collectAnalyticsStrings(row: Record<string, unknown>): string[] {
lowerKey.includes("субконто") ||
lowerKey.includes("контраг") ||
lowerKey.includes("договор") ||
lowerKey.includes("warehouse") ||
lowerKey.includes("склад") ||
lowerKey.includes("item") ||
lowerKey.includes("номенклат") ||
lowerKey.includes("organization") ||
lowerKey.includes("организац")
) {
@@ -1132,6 +1171,16 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
const accountDt = valueAsString(row.СчетДт ?? row.account_dt ?? row.AccountDt).trim() || null;
const accountKt = valueAsString(row.СчетКт ?? row.account_kt ?? row.AccountKt).trim() || null;
const amount = parseFiniteNumber(row.Сумма ?? row.amount ?? row.Amount);
const quantity = firstFiniteNumber(row.Количество, row.quantity, row.Quantity);
const item = firstNonEmptyString(row.Номенклатура, row.Item, row.item, row.НоменклатураПредставление);
const warehouse = firstNonEmptyString(row.Склад, row.Warehouse, row.warehouse, row.СкладПредставление);
const organization = firstNonEmptyString(
row.Организация,
row.Organization,
row.organization,
row.organization_name,
row.ОрганизацияПредставление
);
const analytics = collectAnalyticsStrings(row);
return {
@@ -1140,7 +1189,11 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
account_dt: accountDt,
account_kt: accountKt,
amount,
analytics
analytics,
quantity,
item,
warehouse,
organization
};
})
.filter((item) => Boolean(item.period || item.registrator));
@@ -1313,6 +1366,7 @@ function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
return (
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "inventory_on_hand_as_of_date" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
@@ -2164,6 +2218,8 @@ function buildLimitedOffers(input: {
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
} else if (input.intent === "receivables_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
} else if (input.intent === "inventory_on_hand_as_of_date") {
offers.push("показать подтвержденный срез товаров на складах на дату по остатку счета 41.01");
} else if (input.intent === "open_contracts_confirmed_as_of_date") {
offers.push("показать подтвержденный реестр договоров с открытыми взаиморасчетами на дату по 60/62/76");
} else if (input.intent === "vat_payable_confirmed_as_of_date") {
@@ -2223,6 +2279,7 @@ function buildLimitedIntentSignalLine(input: {
open_contracts_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный список договоров с открытыми взаиморасчетами на дату.",
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
inventory_on_hand_as_of_date: "Сигнал запроса: нужен подтвержденный срез товаров на складе на дату.",
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
payables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез обязательств к оплате на дату.",
vat_payable_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату.",
@@ -2425,7 +2482,9 @@ function buildLimitedExecutionResult(input: {
resultSemantics.result_mode
);
const exactLimitedReason =
input.intent.intent === "payables_confirmed_as_of_date"
input.intent.intent === "inventory_on_hand_as_of_date"
? "exact_inventory_mode_limited_response"
: input.intent.intent === "payables_confirmed_as_of_date"
? "exact_payables_mode_limited_response"
: input.intent.intent === "receivables_confirmed_as_of_date"
? "exact_receivables_mode_limited_response"
@@ -2553,6 +2612,8 @@ export class AddressQueryService {
intent.intent === "receivables_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
const confirmedBalanceVatPayableIntent =
intent.intent === "vat_payable_confirmed_as_of_date" && requestedResultMode === "confirmed_balance";
const confirmedBalanceInventoryIntent =
intent.intent === "inventory_on_hand_as_of_date" && requestedResultMode === "confirmed_balance";
const payablesConfirmedExecution =
confirmedBalancePayablesIntent
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
@@ -2563,7 +2624,11 @@ export class AddressQueryService {
const vatPayableConfirmedExecution = confirmedBalanceVatPayableIntent
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
: null;
const inventoryConfirmedExecution = confirmedBalanceInventoryIntent
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
: null;
const executionFilters =
inventoryConfirmedExecution?.executionFilters ??
payablesConfirmedExecution?.executionFilters ??
receivablesConfirmedExecution?.executionFilters ??
vatPayableConfirmedExecution?.executionFilters ??
@@ -2601,6 +2666,17 @@ export class AddressQueryService {
baseReasons.push("as_of_date_derived_for_confirmed_vat_payable");
}
}
if (
inventoryConfirmedExecution?.asOfDerived &&
!(typeof filters.extracted_filters.as_of_date === "string" && filters.extracted_filters.as_of_date.trim().length > 0)
) {
if (!filters.warnings.includes("as_of_date_derived_for_inventory_on_hand")) {
filters.warnings.push("as_of_date_derived_for_inventory_on_hand");
}
if (!baseReasons.includes("as_of_date_derived_for_inventory_on_hand")) {
baseReasons.push("as_of_date_derived_for_inventory_on_hand");
}
}
const capabilityDecision = resolveAddressCapabilityRouteDecision(intent.intent);
const capabilityAudit = buildCapabilityAudit(intent.intent);
const shadowRouteAudit = buildShadowRouteAudit({
@@ -2686,6 +2762,12 @@ export class AddressQueryService {
) {
baseReasons.push("confirmed_balance_exact_receivables_intent");
}
if (
intent.intent === "inventory_on_hand_as_of_date" &&
!baseReasons.includes("confirmed_balance_exact_inventory_intent")
) {
baseReasons.push("confirmed_balance_exact_inventory_intent");
}
if (
intent.intent === "vat_payable_confirmed_as_of_date" &&
!baseReasons.includes("confirmed_balance_exact_vat_payable_intent")
@@ -137,6 +137,26 @@ const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
Сумма __ORDER_DIRECTION__
`;
const INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"Остатки на дату" КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
"" КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Номенклатура,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК Склад,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
Остатки.КоличествоРазвернутыйОстатокДт КАК Количество
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
ГДЕ
Остатки.КоличествоРазвернутыйОстатокДт > 0
И (__INVENTORY_ACCOUNTS_MATCH__)
УПОРЯДОЧИТЬ ПО
Количество __ORDER_DIRECTION__
`;
const BANK_DOCS_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
БанкСписание.Дата КАК Период,
@@ -676,6 +696,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
account_scope_mode: "preferred",
query_template: "vat_liability_confirmed_tax_period_profile"
},
{
recipe_id: "address_inventory_on_hand_as_of_date_v1",
intent: "inventory_on_hand_as_of_date",
purpose: "Build confirmed stock-on-hand snapshot from balances on goods-on-warehouse account 41.01",
required_filters: ["as_of_date"],
optional_filters: ["period_from", "period_to", "organization", "limit", "sort"],
default_limit: 300,
account_scope: ["41.01"],
account_scope_mode: "strict",
query_template: "inventory_on_hand_as_of_balance_profile"
},
{
recipe_id: "address_open_contracts_confirmed_as_of_date_v1",
intent: "open_contracts_confirmed_as_of_date",
@@ -1035,6 +1066,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
intent === "contract_usage_and_value" ||
intent === "vat_payable_forecast" ||
intent === "vat_liability_confirmed_for_tax_period" ||
intent === "inventory_on_hand_as_of_date" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "list_contracts_by_counterparty" ||
intent === "list_documents_by_counterparty" ||
@@ -1208,6 +1240,25 @@ export function buildAddressRecipePlan(
)
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "inventory_on_hand_as_of_balance_profile"
? (() => {
const asOfExpr =
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__INVENTORY_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "contracts_by_counterparty_profile"
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
@@ -12,6 +12,10 @@ export interface ComposeStageRow {
account_kt: string | null;
amount: number | null;
analytics: string[];
quantity?: number | null;
item?: string | null;
warehouse?: string | null;
organization?: string | null;
}
export interface VatDirectSourceProbeItem {
@@ -750,6 +754,162 @@ function extractCounterpartyName(row: ComposeStageRow): string | null {
return null;
}
function extractInventoryItemName(row: ComposeStageRow): string | null {
const direct = String(row.item ?? "").trim();
if (direct) {
return direct;
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
if (!normalized) {
continue;
}
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
continue;
}
if (/(?:склад|warehouse|ооо|ао|пао|зао|ип|организац)/iu.test(normalized)) {
continue;
}
return normalized;
}
return null;
}
function extractInventoryWarehouseName(row: ComposeStageRow): string | null {
const direct = String(row.warehouse ?? "").trim();
if (direct) {
return direct;
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
if (/(?:склад|warehouse)/iu.test(normalized)) {
return normalized;
}
}
return null;
}
function extractInventoryOrganizationName(row: ComposeStageRow): string | null {
const direct = String(row.organization ?? "").trim();
if (direct) {
return direct;
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
if (!normalized) {
continue;
}
if (/(?:(?:^|[\s"'«»„“()\\\/])(?:ооо|ао|пао|зао|оао|ип|гку)(?=$|[\s"'«»„“()\\\/.,;:]))|организац|комитет|департамент|министерств|служб|управлени|казенн|администрац/iu.test(normalized)) {
return normalized;
}
}
return null;
}
function extractInventoryQuantity(row: ComposeStageRow): number | null {
return typeof row.quantity === "number" && Number.isFinite(row.quantity) ? row.quantity : null;
}
interface InventoryOnHandAggregate {
item: string;
warehouse: string | null;
organization: string | null;
quantity: number;
amount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
sourceRefs: string[];
}
function buildInventoryOnHandAggregate(rows: ComposeStageRow[], asOfDate: string): InventoryOnHandAggregate[] {
const byPosition = new Map<
string,
{
item: string;
warehouse: string | null;
organization: string | null;
quantity: number;
amount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
sourceRefs: Set<string>;
}
>();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const item = extractInventoryItemName(row);
if (!item) {
continue;
}
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const quantity = extractInventoryQuantity(row);
if (quantity === null || quantity <= 0) {
continue;
}
const warehouse = extractInventoryWarehouseName(row);
const organization = extractInventoryOrganizationName(row);
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0;
const key = [normalizeEntityToken(item), normalizeEntityToken(warehouse), normalizeEntityToken(organization)].join("|");
const registrator = String(row.registrator ?? "").trim();
const current = byPosition.get(key);
if (!current) {
byPosition.set(key, {
item,
warehouse,
organization,
quantity,
amount,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
sourceRefs: new Set(registrator && registrator !== "Остатки на дату" ? [registrator] : [])
});
continue;
}
current.quantity += quantity;
current.amount += amount;
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if (registrator && registrator !== "Остатки на дату") {
current.sourceRefs.add(registrator);
}
}
return Array.from(byPosition.values())
.map((item) => ({
item: item.item,
warehouse: item.warehouse,
organization: item.organization,
quantity: item.quantity,
amount: item.amount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
}))
.filter((item) => item.quantity > 0)
.sort((left, right) => {
if (right.quantity !== left.quantity) {
return right.quantity - left.quantity;
}
if (right.amount !== left.amount) {
return right.amount - left.amount;
}
return left.item.localeCompare(right.item, "ru");
});
}
interface CounterpartyRiskAggregate {
name: string;
totalAmount: number;
@@ -3502,6 +3662,63 @@ export function composeFactualReply(
};
}
if (intent === "inventory_on_hand_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const positions = buildInventoryOnHandAggregate(rows, asOfDate);
const uniqueItems = uniqueStrings(positions.map((item) => item.item));
const uniqueWarehouses = uniqueStrings(
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
);
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
const lines: string[] = [
`Собран подтвержденный срез товаров на складах на ${formatDateRu(asOfDate)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный список товарных остатков на дату.",
"",
"Блок 2. Что учтено",
`- Дата среза: ${formatDateRu(asOfDate)}.`,
"- Контур: остатки по счету 41.01 «Товары на складах».",
"- Базовая единица детализации: одна строка = товар, склад и организация на дату.",
"",
"Блок 3. Сводка",
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Позиции с ненулевым остатком: ${formatNumberWithDots(positions.length)}.`,
`- Уникальных товаров: ${formatNumberWithDots(uniqueItems.length)}.`,
`- Уникальных складов: ${formatNumberWithDots(uniqueWarehouses.length)}.`,
`- Суммарное количество: ${formatNumberWithDots(totalQuantity, 3)}.`,
`- Суммарная стоимость: ${formatMoneyRub(totalAmount)}.`,
"",
"Блок 4. Подтвержденные позиции"
];
if (positions.length > 0) {
lines.push(
...positions.slice(0, 20).map((item, index) => {
const warehouseLabel = item.warehouse ?? "склад не определен";
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${formatNumberWithDots(item.quantity, 3)} | стоимость: ${formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
})
);
} else {
lines.push("- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
}
return {
responseType: positions.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: positions.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
if (intent === "open_contracts_confirmed_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
@@ -367,13 +367,22 @@ function mergeFollowupFilters(
const merged: AddressFilterSet = { ...current };
const reasons: string[] = [];
if (!followupContext) {
if ((intent === "list_open_contracts" || intent === "open_contracts_confirmed_as_of_date") && !toNonEmptyString(merged.as_of_date)) {
if (
(intent === "list_open_contracts" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "inventory_on_hand_as_of_date") &&
!toNonEmptyString(merged.as_of_date)
) {
const periodToForOpenContracts = toNonEmptyString(merged.period_to);
const periodFromForOpenContracts = toNonEmptyString(merged.period_from);
const derivedAsOfDate = periodToForOpenContracts ?? periodFromForOpenContracts;
if (derivedAsOfDate) {
merged.as_of_date = derivedAsOfDate;
reasons.push("as_of_date_derived_from_period_for_open_contracts");
reasons.push(
intent === "inventory_on_hand_as_of_date"
? "as_of_date_derived_from_period_for_inventory"
: "as_of_date_derived_from_period_for_open_contracts"
);
}
}
return { filters: merged, reasons };
@@ -480,6 +489,7 @@ function mergeFollowupFilters(
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "inventory_on_hand_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date"
@@ -561,6 +571,7 @@ function mergeFollowupFilters(
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "inventory_on_hand_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date";
@@ -598,13 +609,22 @@ function mergeFollowupFilters(
reasons.push("period_from_followup_context");
}
if ((intent === "list_open_contracts" || intent === "open_contracts_confirmed_as_of_date") && !toNonEmptyString(merged.as_of_date)) {
if (
(intent === "list_open_contracts" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "inventory_on_hand_as_of_date") &&
!toNonEmptyString(merged.as_of_date)
) {
const periodToForOpenContracts = toNonEmptyString(merged.period_to);
const periodFromForOpenContracts = toNonEmptyString(merged.period_from);
const derivedAsOfDate = periodToForOpenContracts ?? periodFromForOpenContracts;
if (derivedAsOfDate) {
merged.as_of_date = derivedAsOfDate;
reasons.push("as_of_date_derived_from_period_for_open_contracts");
reasons.push(
intent === "inventory_on_hand_as_of_date"
? "as_of_date_derived_from_period_for_inventory"
: "as_of_date_derived_from_period_for_open_contracts"
);
}
}
@@ -615,6 +635,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
const requiredByIntent: Record<string, Array<keyof AddressFilterSet>> = {
account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"],
inventory_on_hand_as_of_date: ["as_of_date"],
open_contracts_confirmed_as_of_date: ["as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
@@ -1024,7 +1024,7 @@ function hasStandaloneAddressTopicSignal(text) {
if (!hasRequestCue) {
return false;
}
const hasBusinessObject = /(?:договор|контракт|контрагент|поставщик|покупател|клиент|документ|платеж|оплат|сальдо|остатк|сч[её]т|оборот|выруч|доход|прибыл|ндс|дебитор|кредитор|организац|компан|контор|contract|counterparty|supplier|customer|document|payment|turnover|revenue|profit|balance|account|vat)/iu.test(normalized);
const hasBusinessObject = /(?:договор|контракт|контрагент|поставщик|покупател|клиент|документ|платеж|оплат|сальдо|остатк|сч[её]т|оборот|выруч|доход|прибыл|ндс|дебитор|кредитор|организац|компан|контор|склад|товар|номенклат|материал|contract|counterparty|supplier|customer|document|payment|turnover|revenue|profit|balance|account|vat|warehouse|inventory|stock|item)/iu.test(normalized);
if (!hasBusinessObject) {
return false;
}
@@ -3806,6 +3806,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
"open_items_by_counterparty_or_contract",
"list_payables_counterparties",
"list_receivables_counterparties",
"inventory_on_hand_as_of_date",
"payables_confirmed_as_of_date",
"receivables_confirmed_as_of_date",
"list_documents_by_contract",
@@ -4195,7 +4196,7 @@ export function resolveAssistantOrchestrationDecision(input) {
}
function hasStrongDataIntentSignal(text) {
const lower = String(text ?? "").toLowerCase();
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм)/i.test(lower);
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм)/i.test(lower);
}
function hasDataRetrievalRequestSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
@@ -4203,12 +4204,12 @@ function hasDataRetrievalRequestSignal(text) {
return false;
}
const hasBroadInterrogative = /(?:\u0433\u0434\u0435|\u0432\s+\u043a\u0430\u043a\u0438\u0445|\u043f\u043e\s+\u043a\u0430\u043a\u0438\u043c|\u043f\u043e\s+\u043a\u043e\u043c\u0443|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u043e\u0439|\u043a\u0442\u043e|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|where|which|who|how\s+many)/iu.test(lower);
const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover)/iu.test(lower);
const hasBroadBusinessObject = /(?:\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0433\u043e\u0434|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b|advance|prepay|shipment|receivab|payab|counterparty|contract|document|account|balance|turnover|warehouse|inventory|stock|item)/iu.test(lower);
if (hasBroadInterrogative && hasBroadBusinessObject) {
return true;
}
const hasRussianRetrievalAction = /(?:^|\s)(?:\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|\u043d\u0430\u0439\u0434\u0438|\u0432\u044b\u0432\u0435\u0434\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u043a\u0440\u043e\u0439|\u0441\u043f\u0438\u0441\u043e\u043a|\u043f\u0440\u043e\u0432\u0435\u0440\u044c|\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c)(?:$|[\s,.!?;:])/iu.test(lower);
const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433)/iu.test(lower);
const hasRussianRetrievalObject = /(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0441\u0447(?:\u0435|\u0451)\u0442|\u043e\u0441\u0442\u0430\u0442|\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0431\u043e\u0440\u043e\u0442|\u043f\u043b\u0430\u0442(?:\u0435|\u0451)\u0436|\u043e\u043f\u0435\u0440\u0430\u0446|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043a\u043b\u0438\u0435\u043d\u0442|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043c\u0435\u0441\u044f\u0446|\u0430\u0432\u0430\u043d\u0441|\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442|\u043e\u0442\u0433\u0440\u0443\u0437|\u0437\u0430\u0434\u043e\u043b\u0436|\u0434\u043e\u043b\u0433|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442|\u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b)/iu.test(lower);
if (hasRussianRetrievalAction && hasRussianRetrievalObject) {
return true;
}
@@ -4217,7 +4218,7 @@ function hasDataRetrievalRequestSignal(text) {
if (!hasExplicitRetrievalAction && !hasInterrogativeRetrievalAction) {
return false;
}
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
const hasRetrievalObject = /(1с|база|док|документ|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|bank|counterparty|contract|document|account|balance|ledger|posting|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм|возраст|дата\s+регистрац|регистрац|основан)/i.test(lower);
if (!hasRetrievalObject) {
return false;
}