ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Протянуть exact capability складских остатков товаров на дату
This commit is contained in:
parent
c2ac0c610b
commit
2b48229312
|
|
@ -20,6 +20,12 @@
|
|||
"capability_layer": "compute",
|
||||
"capability_route_mode": "exact"
|
||||
},
|
||||
{
|
||||
"intent": "inventory_on_hand_as_of_date",
|
||||
"capability_id": "confirmed_inventory_on_hand_as_of_date",
|
||||
"capability_layer": "compute",
|
||||
"capability_route_mode": "exact"
|
||||
},
|
||||
{
|
||||
"intent": "list_payables_counterparties",
|
||||
"capability_id": "payables_candidates_list",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@
|
|||
"expected_requested_result_modes": ["confirmed_balance"],
|
||||
"expected_result_modes": ["confirmed_balance"]
|
||||
},
|
||||
{
|
||||
"intent": "inventory_on_hand_as_of_date",
|
||||
"expected_selected_recipes": ["address_inventory_on_hand_as_of_date_v1"],
|
||||
"expected_requested_result_modes": ["confirmed_balance"],
|
||||
"expected_result_modes": ["confirmed_balance"]
|
||||
},
|
||||
{
|
||||
"intent": "vat_payable_forecast",
|
||||
"expected_selected_recipes": ["address_vat_payable_forecast_v1"]
|
||||
|
|
|
|||
|
|
@ -163,6 +163,47 @@
|
|||
"Счет 50, 51, 52, 55"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group_code": "inventory",
|
||||
"group_title": "Товары и складские остатки",
|
||||
"description": "Подтвержденные срезы товарных остатков по складам и организациям на дату.",
|
||||
"risk_level": "medium",
|
||||
"maturity_status": "production_ready",
|
||||
"supported_operations": [
|
||||
"inventory_on_hand_as_of_date",
|
||||
"inventory_positions_by_warehouse"
|
||||
],
|
||||
"unsupported_operations": [
|
||||
"post_goods_issue",
|
||||
"revalue_inventory"
|
||||
],
|
||||
"required_entities": [
|
||||
"as_of_date"
|
||||
],
|
||||
"optional_entities": [
|
||||
"organization",
|
||||
"warehouse",
|
||||
"item_scope"
|
||||
],
|
||||
"typical_queries": [
|
||||
"Какие товары сейчас лежат на складе?",
|
||||
"Покажи остатки товаров на дату.",
|
||||
"Какие позиции с ненулевым остатком были на складе?"
|
||||
],
|
||||
"related_routes": [
|
||||
"address_inventory_on_hand_as_of_date_v1"
|
||||
],
|
||||
"safe_alternatives": [
|
||||
"Уточнить дату среза",
|
||||
"Ограничить организацию или склад"
|
||||
],
|
||||
"one_c_hints": [
|
||||
"Счет 41.01",
|
||||
"РегистрБухгалтерии.Хозрасчетный.Остатки",
|
||||
"Справочник.Склады",
|
||||
"Справочник.Номенклатура"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group_code": "capability_boundaries",
|
||||
"group_title": "Ограничения",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const config_1 = require("../config");
|
|||
const COMPUTE_EXACT_INTENTS = new Set([
|
||||
"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",
|
||||
"vat_payable_confirmed_as_of_date",
|
||||
|
|
@ -41,12 +43,18 @@ function defaultCapabilityId(intent) {
|
|||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
return "confirmed_receivables_as_of_date";
|
||||
}
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
return "confirmed_open_contracts_as_of_date";
|
||||
}
|
||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||
return "confirmed_vat_payable_as_of_date";
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
|
@ -82,6 +90,14 @@ function resolveCapabilityEnabled(intent) {
|
|||
: "receivables_confirmed_route_disabled_by_flag"
|
||||
};
|
||||
}
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
return {
|
||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||
reason: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1
|
||||
? "open_contracts_confirmed_route_enabled"
|
||||
: "open_contracts_confirmed_route_disabled_by_flag"
|
||||
};
|
||||
}
|
||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||
return {
|
||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||
|
|
@ -98,6 +114,14 @@ function resolveCapabilityEnabled(intent) {
|
|||
: "vat_liability_confirmed_tax_period_route_disabled_by_flag"
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
return {
|
||||
enabled: config_1.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||
reason: config_1.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: config_1.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1,
|
||||
|
|
|
|||
|
|
@ -816,12 +816,18 @@ function requiredFiltersByIntent(intent) {
|
|||
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"];
|
||||
}
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
|
|
@ -839,8 +845,10 @@ function requiredFiltersByIntent(intent) {
|
|||
return [];
|
||||
}
|
||||
function usesAsOfPrimaryWindow(intent) {
|
||||
return (intent === "open_items_by_counterparty_or_contract" ||
|
||||
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" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_confirmed_as_of_date");
|
||||
|
|
@ -861,8 +869,10 @@ function extractAddressFilters(userMessage, intent) {
|
|||
sort: "period_desc"
|
||||
};
|
||||
if (!isManagementProfileIntent) {
|
||||
if (intent !== "open_contracts_confirmed_as_of_date") {
|
||||
filters.limit = 20;
|
||||
}
|
||||
}
|
||||
const warnings = [];
|
||||
const explicitAsOfDate = extractAsOfDate(text);
|
||||
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
||||
|
|
@ -1016,6 +1026,7 @@ function extractAddressFilters(userMessage, intent) {
|
|||
// - else default to today.
|
||||
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") &&
|
||||
|
|
|
|||
|
|
@ -390,8 +390,10 @@ function hasFlexibleReceivablesDebtSignal(text) {
|
|||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return (/(?:кто(?:\s+\S+){0,4}\s+нам(?:\s+\S+){0,4}\s+долж)/iu.test(normalized) ||
|
||||
/(?:нам(?:\s+\S+){0,4}\s+кто(?:\s+\S+){0,4}\s+долж)/iu.test(normalized));
|
||||
const hasFlexibleWhoOwesUs = /(?:\u043a\u0442\u043e(?:\s+\S+){0,4}\s+\u043d\u0430\u043c(?:\s+\S+){0,4}\s+\u0434\u043e\u043b\u0436)/iu.test(normalized) ||
|
||||
/(?:\u043d\u0430\u043c(?:\s+\S+){0,4}\s+\u043a\u0442\u043e(?:\s+\S+){0,4}\s+\u0434\u043e\u043b\u0436)/iu.test(normalized);
|
||||
const hasTorchatToUsSignal = /(?:\u043d\u0430\u043c(?:\s+\S+){0,3}\s+\u0442\u043e\u0440\u0447(?:\u0430\u0442|\u0438\u0442)|\u0442\u043e\u0440\u0447(?:\u0430\u0442|\u0438\u0442)(?:\s+\S+){0,3}\s+\u043d\u0430\u043c)/iu.test(normalized);
|
||||
return hasFlexibleWhoOwesUs || hasTorchatToUsSignal;
|
||||
}
|
||||
function hasFlexiblePayablesDebtSignal(text) {
|
||||
const normalized = String(text ?? "");
|
||||
|
|
@ -1300,6 +1302,16 @@ function hasGenericAddressLookupSignal(text) {
|
|||
function hasAccountNumberAnchor(text) {
|
||||
return /(?:account|сч[её]т|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
|
||||
}
|
||||
function hasInventoryOnHandSignal(text) {
|
||||
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);
|
||||
}
|
||||
function resolveAddressIntent(userMessage) {
|
||||
const text = String(userMessage ?? "").trim().toLowerCase();
|
||||
if (hasVatLiabilityConfirmedTaxPeriodSignal(text)) {
|
||||
|
|
@ -1402,9 +1414,16 @@ function resolveAddressIntent(userMessage) {
|
|||
reasons: ["documents_by_account_drilldown_signal_detected"]
|
||||
};
|
||||
}
|
||||
if (hasInventoryOnHandSignal(text)) {
|
||||
return {
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
confidence: "high",
|
||||
reasons: ["inventory_on_hand_signal_detected"]
|
||||
};
|
||||
}
|
||||
if (hasOpenContractsListSignal(text)) {
|
||||
return {
|
||||
intent: "list_open_contracts",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
confidence: "medium",
|
||||
reasons: ["open_contract_signal_detected"]
|
||||
};
|
||||
|
|
@ -1546,7 +1565,7 @@ function resolveAddressIntent(userMessage) {
|
|||
}
|
||||
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("контракт") || text.includes("contract"))) {
|
||||
return {
|
||||
intent: "list_open_contracts",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
confidence: "medium",
|
||||
reasons: ["open_contract_signal_detected"]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -100,6 +100,14 @@ const ADDRESS_ENTITY_TOKENS = [
|
|||
"поступлени",
|
||||
"списан",
|
||||
"списани",
|
||||
"склад",
|
||||
"складе",
|
||||
"складу",
|
||||
"товар",
|
||||
"товары",
|
||||
"товарн",
|
||||
"номенклат",
|
||||
"материал",
|
||||
"долг",
|
||||
"должен",
|
||||
"должны",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,24 @@ function detectVatMetadataObjectType(fullName) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function firstNonEmptyString(...values) {
|
||||
for (const value of values) {
|
||||
const normalized = valueAsString(value).trim();
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function firstFiniteNumber(...values) {
|
||||
for (const value of values) {
|
||||
const parsed = parseFiniteNumber(value);
|
||||
if (parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function extractVatMetadataObjects(rows) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
|
|
@ -780,6 +798,7 @@ function shouldAttemptCounterpartyCatalogResolution(intent, filters) {
|
|||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "list_payables_counterparties" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
|
|
@ -893,6 +912,17 @@ function collectAnalyticsStrings(row) {
|
|||
"Контрагент",
|
||||
"Contract",
|
||||
"Договор",
|
||||
"Item",
|
||||
"item",
|
||||
"Номенклатура",
|
||||
"НоменклатураПредставление",
|
||||
"Warehouse",
|
||||
"warehouse",
|
||||
"Склад",
|
||||
"СкладПредставление",
|
||||
"Quantity",
|
||||
"quantity",
|
||||
"Количество",
|
||||
"Organization",
|
||||
"Организация",
|
||||
"ОрганизацияПредставление",
|
||||
|
|
@ -912,6 +942,10 @@ function collectAnalyticsStrings(row) {
|
|||
lowerKey.includes("субконто") ||
|
||||
lowerKey.includes("контраг") ||
|
||||
lowerKey.includes("договор") ||
|
||||
lowerKey.includes("warehouse") ||
|
||||
lowerKey.includes("склад") ||
|
||||
lowerKey.includes("item") ||
|
||||
lowerKey.includes("номенклат") ||
|
||||
lowerKey.includes("organization") ||
|
||||
lowerKey.includes("организац")) {
|
||||
const value = valueAsString(rawValue).trim();
|
||||
|
|
@ -932,6 +966,10 @@ function toNormalizedRows(rows) {
|
|||
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 {
|
||||
period,
|
||||
|
|
@ -939,7 +977,11 @@ function toNormalizedRows(rows) {
|
|||
account_dt: accountDt,
|
||||
account_kt: accountKt,
|
||||
amount,
|
||||
analytics
|
||||
analytics,
|
||||
quantity,
|
||||
item,
|
||||
warehouse,
|
||||
organization
|
||||
};
|
||||
})
|
||||
.filter((item) => Boolean(item.period || item.registrator));
|
||||
|
|
@ -1066,6 +1108,7 @@ function parseIsoDateUtcTimestamp(value) {
|
|||
function isCounterpartyRiskIntent(intent) {
|
||||
return (intent === "list_receivables_counterparties" ||
|
||||
intent === "list_payables_counterparties" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "list_open_contracts" ||
|
||||
|
|
@ -1080,6 +1123,8 @@ function isHeuristicCandidatesIntent(intent) {
|
|||
function isConfirmedBalanceIntent(intent) {
|
||||
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" ||
|
||||
intent === "vat_payable_confirmed_as_of_date" ||
|
||||
|
|
@ -1125,6 +1170,9 @@ function resolveRequestedResultMode(intent, filters) {
|
|||
if (isConfirmedBalanceIntent(intent)) {
|
||||
return "confirmed_balance";
|
||||
}
|
||||
if (intent === "list_open_contracts") {
|
||||
return "heuristic_candidates";
|
||||
}
|
||||
if (isHeuristicCandidatesIntent(intent)) {
|
||||
const asOfDateBasis = resolveAsOfDateBasis(filters);
|
||||
if (asOfDateBasis === "explicit_as_of_date" || asOfDateBasis === "period_end" || asOfDateBasis === "period_range") {
|
||||
|
|
@ -1245,7 +1293,16 @@ function buildRouteExpectationAudit(input) {
|
|||
};
|
||||
}
|
||||
function enforceStrictAccountScopeForIntent(plan, intent) {
|
||||
if (intent !== "list_receivables_counterparties" || plan.account_scope_mode === "strict") {
|
||||
if (intent === "list_open_contracts" && plan.recipe.recipe_id === "address_open_items_by_party_or_contract_v1") {
|
||||
return plan;
|
||||
}
|
||||
const strictScopeIntents = [
|
||||
"list_receivables_counterparties",
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract"
|
||||
];
|
||||
const shouldEnforceStrictScope = strictScopeIntents.includes(intent);
|
||||
if (!shouldEnforceStrictScope || plan.account_scope_mode === "strict") {
|
||||
return plan;
|
||||
}
|
||||
return {
|
||||
|
|
@ -1737,6 +1794,12 @@ function buildLimitedOffers(input) {
|
|||
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") {
|
||||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
||||
}
|
||||
|
|
@ -1786,8 +1849,10 @@ function buildLimitedIntentSignalLine(input) {
|
|||
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
|
||||
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
||||
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
||||
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: "Сигнал запроса: нужен подтвержденный срез НДС к уплате на дату.",
|
||||
|
|
@ -1917,7 +1982,9 @@ function buildLimitedExecutionResult(input) {
|
|||
});
|
||||
const requestedResultMode = resolveRequestedResultMode(input.intent.intent, input.filters);
|
||||
const reasonsWithConfirmedFallback = withConfirmedBalanceFallbackReason(input.reasons, requestedResultMode, undefined, resultSemantics.result_mode);
|
||||
const exactLimitedReason = input.intent.intent === "payables_confirmed_as_of_date"
|
||||
const exactLimitedReason = 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"
|
||||
|
|
@ -2036,6 +2103,7 @@ class AddressQueryService {
|
|||
requestedResultMode === "confirmed_balance";
|
||||
const confirmedBalanceReceivablesIntent = 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)
|
||||
: null;
|
||||
|
|
@ -2045,7 +2113,11 @@ class AddressQueryService {
|
|||
const vatPayableConfirmedExecution = confirmedBalanceVatPayableIntent
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||
: null;
|
||||
const executionFilters = payablesConfirmedExecution?.executionFilters ??
|
||||
const inventoryConfirmedExecution = confirmedBalanceInventoryIntent
|
||||
? resolveExecutionFiltersForConfirmedBalance(filters.extracted_filters, analysisDate)
|
||||
: null;
|
||||
const executionFilters = inventoryConfirmedExecution?.executionFilters ??
|
||||
payablesConfirmedExecution?.executionFilters ??
|
||||
receivablesConfirmedExecution?.executionFilters ??
|
||||
vatPayableConfirmedExecution?.executionFilters ??
|
||||
filters.extracted_filters;
|
||||
|
|
@ -2076,6 +2148,15 @@ 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 = (0, addressCapabilityPolicy_1.resolveAddressCapabilityRouteDecision)(intent.intent);
|
||||
const capabilityAudit = buildCapabilityAudit(intent.intent);
|
||||
const shadowRouteAudit = buildShadowRouteAudit({
|
||||
|
|
@ -2149,6 +2230,10 @@ class AddressQueryService {
|
|||
!baseReasons.includes("confirmed_balance_exact_receivables_intent")) {
|
||||
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")) {
|
||||
baseReasons.push("confirmed_balance_exact_vat_payable_intent");
|
||||
|
|
@ -2976,6 +3061,7 @@ class AddressQueryService {
|
|||
}
|
||||
const allowConfirmedAsOfZeroSnapshot = filteredRows.length === 0 &&
|
||||
(composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||
composeIntent === "open_contracts_confirmed_as_of_date" ||
|
||||
composeIntent === "payables_confirmed_as_of_date" ||
|
||||
composeIntent === "receivables_confirmed_as_of_date") &&
|
||||
(stageStatus === "no_raw_rows" || stageStatus === "materialized_but_filtered_out_by_recipe") &&
|
||||
|
|
|
|||
|
|
@ -66,6 +66,47 @@ const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"Остатки на дату" КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
ГДЕ
|
||||
Остатки.СуммаРазвернутыйОстатокДт > 0
|
||||
И (__OPEN_CONTRACT_ACCOUNTS_MATCH__)
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"Остатки на дату" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
ГДЕ
|
||||
Остатки.СуммаРазвернутыйОстатокКт > 0
|
||||
И (__OPEN_CONTRACT_ACCOUNTS_MATCH__)
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
|
|
@ -88,6 +129,25 @@ 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__
|
||||
БанкСписание.Дата КАК Период,
|
||||
|
|
@ -615,6 +675,28 @@ const BASE_RECIPES = [
|
|||
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",
|
||||
purpose: "Build confirmed snapshot of contracts with open settlements as-of date from balances on accounts 60/62/76",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope: ["60", "62", "76"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_contracts_by_counterparty_v1",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
|
|
@ -921,6 +1003,8 @@ function maxLimitForIntent(intent) {
|
|||
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" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
|
|
@ -1048,8 +1132,44 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.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"
|
||||
? (() => {
|
||||
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 OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
|
|
|
|||
|
|
@ -565,6 +565,130 @@ function extractCounterpartyName(row) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function extractInventoryItemName(row) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
return typeof row.quantity === "number" && Number.isFinite(row.quantity) ? row.quantity : null;
|
||||
}
|
||||
function buildInventoryOnHandAggregate(rows, asOfDate) {
|
||||
const byPosition = new Map();
|
||||
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");
|
||||
});
|
||||
}
|
||||
function liabilityCategoryLabel(category) {
|
||||
if (category === "supplier_or_contractor") {
|
||||
return "поставщики/подрядчики";
|
||||
|
|
@ -1212,6 +1336,455 @@ function contractCandidatesFromRows(rows) {
|
|||
}
|
||||
return uniqueStrings(candidates);
|
||||
}
|
||||
function isFinancialContractLike(value) {
|
||||
return /(?:кредит|кред\.?|loan|overdraft|овердрафт|лизинг|leasing|займ|guarantee|гарант|банк|bank)/iu.test(value);
|
||||
}
|
||||
function hasStrongContractIdentitySignal(value) {
|
||||
return /(?:договор|contract|дог\.|№|\d{1,4}[\\/.-]\d{1,4}|\d{1,4}\sот\s\d{2}\.\d{2}\.\d{2,4}|[A-ZА-Я]{1,6}-\d+)/iu.test(value);
|
||||
}
|
||||
function isLikelyOrganizationName(value) {
|
||||
return /(?:(?:^|[\s"'«»„“()\\\/])(?:ооо|ао|пао|зао|оао|ип|гку)(?=$|[\s"'«»„“()\\\/.,;:]))|комитет|департамент|министерств|служб|управлени|казенн|администрац|bank|банк/iu.test(value);
|
||||
}
|
||||
function isContractLikeCounterparty(value) {
|
||||
return /(?:договор|дог[-.\s]?р|contract|кредитн|loan|овердрафт|лизинг|\b№\b)/iu.test(value);
|
||||
}
|
||||
function isLowQualityContractIdentity(contract, counterparty) {
|
||||
const normalizedContract = normalizeEntityToken(contract);
|
||||
if (!normalizedContract || normalizedContract.length < 3) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(contract.trim())) {
|
||||
return true;
|
||||
}
|
||||
if (counterparty && normalizeEntityToken(counterparty) === normalizedContract) {
|
||||
return true;
|
||||
}
|
||||
if (!hasStrongContractIdentitySignal(contract) && isLikelyOrganizationName(contract)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasStrongContractIdentitySignal(contract) && /^[A-ZА-Я]{2,6}$/u.test(contract.trim())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isLowQualityCounterpartyForContract(counterparty, contract) {
|
||||
if (!counterparty) {
|
||||
return true;
|
||||
}
|
||||
const normalizedCounterparty = normalizeEntityToken(counterparty);
|
||||
const normalizedContract = normalizeEntityToken(contract);
|
||||
if (!normalizedCounterparty) {
|
||||
return true;
|
||||
}
|
||||
if (normalizedCounterparty === normalizedContract) {
|
||||
return true;
|
||||
}
|
||||
if (isContractLikeCounterparty(counterparty)) {
|
||||
return true;
|
||||
}
|
||||
return normalizedCounterparty.length < 3;
|
||||
}
|
||||
function normalizeDisplayAccountToken(value) {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized || /^(?:0|<пусто>|пустая ссылка|-)$/iu.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function classifyOpenContractCategory(contract, counterparties, qualityFlags) {
|
||||
if (isFinancialContractLike(contract)) {
|
||||
return "financial";
|
||||
}
|
||||
if (counterparties.some((item) => isFinancialContractLike(item))) {
|
||||
return "financial";
|
||||
}
|
||||
if (qualityFlags.includes("counterparty_not_reliably_resolved") ||
|
||||
qualityFlags.includes("contract_identity_not_reliable") ||
|
||||
qualityFlags.includes("contract_identity_looks_like_counterparty") ||
|
||||
qualityFlags.includes("multiple_counterparties_for_contract")) {
|
||||
return "uncertain";
|
||||
}
|
||||
if (counterparties.length === 0) {
|
||||
return "uncertain";
|
||||
}
|
||||
return "commercial";
|
||||
}
|
||||
function classifyOpenContractSettlementKind(row) {
|
||||
const dt = extractAccountSectionCode(row.account_dt);
|
||||
const kt = extractAccountSectionCode(row.account_kt);
|
||||
if (dt === "62") {
|
||||
return "receivable";
|
||||
}
|
||||
if (kt === "60") {
|
||||
return "payable";
|
||||
}
|
||||
if (dt === "60") {
|
||||
return "advance_issued";
|
||||
}
|
||||
if (kt === "62") {
|
||||
return "advance_received";
|
||||
}
|
||||
if (dt === "76") {
|
||||
return "other_receivable";
|
||||
}
|
||||
if (kt === "76") {
|
||||
return "other_payable";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function openContractSettlementKindLabel(kind) {
|
||||
if (kind === "receivable") {
|
||||
return "дебиторская задолженность";
|
||||
}
|
||||
if (kind === "payable") {
|
||||
return "кредиторская задолженность";
|
||||
}
|
||||
if (kind === "advance_issued") {
|
||||
return "аванс выданный";
|
||||
}
|
||||
if (kind === "advance_received") {
|
||||
return "аванс полученный";
|
||||
}
|
||||
if (kind === "other_receivable") {
|
||||
return "прочий дебетовый остаток";
|
||||
}
|
||||
return "прочий кредитовый остаток";
|
||||
}
|
||||
function openContractSettlementKindSign(kind) {
|
||||
if (kind === "receivable" || kind === "advance_issued" || kind === "other_receivable") {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function classifyOpenContractReviewBucket(item) {
|
||||
if (item.category === "commercial") {
|
||||
return null;
|
||||
}
|
||||
if (item.qualityFlags.includes("counterparty_not_reliably_resolved") ||
|
||||
item.qualityFlags.includes("contract_identity_not_reliable") ||
|
||||
item.qualityFlags.includes("contract_identity_looks_like_counterparty") ||
|
||||
item.qualityFlags.includes("multiple_counterparties_for_contract")) {
|
||||
return "dirty_unresolved";
|
||||
}
|
||||
return "special_valid";
|
||||
}
|
||||
function openContractNetBalanceDirectionLabel(amount) {
|
||||
if (amount > 0.005) {
|
||||
return "к получению";
|
||||
}
|
||||
if (amount < -0.005) {
|
||||
return "к оплате";
|
||||
}
|
||||
return "нетто закрыт";
|
||||
}
|
||||
function formatOpenContractComponentsSummary(components) {
|
||||
const kindOrder = [
|
||||
"receivable",
|
||||
"payable",
|
||||
"advance_issued",
|
||||
"advance_received",
|
||||
"other_receivable",
|
||||
"other_payable"
|
||||
];
|
||||
const ordered = [...components].sort((left, right) => kindOrder.indexOf(left.kind) - kindOrder.indexOf(right.kind));
|
||||
return ordered
|
||||
.map((component) => `${openContractSettlementKindLabel(component.kind)} ${formatMoneyRub(component.amount)}`)
|
||||
.join("; ");
|
||||
}
|
||||
function summarizeOpenContractSpecialReason(item) {
|
||||
if (item.category === "financial") {
|
||||
return "похоже на финансовый договор (кредит/банк)";
|
||||
}
|
||||
if (item.qualityFlags.includes("contract_identity_looks_like_counterparty")) {
|
||||
return "в поле договора похоже попал контрагент или чужая аналитика";
|
||||
}
|
||||
if (item.qualityFlags.includes("contract_identity_not_reliable")) {
|
||||
return "договор не похож на устойчивый договорный реквизит";
|
||||
}
|
||||
if (item.qualityFlags.includes("multiple_counterparties_for_contract")) {
|
||||
return "по одному договору найдено несколько контрагентов";
|
||||
}
|
||||
if (item.qualityFlags.includes("counterparty_not_reliably_resolved")) {
|
||||
return "не удалось надежно определить контрагента";
|
||||
}
|
||||
return "требуется ручная проверка карточки договора";
|
||||
}
|
||||
function buildOpenContractConfirmedBalanceAggregate(rows, asOfDate) {
|
||||
const byContract = new Map();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
for (const row of rows) {
|
||||
const rowTimestamp = toUtcDayTimestamp(row.period);
|
||||
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
|
||||
continue;
|
||||
}
|
||||
const contract = extractContractName(row);
|
||||
if (!contract) {
|
||||
continue;
|
||||
}
|
||||
const amount = row.amount;
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||
continue;
|
||||
}
|
||||
const settlementKind = classifyOpenContractSettlementKind(row);
|
||||
if (!settlementKind) {
|
||||
continue;
|
||||
}
|
||||
const counterpartyCandidate = extractCounterpartyName(row);
|
||||
const counterparty = isLowQualityCounterpartyForContract(counterpartyCandidate, contract) ? null : counterpartyCandidate;
|
||||
const sourceRefs = extractPayablesSourceRefs(row, counterparty ?? contract, contract);
|
||||
const accountToken = normalizeDisplayAccountToken(row.account_dt) ?? normalizeDisplayAccountToken(row.account_kt);
|
||||
const absAmount = Math.abs(amount);
|
||||
const contractKey = normalizeEntityToken(contract);
|
||||
const counterpartyKey = counterparty ? normalizeEntityToken(counterparty) : "__unknown_counterparty__";
|
||||
const aggregateKey = `${contractKey}::${counterpartyKey}::${settlementKind}`;
|
||||
const current = byContract.get(aggregateKey);
|
||||
if (!current) {
|
||||
const qualityFlags = new Set();
|
||||
if (!counterparty) {
|
||||
qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
if (isLowQualityContractIdentity(contract, counterparty)) {
|
||||
qualityFlags.add("contract_identity_not_reliable");
|
||||
}
|
||||
if (counterparty && normalizeEntityToken(counterparty) === normalizeEntityToken(contract)) {
|
||||
qualityFlags.add("contract_identity_looks_like_counterparty");
|
||||
}
|
||||
const counterparties = new Set();
|
||||
if (counterparty) {
|
||||
counterparties.add(counterparty);
|
||||
}
|
||||
const accounts = new Set();
|
||||
if (accountToken) {
|
||||
accounts.add(accountToken);
|
||||
}
|
||||
byContract.set(aggregateKey, {
|
||||
contract,
|
||||
counterparty,
|
||||
confirmedAmount: absAmount,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
counterparties,
|
||||
settlementKind,
|
||||
accounts,
|
||||
sourceRefs: new Set(sourceRefs),
|
||||
qualityFlags
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.confirmedAmount += absAmount;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
if (counterparty) {
|
||||
current.counterparty = current.counterparty ?? counterparty;
|
||||
current.counterparties.add(counterparty);
|
||||
}
|
||||
else {
|
||||
current.qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
if (accountToken) {
|
||||
current.accounts.add(accountToken);
|
||||
}
|
||||
for (const ref of sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
return Array.from(byContract.values())
|
||||
.map((item) => {
|
||||
const counterparties = Array.from(item.counterparties);
|
||||
if (counterparties.length > 1) {
|
||||
item.qualityFlags.add("multiple_counterparties_for_contract");
|
||||
}
|
||||
return {
|
||||
contract: item.contract,
|
||||
counterparty: item.counterparty,
|
||||
confirmedAmount: item.confirmedAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
category: classifyOpenContractCategory(item.contract, counterparties, Array.from(item.qualityFlags)),
|
||||
settlementKind: item.settlementKind,
|
||||
accounts: Array.from(item.accounts).slice(0, 3),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
|
||||
qualityFlags: Array.from(item.qualityFlags)
|
||||
};
|
||||
})
|
||||
.filter((item) => item.confirmedAmount > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.confirmedAmount !== left.confirmedAmount) {
|
||||
return right.confirmedAmount - left.confirmedAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.contract.localeCompare(right.contract);
|
||||
});
|
||||
}
|
||||
function buildOpenContractNetAggregate(items) {
|
||||
const byContract = new Map();
|
||||
const categoryPriority = (value) => {
|
||||
if (value === "financial") {
|
||||
return 2;
|
||||
}
|
||||
if (value === "uncertain") {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
for (const item of items) {
|
||||
const counterpartyKey = item.counterparty ? normalizeEntityToken(item.counterparty) : "__unknown_counterparty__";
|
||||
const aggregateKey = `${normalizeEntityToken(item.contract)}::${counterpartyKey}`;
|
||||
const current = byContract.get(aggregateKey);
|
||||
if (!current) {
|
||||
byContract.set(aggregateKey, {
|
||||
contract: item.contract,
|
||||
counterparty: item.counterparty,
|
||||
category: item.category,
|
||||
netOpenBalance: openContractSettlementKindSign(item.settlementKind) * item.confirmedAmount,
|
||||
grossOpenBalance: item.confirmedAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
accounts: new Set(item.accounts),
|
||||
sourceRefs: new Set(item.sourceRefs),
|
||||
qualityFlags: new Set(item.qualityFlags),
|
||||
componentAmounts: new Map([[item.settlementKind, item.confirmedAmount]])
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (categoryPriority(item.category) > categoryPriority(current.category)) {
|
||||
current.category = item.category;
|
||||
}
|
||||
current.netOpenBalance += openContractSettlementKindSign(item.settlementKind) * item.confirmedAmount;
|
||||
current.grossOpenBalance += item.confirmedAmount;
|
||||
current.operations += item.operations;
|
||||
if ((item.firstPeriod ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = item.firstPeriod;
|
||||
}
|
||||
if ((item.lastPeriod ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = item.lastPeriod;
|
||||
}
|
||||
for (const account of item.accounts) {
|
||||
current.accounts.add(account);
|
||||
}
|
||||
for (const ref of item.sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
for (const flag of item.qualityFlags) {
|
||||
current.qualityFlags.add(flag);
|
||||
}
|
||||
current.componentAmounts.set(item.settlementKind, (current.componentAmounts.get(item.settlementKind) ?? 0) + item.confirmedAmount);
|
||||
}
|
||||
return Array.from(byContract.values())
|
||||
.map((item) => {
|
||||
const qualityFlags = Array.from(item.qualityFlags);
|
||||
return {
|
||||
contract: item.contract,
|
||||
counterparty: item.counterparty,
|
||||
category: item.category,
|
||||
reviewBucket: classifyOpenContractReviewBucket({
|
||||
category: item.category,
|
||||
qualityFlags
|
||||
}),
|
||||
netOpenBalance: item.netOpenBalance,
|
||||
grossOpenBalance: item.grossOpenBalance,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
accounts: Array.from(item.accounts).slice(0, 4),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
|
||||
qualityFlags,
|
||||
componentAmounts: Array.from(item.componentAmounts.entries())
|
||||
.map(([kind, amount]) => ({ kind, amount }))
|
||||
.filter((component) => component.amount > 0.005)
|
||||
};
|
||||
})
|
||||
.filter((item) => item.grossOpenBalance > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.grossOpenBalance !== left.grossOpenBalance) {
|
||||
return right.grossOpenBalance - left.grossOpenBalance;
|
||||
}
|
||||
if (Math.abs(right.netOpenBalance) !== Math.abs(left.netOpenBalance)) {
|
||||
return Math.abs(right.netOpenBalance) - Math.abs(left.netOpenBalance);
|
||||
}
|
||||
return left.contract.localeCompare(right.contract);
|
||||
});
|
||||
}
|
||||
function buildOpenContractRiskAggregate(rows) {
|
||||
const byContract = new Map();
|
||||
for (const row of rows) {
|
||||
const contract = extractContractName(row);
|
||||
if (!contract) {
|
||||
continue;
|
||||
}
|
||||
const amountRaw = row.amount ?? 0;
|
||||
const amount = Number.isFinite(amountRaw) ? Math.abs(amountRaw) : 0;
|
||||
const current = byContract.get(contract);
|
||||
const counterpartyCandidate = extractCounterpartyName(row);
|
||||
const counterparty = isLowQualityCounterpartyForContract(counterpartyCandidate, contract) ? null : counterpartyCandidate;
|
||||
const sourceRefs = extractPayablesSourceRefs(row, counterparty ?? contract, contract);
|
||||
if (!current) {
|
||||
const qualityFlags = new Set();
|
||||
if (!counterparty) {
|
||||
qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
byContract.set(contract, {
|
||||
contract,
|
||||
totalAmount: amount,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
counterparties: new Set(counterparty ? [counterparty] : []),
|
||||
sourceRefs: new Set(sourceRefs),
|
||||
qualityFlags
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.totalAmount += amount;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
if (counterparty) {
|
||||
current.counterparties.add(counterparty);
|
||||
}
|
||||
else {
|
||||
current.qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
for (const ref of sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
return Array.from(byContract.values())
|
||||
.map((item) => ({
|
||||
contract: item.contract,
|
||||
totalAmount: item.totalAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
counterparties: Array.from(item.counterparties).slice(0, 2),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
|
||||
category: classifyOpenContractCategory(item.contract, Array.from(item.counterparties), Array.from(item.qualityFlags)),
|
||||
qualityFlags: Array.from(item.qualityFlags)
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (right.totalAmount !== left.totalAmount) {
|
||||
return right.totalAmount - left.totalAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.contract.localeCompare(right.contract);
|
||||
});
|
||||
}
|
||||
function composeFactualReply(intent, rows, options = {}) {
|
||||
const applyNumericEmphasis = (line) => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line);
|
||||
const joinLines = (lines) => lines.map(applyNumericEmphasis).join("\n");
|
||||
|
|
@ -2250,31 +2823,259 @@ function composeFactualReply(intent, rows, options = {}) {
|
|||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (intent === "list_open_contracts") {
|
||||
const contracts = contractCandidatesFromRows(rows);
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
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 = [
|
||||
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
|
||||
`Строк движения: ${rows.length}.`,
|
||||
`Договорных кандидатов: ${contracts.length}.`
|
||||
`Собран подтвержденный срез товаров на складах на ${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 (contracts.length > 0) {
|
||||
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||
}
|
||||
else if (counterparties.length > 0) {
|
||||
lines.push(`Контрагентов с сигналом незакрытых хвостов: ${counterparties.length}.`);
|
||||
lines.push(...counterparties
|
||||
.slice(0, 8)
|
||||
.map((item, index) => `${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`));
|
||||
lines.push("Договорные якоря в этом live-срезе не выделены, поэтому показан контрагентный рейтинг риска.");
|
||||
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("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
|
||||
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);
|
||||
const contractProfiles = buildOpenContractNetAggregate(confirmedContracts);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const commercialContracts = confirmedContracts.filter((item) => item.category === "commercial");
|
||||
const commercialProfiles = contractProfiles.filter((item) => item.category === "commercial");
|
||||
const specialProfiles = contractProfiles.filter((item) => item.reviewBucket === "special_valid");
|
||||
const dirtyProfiles = contractProfiles.filter((item) => item.reviewBucket === "dirty_unresolved");
|
||||
const uniqueContracts = uniqueStrings(contractProfiles.map((item) => item.contract));
|
||||
const commercialReceivables = commercialContracts.filter((item) => item.settlementKind === "receivable");
|
||||
const commercialPayables = commercialContracts.filter((item) => item.settlementKind === "payable");
|
||||
const commercialAdvances = commercialContracts.filter((item) => item.settlementKind === "advance_issued" || item.settlementKind === "advance_received");
|
||||
const commercialOther = commercialContracts.filter((item) => item.settlementKind === "other_receivable" || item.settlementKind === "other_payable");
|
||||
const sumConfirmedAmount = (items) => items.reduce((sum, item) => sum + item.confirmedAmount, 0);
|
||||
const sumNetAmount = (items) => items.reduce((sum, item) => sum + item.netOpenBalance, 0);
|
||||
const sumGrossAmount = (items) => items.reduce((sum, item) => sum + item.grossOpenBalance, 0);
|
||||
const commercialNetTotal = sumNetAmount(commercialProfiles);
|
||||
const commercialGrossTotal = sumGrossAmount(commercialProfiles);
|
||||
const specialTotal = sumGrossAmount(specialProfiles);
|
||||
const dirtyTotal = sumGrossAmount(dirtyProfiles);
|
||||
const periodScopeLine = !options.asOfDate && (periodFrom || periodTo)
|
||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||
: null;
|
||||
const renderContractProfileLines = (items, includeSpecialReason) => items.slice(0, 12).map((item, index) => {
|
||||
const counterpartyLabel = item.counterparty ?? "контрагент не определен";
|
||||
const accountsLabel = item.accounts.length > 0 ? ` | через счета: ${item.accounts.join("; ")}` : "";
|
||||
const evidenceLabel = item.sourceRefs.length > 0 ? ` | основное основание: ${item.sourceRefs[0]}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 1 ? ` | source refs: ${item.sourceRefs.slice(1, 3).join("; ")}` : "";
|
||||
const specialReasonLabel = includeSpecialReason
|
||||
? ` | причина вынесения: ${summarizeOpenContractSpecialReason(item)}`
|
||||
: "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartyLabel} | чистый остаток: ${openContractNetBalanceDirectionLabel(item.netOpenBalance)} ${formatMoneyRub(Math.abs(item.netOpenBalance))} | брутто компонентов: ${formatMoneyRub(item.grossOpenBalance)} | состав: ${formatOpenContractComponentsSummary(item.componentAmounts)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${accountsLabel}${evidenceLabel}${refsLabel}${specialReasonLabel}`;
|
||||
});
|
||||
const renderConfirmedContractLines = (items, includeSpecialReason) => items.slice(0, 12).map((item, index) => {
|
||||
const counterpartyLabel = item.counterparty ?? "контрагент не определен";
|
||||
const accountsLabel = item.accounts.length > 0 ? ` | счета: ${item.accounts.join("; ")}` : "";
|
||||
const evidenceLabel = item.sourceRefs.length > 0 ? ` | основное основание: ${item.sourceRefs[0]}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 1 ? ` | source refs: ${item.sourceRefs.slice(1, 3).join("; ")}` : "";
|
||||
const specialReasonLabel = includeSpecialReason
|
||||
? ` | причина вынесения: ${summarizeOpenContractSpecialReason(item)}`
|
||||
: "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartyLabel} | подтвержденный открытый остаток: ${formatMoneyRub(item.confirmedAmount)} | тип остатка: ${openContractSettlementKindLabel(item.settlementKind)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${accountsLabel}${evidenceLabel}${refsLabel}${specialReasonLabel}`;
|
||||
});
|
||||
const lines = [
|
||||
`Собран подтвержденный срез открытых договоров на ${formatDateRu(asOfDate)}.`,
|
||||
`Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
|
||||
`Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`,
|
||||
`Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
|
||||
`Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
|
||||
"- База ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.",
|
||||
"- Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов.",
|
||||
"- Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка."
|
||||
];
|
||||
lines.push("");
|
||||
lines.push("Блок 2. Что учтено");
|
||||
lines.push(`- Дата среза: ${formatDateRu(asOfDate)}.`);
|
||||
if (periodScopeLine) {
|
||||
lines.push(periodScopeLine);
|
||||
}
|
||||
lines.push("- Дефолтная бизнес-дефиниция: открыт договор, по которому на дату есть ненулевой остаток взаиморасчетов.");
|
||||
lines.push("- Контур: остатки по счетам 60/62/76.");
|
||||
lines.push("- Смешанные экономические смыслы не склеиваются: дебиторка, кредиторка, авансы и прочие остатки показаны раздельно.");
|
||||
lines.push("");
|
||||
lines.push("Блок 3. Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Уникальных договоров: ${formatNumberWithDots(uniqueContracts.length)}.`);
|
||||
lines.push(`- Подтвержденных договор-контрагент профилей: ${formatNumberWithDots(contractProfiles.length)}.`);
|
||||
lines.push(`- Подтвержденных договорных компонентов: ${formatNumberWithDots(confirmedContracts.length)}.`);
|
||||
lines.push(`- Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`);
|
||||
lines.push(`- Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`);
|
||||
lines.push(`- Коммерческая дебиторка: ${formatNumberWithDots(commercialReceivables.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialReceivables))}.`);
|
||||
lines.push(`- Коммерческая кредиторка: ${formatNumberWithDots(commercialPayables.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialPayables))}.`);
|
||||
lines.push(`- Коммерческие авансы: ${formatNumberWithDots(commercialAdvances.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialAdvances))}.`);
|
||||
lines.push(`- Прочие расчеты по 76: ${formatNumberWithDots(commercialOther.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialOther))}.`);
|
||||
lines.push(`- Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`);
|
||||
lines.push(`- Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`);
|
||||
if (commercialProfiles.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Чистый открытый остаток по договорам");
|
||||
lines.push(...renderContractProfileLines(commercialProfiles, false));
|
||||
}
|
||||
if (commercialReceivables.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Коммерческие дебиторские компоненты");
|
||||
lines.push(...renderConfirmedContractLines(commercialReceivables, false));
|
||||
}
|
||||
if (commercialPayables.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 6. Коммерческие кредиторские компоненты");
|
||||
lines.push(...renderConfirmedContractLines(commercialPayables, false));
|
||||
}
|
||||
if (commercialAdvances.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 7. Коммерческие авансовые компоненты");
|
||||
lines.push(...renderConfirmedContractLines(commercialAdvances, false));
|
||||
}
|
||||
if (commercialOther.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 8. Прочие компоненты по 76");
|
||||
lines.push(...renderConfirmedContractLines(commercialOther, false));
|
||||
}
|
||||
if (specialProfiles.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 9. Финансовые/специальные позиции");
|
||||
lines.push(...renderContractProfileLines(specialProfiles, true));
|
||||
}
|
||||
if (dirtyProfiles.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 10. Спорные/некачественно нормализованные позиции");
|
||||
lines.push(...renderContractProfileLines(dirtyProfiles, true));
|
||||
}
|
||||
if (confirmedContracts.length === 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Подтвержденные позиции");
|
||||
lines.push("- На дату среза подтвержденные договоры с открытыми взаиморасчетами не найдены.");
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: "strong",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "list_open_contracts") {
|
||||
const contracts = buildOpenContractRiskAggregate(rows);
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate ?? options.periodTo ?? options.periodFrom);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const commercialContracts = contracts.filter((item) => item.category === "commercial");
|
||||
const specialContracts = contracts.filter((item) => item.category !== "commercial");
|
||||
const commercialTotal = commercialContracts.reduce((sum, item) => sum + item.totalAmount, 0);
|
||||
const lines = [
|
||||
`Итого по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""}: ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: предварительный список договоров с возможными незакрытыми расчетами.",
|
||||
"- Перед финансовым решением нужна сверка карточек договоров и взаиморасчетов в 1С.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
...(asOfDate
|
||||
? [`- Дата среза: ${formatDateRu(asOfDate)}.`]
|
||||
: periodFrom || periodTo
|
||||
? [`- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`]
|
||||
: []),
|
||||
"- Контур: движения по счетам 60/62/76 и договорная аналитика.",
|
||||
"",
|
||||
"Блок 3. Сводка",
|
||||
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||
`- Договоров-кандидатов всего: ${formatNumberWithDots(contracts.length)}.`,
|
||||
`- Основной список (коммерческие): ${formatNumberWithDots(commercialContracts.length)}.`,
|
||||
`- Вынесено в финансовые/спорные: ${formatNumberWithDots(specialContracts.length)}.`
|
||||
];
|
||||
if (commercialContracts.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Основной список (коммерческие договоры)");
|
||||
lines.push(...commercialContracts.slice(0, 10).map((item, index) => {
|
||||
const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
|
||||
const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartiesLabel} | сумма возможного открытого остатка: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""} | почему в списке: есть признаки незакрытых расчетов на дату${sourceRefsSuffix}`;
|
||||
}));
|
||||
if (specialContracts.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Финансовые/спорные позиции (вынесены отдельно)");
|
||||
lines.push(...specialContracts.slice(0, 8).map((item, index) => {
|
||||
const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
|
||||
const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartiesLabel} | сумма сигнала: ${formatMoneyRub(item.totalAmount)} | причина вынесения: ${summarizeOpenContractSpecialReason(item)}${sourceRefsSuffix}`;
|
||||
}));
|
||||
}
|
||||
}
|
||||
else if (counterparties.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов");
|
||||
lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`);
|
||||
lines.push(...counterparties
|
||||
.slice(0, 8)
|
||||
.map((item, index) => `${index + 1}. ${item.name} | сумма сигнала: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`));
|
||||
lines.push("- Договорные реквизиты выделены недостаточно надежно, поэтому показан контрагентный список для проверки.");
|
||||
}
|
||||
else {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Позиции не выделены");
|
||||
lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации.");
|
||||
lines.push("Блок 5. Примеры исходных строк");
|
||||
lines.push(...formatTopRows(rows, 6));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: contracts.length > 0 || counterparties.length > 0 ? "medium" : "weak",
|
||||
balance_confirmed: false
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function hasAllTimeHint(text) {
|
|||
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+весь\s+срок|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|за\s+любой\s+срок|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(normalized);
|
||||
}
|
||||
function hasSameDateHint(text) {
|
||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasExplicitPeriodLiteral(text) {
|
||||
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
|
||||
|
|
@ -286,6 +286,20 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
const merged = { ...current };
|
||||
const reasons = [];
|
||||
if (!followupContext) {
|
||||
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(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 };
|
||||
}
|
||||
const previous = followupContext.previous_filters ?? {};
|
||||
|
|
@ -374,6 +388,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
}
|
||||
if (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") {
|
||||
|
|
@ -442,6 +458,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
|
||||
const asOfPrimaryIntent = 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";
|
||||
|
|
@ -474,12 +492,28 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
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(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 };
|
||||
}
|
||||
function resolveMissingRequiredFilters(intent, filters) {
|
||||
const requiredByIntent = {
|
||||
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"],
|
||||
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
||||
|
|
@ -527,7 +561,8 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_followup_context"]
|
||||
};
|
||||
}
|
||||
if (hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
||||
const allowOpenItemsFollowupFallback = detectedIntent.intent === "unknown" && !isVatFollowup;
|
||||
if (allowOpenItemsFollowupFallback && hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
||||
return {
|
||||
intent: "open_items_by_counterparty_or_contract",
|
||||
confidence: "low",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ function inferAggregationProfile(intent, shape) {
|
|||
}
|
||||
if (intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_confirmed_as_of_date" ||
|
||||
|
|
|
|||
|
|
@ -1070,7 +1070,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;
|
||||
}
|
||||
|
|
@ -3843,10 +3843,12 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
|||
"counterparty_activity_lifecycle",
|
||||
"customer_revenue_and_payments",
|
||||
"supplier_payouts_profile",
|
||||
"open_contracts_confirmed_as_of_date",
|
||||
"list_open_contracts",
|
||||
"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",
|
||||
|
|
@ -4236,7 +4238,7 @@ 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());
|
||||
|
|
@ -4244,12 +4246,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;
|
||||
}
|
||||
|
|
@ -4258,7 +4260,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;
|
||||
}
|
||||
|
|
@ -5080,14 +5082,13 @@ async function resolveAssistantDataScopeProbe() {
|
|||
};
|
||||
}
|
||||
const catalogQueryCandidates = [
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 20 ПРЕДСТАВЛЕН<D095>?Е(Организации.Ссылка) КАК Организация <20>?З Справочник.Организации КАК Организации",
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация <20>?З Справочник.Организации КАК Организации",
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация <20>?З Справочник.Организации КАК Организации",
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕН<D095>?Е(Организации.Ссылка) КАК ОрганизацияПредставление <20>?З Справочник.Организации КАК Организации"
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.Наименование КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 20 Организации.НаименованиеПолное КАК Организация ИЗ Справочник.Организации КАК Организации",
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 100 Организации.Ссылка КАК Организация, ПРЕДСТАВЛЕНИЕ(Организации.Ссылка) КАК ОрганизацияПредставление ИЗ Справочник.Организации КАК Организации"
|
||||
];
|
||||
const movementProbeCandidates = [
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация, ПРЕДСТАВЛЕН<D095>?Е(Движения.Организация) КАК ОрганизацияПредставление <20>?З РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧ<EFBFBD>?ТЬ ПО Движения.Период УБЫВ",
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация <EFBFBD>?З РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения УПОРЯДОЧИТЬ ПО Движения.Период УБЫВ",
|
||||
"ВЫБРАТЬ ПЕРВЫЕ 60 Движения.Организация КАК Организация ИЗ РегистрБухгалтерии.Хозрасчетный КАК Движения"
|
||||
];
|
||||
let lastError = null;
|
||||
const catalogFacts = { names: [], refs: [], pairs: [] };
|
||||
|
|
@ -5218,7 +5219,7 @@ function buildAssistantOperationalBoundaryReply() {
|
|||
return [
|
||||
"Понимаю, что ситуация срочная.",
|
||||
"Я не могу сам настраивать 1С или менять базу/конфигурацию.",
|
||||
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/<EFBFBD>?Т-админа."
|
||||
"Могу помочь безопасно: разберем симптомы и подготовим точные шаги для вашего 1С/ИТ-админа."
|
||||
].join(" ");
|
||||
}
|
||||
function buildAssistantSafetyRefusalReply() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type AddressIntent =
|
|||
| "payables_confirmed_as_of_date"
|
||||
| "receivables_confirmed_as_of_date"
|
||||
| "list_receivables_counterparties"
|
||||
| "inventory_on_hand_as_of_date"
|
||||
| "account_balance_snapshot"
|
||||
| "open_items_by_counterparty_or_contract"
|
||||
| "list_documents_by_counterparty"
|
||||
|
|
@ -138,7 +139,8 @@ export interface AddressRecipeDefinition {
|
|||
| "vat_payable_confirmed_as_of_balance_profile"
|
||||
| "open_contracts_confirmed_as_of_balance_profile"
|
||||
| "payables_confirmed_as_of_balance_profile"
|
||||
| "receivables_confirmed_as_of_balance_profile";
|
||||
| "receivables_confirmed_as_of_balance_profile"
|
||||
| "inventory_on_hand_as_of_balance_profile";
|
||||
required_filters: Array<keyof AddressFilterSet>;
|
||||
optional_filters: Array<keyof AddressFilterSet>;
|
||||
default_limit: number;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@ describe("address capability policy", () => {
|
|||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("maps inventory-on-hand intent to compute exact capability", () => {
|
||||
const decision = resolveAddressCapabilityRouteDecision("inventory_on_hand_as_of_date");
|
||||
expect(decision.capability_id).toBe("confirmed_inventory_on_hand_as_of_date");
|
||||
expect(decision.capability_layer).toBe("compute");
|
||||
expect(decision.capability_route_mode).toBe("exact");
|
||||
expect(decision.capability_route_enabled).toBe(true);
|
||||
expect(isCapabilityRouteBlocked(decision)).toBe(false);
|
||||
});
|
||||
|
||||
it("maps document drilldown intent to navigation capability", () => {
|
||||
const decision = resolveAddressCapabilityRouteDecision("list_documents_by_contract");
|
||||
expect(decision.capability_id).toBe("documents_drilldown");
|
||||
|
|
|
|||
|
|
@ -4327,6 +4327,72 @@ describe("address recipe catalog counterparty filtering", () => {
|
|||
expect(plan.query).toContain("VAT_BOOK_SALES");
|
||||
expect(plan.query).toContain("VAT_BOOK_PURCHASES");
|
||||
});
|
||||
|
||||
it("keeps inventory-on-hand phrasing in address lane", () => {
|
||||
const result = detectAddressQuestionMode("Какие товары сейчас лежат на складе");
|
||||
expect(result.mode).toBe("address_query");
|
||||
});
|
||||
|
||||
it("detects exact inventory-on-hand intent", () => {
|
||||
const result = resolveAddressIntent("Какие товары сейчас лежат на складе");
|
||||
expect(result.intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(result.confidence).toBe("high");
|
||||
});
|
||||
|
||||
it("derives as_of_date for inventory-on-hand from explicit month window", () => {
|
||||
const filters = extractAddressFilters(
|
||||
"Какие товары лежат на складе на март 2020",
|
||||
"inventory_on_hand_as_of_date"
|
||||
).extracted_filters;
|
||||
expect(filters.period_from).toBe("2020-03-01");
|
||||
expect(filters.period_to).toBe("2020-03-31");
|
||||
expect(filters.as_of_date).toBe("2020-03-31");
|
||||
});
|
||||
|
||||
it("builds exact balance query for inventory-on-hand snapshot", () => {
|
||||
const selected = selectAddressRecipe("inventory_on_hand_as_of_date", {
|
||||
as_of_date: "2020-03-31"
|
||||
});
|
||||
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_on_hand_as_of_date_v1");
|
||||
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||
as_of_date: "2020-03-31"
|
||||
});
|
||||
|
||||
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки(");
|
||||
expect(plan.query).toContain("КоличествоРазвернутыйОстатокДт");
|
||||
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 5) = \"41.01\"");
|
||||
});
|
||||
|
||||
it("renders confirmed inventory-on-hand snapshot from normalized rows", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_on_hand_as_of_date",
|
||||
[
|
||||
{
|
||||
period: "2020-03-31T23:59:59Z",
|
||||
registrator: "Остатки на дату",
|
||||
account_dt: "41.01",
|
||||
account_kt: "",
|
||||
amount: 712500,
|
||||
analytics: ["Шкаф картотечный", "Основной склад", "ООО Ромашка"],
|
||||
item: "Шкаф картотечный",
|
||||
warehouse: "Основной склад",
|
||||
organization: "ООО Ромашка",
|
||||
quantity: 15
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2020-03-31",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text).toContain("Собран подтвержденный срез товаров на складах");
|
||||
expect(reply.text).toContain("Контур: остатки по счету 41.01");
|
||||
expect(reply.text).toContain("Шкаф картотечный");
|
||||
expect(reply.text).toContain("Основной склад");
|
||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,17 @@ describe("address route expectations contract", () => {
|
|||
expect(audit.reason).toBe("route_expectation_matched");
|
||||
});
|
||||
|
||||
it("matches expected recipe and result mode for exact inventory route", () => {
|
||||
const audit = evaluateAddressRouteExpectation({
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
selectedRecipe: "address_inventory_on_hand_as_of_date_v1",
|
||||
requestedResultMode: "confirmed_balance",
|
||||
resultMode: "confirmed_balance"
|
||||
});
|
||||
expect(audit.status).toBe("matched");
|
||||
expect(audit.reason).toBe("route_expectation_matched");
|
||||
});
|
||||
|
||||
it("matches expected recipe and result mode for exact VAT tax-period liability route", () => {
|
||||
const audit = evaluateAddressRouteExpectation({
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
|
|
|
|||
|
|
@ -646,6 +646,24 @@ describe("assistant orchestration contract", () => {
|
|||
expect(decision.toolGateReason).toBe("address_mode_classifier_detected");
|
||||
});
|
||||
|
||||
it("keeps inventory-on-hand query in address lane", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "Какие товары сейчас лежат на складе",
|
||||
effectiveAddressUserMessage: "Какие товары сейчас лежат на складе",
|
||||
followupContext: null,
|
||||
llmPreDecomposeMeta: null as any,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(decision.runAddressLane).toBe(true);
|
||||
expect(decision.toolGateDecision).toBe("run_address_lane");
|
||||
expect(["address_intent_resolver_detected", "address_mode_classifier_detected", "address_signal_detected"]).toContain(
|
||||
String(decision.toolGateReason)
|
||||
);
|
||||
expect(decision.livingMode).toBe("address_data");
|
||||
expect(decision.livingReason).toBe("address_lane_triggered");
|
||||
});
|
||||
|
||||
it("keeps open-contracts request in address lane even with stale deep followup context when LLM contract is absent", () => {
|
||||
const decision = resolveAssistantOrchestrationDecision({
|
||||
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",
|
||||
|
|
|
|||
Loading…
Reference in New Issue