Стабилизировать маржинальность номенклатуры 1С

This commit is contained in:
2026-05-23 14:39:37 +03:00
parent 473cdc3a9b
commit a15f24f21d
14 changed files with 760 additions and 18 deletions
@@ -387,7 +387,7 @@ describe("inventory profitability selected-object regressions", () => {
expect(reply).toContain("\u041d\u0438\u0437\u043a\u0430\u044f \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f");
expect(reply).toContain("Item A");
expect(reply).toContain("Item B");
expect(reply).toContain("\u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
expect(reply).toContain("\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c \u0447\u0438\u0441\u0442\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u0438");
expect(reply).not.toContain("\u041e\u0421");
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
@@ -329,9 +329,9 @@ describe("address reply builders regressions", () => {
expect(firstLine).toContain("7.271,20");
expect(firstLine).toContain("Авант мебель");
expect(firstLine).not.toContain("3.677.454,14");
expect(result.text).toContain("встречных остатков");
expect(result.text).toContain("Встречная часть");
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
expect(result.text).toContain("Финансовое обеспечение заявки");
expect(result.text).not.toContain("Финансовое обеспечение заявки");
expect(result.text).not.toContain("договор/аналитика: ООО \\Альтернатива Плюс\\");
});
@@ -380,7 +380,7 @@ describe("address reply builders regressions", () => {
expect(firstLine).toContain("9.612.904,90");
expect(firstLine).toContain("Департамент капитального ремонта города Москвы.");
expect(firstLine).not.toContain("13.290.359,04");
expect(result.text).toContain("встречных остатков");
expect(result.text).toContain("Встречная часть");
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
});
@@ -525,4 +525,58 @@ describe("address reply builders regressions", () => {
expect(result?.text).toContain("Общее количество не свожу в один управленческий показатель");
expect(result?.text).toContain("Следующий шаг: могу раскрыть полный список");
});
it("answers cost-base evidence follow-up directly when margin ranking has sales without confirmed cost", () => {
const result = composeInventoryReply(
"inventory_margin_ranking_for_nomenclature",
[
{
amount: 120000,
quantity: 2,
item: "Рабочая станция",
period: "2020-05-20",
registrator: "Реализация"
} as any
],
{
userMessage: "покажи найденные строки себестоимостной базы",
periodFrom: "2020-05-01",
periodTo: "2020-05-31"
},
{
resolvePayablesAsOfDate: () => "2020-05-31",
buildInventoryOnHandAggregate: () => [],
uniqueStrings: (values: string[]) => Array.from(new Set(values)),
formatDateRu: (value: string) => value,
formatNumberWithDots: (value: number, fractionDigits = 0) => value.toFixed(fractionDigits),
formatMoneyRub: (value: number) => `${value}`,
isInventoryPurchaseMovement: () => false,
summarizeInventoryTraceRows: (rows: any[]) => ({
item: rows[0]?.item ?? null,
warehouses: [],
organizations: [],
counterparties: [],
documents: [],
firstPeriod: null,
lastPeriod: null,
totalAmount: 0
}),
formatInventoryTraceRows: () => [],
hasInventoryPurchaseDateActionFocus: () => false,
inventoryTraceDateLabel: () => "",
extractInventoryCounterpartyCandidates: () => [],
buildInventoryAgingByItemAggregate: () => [],
formatInventoryAgingRows: () => [],
isInventorySaleMovement: () => true
}
);
expect(result?.text.split("\n")[0]).toContain(
"подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено"
);
expect(result?.text).toContain("Есть реализация по 1 номенклатурной позиции");
expect(result?.text).toContain("Строк себестоимости реализации / себестоимостной базы для показа нет");
expect(result?.text).not.toContain("входящих денежных поступлений");
expect(result?.text).not.toContain("амортизац");
});
});
@@ -135,6 +135,42 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
});
it("keeps inventory margin-ranking exact reply over stale value-flow discovery candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"За период 2017 рейтинг прибыльности номенклатуры построить нельзя: нет подтвержденной себестоимости реализации.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
detected_intent: "inventory_margin_ranking_for_nomenclature",
selected_recipe: "address_inventory_margin_ranking_for_nomenclature_v1",
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover"
},
data_need_graph: {
business_fact_family: "value_flow",
clarification_gaps: []
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toContain("рейтинг прибыльности номенклатуры");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
});
it("lets a grounded business overview candidate override a semantically wrong exact address recipe", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Supplier and stock overlap was confirmed for 2020.",
@@ -183,6 +183,75 @@ describe("assistantRoutePolicy", () => {
expect(decision.livingMode).toBe("address_data");
});
it("keeps margin-ranking found-rows follow-up in address lane", () => {
const policy = buildPolicy();
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "покажи найденные строки себестоимостной базы",
effectiveAddressUserMessage: "покажи найденные строки себестоимостной базы",
followupContext: {
previous_intent: "inventory_margin_ranking_for_nomenclature",
root_intent: "inventory_margin_ranking_for_nomenclature",
previous_filters: {
organization: "ООО Альтернатива Плюс",
period_from: "2020-05-01",
period_to: "2020-05-31"
}
},
llmPreDecomposeMeta: {
applied: false,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
}
},
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.toolGateReason).toBe("followup_context_detected");
expect(decision.livingMode).toBe("address_data");
expect(decision.orchestrationContract?.hard_meta_mode).toBe(null);
expect(decision.orchestrationContract?.final_decision?.tool_gate_reason).toBe("followup_context_detected");
});
it("keeps margin-ranking period expansion follow-up in address lane", () => {
const policy = buildPolicy();
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "расширь до 2017 года",
effectiveAddressUserMessage: "расширь до 2017 года",
followupContext: {
previous_intent: "inventory_margin_ranking_for_nomenclature",
root_intent: "inventory_margin_ranking_for_nomenclature",
previous_filters: {
organization: "ООО Альтернатива Плюс",
period_from: "2020-05-01",
period_to: "2020-05-31"
}
},
llmPreDecomposeMeta: {
applied: false,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
}
},
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.toolGateReason).toBe("followup_context_detected");
expect(decision.livingMode).toBe("address_data");
expect(decision.orchestrationContract?.hard_meta_mode).toBe(null);
});
it("does not let deep session continuation override an exact VAT period route", () => {
const policy = buildPolicy({
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),