Post-F: зафиксировать VAT materialization и приоритет явного контрагента

This commit is contained in:
dctouch 2026-04-23 23:39:55 +03:00
parent 92cd272efc
commit 2f282f1479
6 changed files with 159 additions and 24 deletions

View File

@ -645,7 +645,7 @@ __WHERE_CLAUSE__
`;
const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_TO_EXPR__ КАК Период,
"VAT_BOOK_SALES" КАК Регистратор,
"68.02" КАК СчетДт,
"" КАК СчетКт,
@ -655,7 +655,7 @@ const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_TO_EXPR__ КАК Период,
"VAT_BOOK_PURCHASES" КАК Регистратор,
"19" КАК СчетДт,
"" КАК СчетКт,
@ -1395,7 +1395,18 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
? (() => {
const periodToExpr = (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) ??
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
return VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
})()
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0

View File

@ -1120,7 +1120,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
});
const metadataLaneScopeHint = rawMetadataScopeHint ??
const explicitCurrentCounterpartyCandidate = normalizedPredecomposeCounterparty && !isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty)
? normalizedPredecomposeCounterparty
: null;
const explicitCurrentCounterpartyOverridesFollowupEntity = Boolean(explicitCurrentCounterpartyCandidate &&
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
!sameScopedName(explicitCurrentCounterpartyCandidate, followupSeed.counterparty ?? followupSeed.discoveryEntity) &&
(valueFlowSignal ||
lifecycleSignal ||
metadataGroundedDocumentLaneApplicable ||
metadataGroundedMovementLaneApplicable));
const metadataLaneScopeHint = explicitCurrentCounterpartyOverridesFollowupEntity
? null
: rawMetadataScopeHint ??
followupSeed.metadataScopeHint ??
followupSeed.discoveryEntity ??
followupSeed.metadataSelectedEntitySet ??
@ -1129,6 +1141,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!followupSeed.counterparty &&
metadataLaneCarryoverAvailable);
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
? null
: explicitCurrentCounterpartyOverridesFollowupEntity
? null
: followupSeed.counterparty ?? followupSeed.discoveryEntity;
const entityCandidates = entityResolutionSignal ? [] : [];
@ -1179,6 +1193,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? currentTurnOrganizationScope ?? followupSeed.organization
: null;
if (explicitCurrentCounterpartyCandidate &&
(valueFlowSignal || lifecycleSignal || metadataGroundedDocumentLaneApplicable || metadataGroundedMovementLaneApplicable)) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
const candidate = entityCandidates[index];
if (!candidate || sameScopedName(candidate, explicitCurrentCounterpartyCandidate)) {
continue;
}
if ((metadataLaneScopeHint && sameScopedName(candidate, metadataLaneScopeHint)) ||
(explicitOrganizationScope && sameScopedName(candidate, explicitOrganizationScope)) ||
(followupSeed.organization && sameScopedName(candidate, followupSeed.organization)) ||
(followupSeed.counterparty &&
!sameScopedName(followupSeed.counterparty, explicitCurrentCounterpartyCandidate) &&
sameScopedName(candidate, followupSeed.counterparty))) {
entityCandidates.splice(index, 1);
}
}
}
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
if (entityCandidates[index] === explicitOrganizationScope) {

View File

@ -668,7 +668,7 @@ __WHERE_CLAUSE__
const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_TO_EXPR__ КАК Период,
"VAT_BOOK_SALES" КАК Регистратор,
"68.02" КАК СчетДт,
"" КАК СчетКт,
@ -678,7 +678,7 @@ const VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE = `
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_TO_EXPR__ КАК Период,
"VAT_BOOK_PURCHASES" КАК Регистратор,
"19" КАК СчетДт,
"" КАК СчетКт,
@ -1553,10 +1553,19 @@ export function buildAddressRecipePlan(
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
? VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE.replaceAll(
"__WHERE_CLAUSE__",
buildManagementWhereClause(filters, "Движения.Период")
)
? (() => {
const periodToExpr =
(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) ??
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
return VAT_LIABILITY_CONFIRMED_TAX_PERIOD_QUERY_TEMPLATE
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
})()
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr =

View File

@ -1478,8 +1478,23 @@ export function buildAssistantMcpDiscoveryTurnInput(
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
});
const explicitCurrentCounterpartyCandidate =
normalizedPredecomposeCounterparty && !isReferentialEntityPlaceholder(normalizedPredecomposeCounterparty)
? normalizedPredecomposeCounterparty
: null;
const explicitCurrentCounterpartyOverridesFollowupEntity = Boolean(
explicitCurrentCounterpartyCandidate &&
(followupSeed.counterparty || followupSeed.discoveryEntity) &&
!sameScopedName(explicitCurrentCounterpartyCandidate, followupSeed.counterparty ?? followupSeed.discoveryEntity) &&
(valueFlowSignal ||
lifecycleSignal ||
metadataGroundedDocumentLaneApplicable ||
metadataGroundedMovementLaneApplicable)
);
const metadataLaneScopeHint =
rawMetadataScopeHint ??
explicitCurrentCounterpartyOverridesFollowupEntity
? null
: rawMetadataScopeHint ??
followupSeed.metadataScopeHint ??
followupSeed.discoveryEntity ??
followupSeed.metadataSelectedEntitySet ??
@ -1490,6 +1505,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataLaneCarryoverAvailable
);
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
? null
: explicitCurrentCounterpartyOverridesFollowupEntity
? null
: followupSeed.counterparty ?? followupSeed.discoveryEntity;
const entityCandidates = entityResolutionSignal ? [] : [];
@ -1548,6 +1565,27 @@ export function buildAssistantMcpDiscoveryTurnInput(
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? currentTurnOrganizationScope ?? followupSeed.organization
: null;
if (
explicitCurrentCounterpartyCandidate &&
(valueFlowSignal || lifecycleSignal || metadataGroundedDocumentLaneApplicable || metadataGroundedMovementLaneApplicable)
) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
const candidate = entityCandidates[index];
if (!candidate || sameScopedName(candidate, explicitCurrentCounterpartyCandidate)) {
continue;
}
if (
(metadataLaneScopeHint && sameScopedName(candidate, metadataLaneScopeHint)) ||
(explicitOrganizationScope && sameScopedName(candidate, explicitOrganizationScope)) ||
(followupSeed.organization && sameScopedName(candidate, followupSeed.organization)) ||
(followupSeed.counterparty &&
!sameScopedName(followupSeed.counterparty, explicitCurrentCounterpartyCandidate) &&
sameScopedName(candidate, followupSeed.counterparty))
) {
entityCandidates.splice(index, 1);
}
}
}
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
if (entityCandidates[index] === explicitOrganizationScope) {

View File

@ -5177,6 +5177,8 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("РегистрНакопления.НДСЗаписиКнигиПокупок");
expect(plan.query).toContain("VAT_BOOK_SALES");
expect(plan.query).toContain("VAT_BOOK_PURCHASES");
expect(plan.query).toContain("ДАТАВРЕМЯ(2019, 12, 31, 23, 59, 59)");
expect(plan.query).not.toContain("ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период");
});
it("keeps inventory-on-hand phrasing in address lane", () => {

View File

@ -133,6 +133,50 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
});
it("prefers the explicit current-turn counterparty over stale organization-scoped carryover in net follow-up discovery", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Альтернатива Плюс"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: { counterparty: "Группа СВК" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
followupContext: {
previous_intent: "counterparty_activity_lifecycle",
previous_filters: {
organization: "ООО Альтернатива Плюс",
counterparty: "Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "organization",
previous_anchor_value: "ООО Альтернатива Плюс"
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.metadata_scope_hint).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual(["Группа СВК"]);
});
it("captures monthly aggregation as part of bidirectional value-flow meaning", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "какое нетто по деньгам с Группа СВК за 2020 год по месяцам: сколько получили и сколько заплатили помесячно?",