Стабилизировать маржинальность номенклатуры 1С
This commit is contained in:
@@ -162,6 +162,14 @@ function inventoryProfitabilityPeriodLabel(options: InventoryComposeOptions, dep
|
||||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
||||
}
|
||||
|
||||
function asksForInventoryCostBaseRows(userMessage: string | null | undefined): boolean {
|
||||
const text = String(userMessage ?? "").toLowerCase();
|
||||
if (!/(?:покажи|показать|выведи|вывести|дай|дать|раскрой|раскрыть|строк|строки|строку|баз)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:себестоимостн|себестоимост|себестоим|закупочн|закупк|90\.02|\b41\b|баз)/iu.test(text);
|
||||
}
|
||||
|
||||
interface InventoryMarginRankingEntry {
|
||||
item: string;
|
||||
revenue: number;
|
||||
@@ -631,7 +639,12 @@ export function composeInventoryReply(
|
||||
const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0);
|
||||
const totalSpread = totalRevenue - totalCostProxy;
|
||||
if (confirmedEntries.length === 0) {
|
||||
const lines: string[] = [`За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`];
|
||||
const costBaseRowsRequested = asksForInventoryCostBaseRows(options.userMessage);
|
||||
const lines: string[] = [
|
||||
costBaseRowsRequested && purchasesWithoutSales.length === 0
|
||||
? `За период ${periodLabel} подтвержденных строк себестоимостной базы по реализованной номенклатуре не найдено.`
|
||||
: `За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`
|
||||
];
|
||||
const findings: string[] = [];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
const salesCount = deps.formatNumberWithDots(salesWithoutCost.length);
|
||||
@@ -647,6 +660,9 @@ export function composeInventoryReply(
|
||||
);
|
||||
findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя.");
|
||||
}
|
||||
if (costBaseRowsRequested && purchasesWithoutSales.length === 0) {
|
||||
findings.push("Строк себестоимости реализации / себестоимостной базы для показа нет.");
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length);
|
||||
const purchaseItemPhrase =
|
||||
@@ -679,7 +695,7 @@ export function composeInventoryReply(
|
||||
appendInventoryBulletSection(lines, "Что можно сделать дальше:", nextActions);
|
||||
appendInventoryBulletSection(lines, "Граница ответа:", [
|
||||
"Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.",
|
||||
"Это не чистая прибыль компании и не замена закрытию месяца."
|
||||
"Это не показатель чистой прибыли и не замена закрытию месяца."
|
||||
]);
|
||||
return buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics(entries.length > 0 ? "medium" : "weak", false));
|
||||
}
|
||||
@@ -709,7 +725,7 @@ export function composeInventoryReply(
|
||||
}
|
||||
|
||||
const boundaryLines = [
|
||||
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не чистая прибыль компании.",
|
||||
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не показатель чистой прибыли.",
|
||||
"Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца."
|
||||
];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
|
||||
@@ -496,6 +496,31 @@ function hasExactBankOperationsAddressReply(
|
||||
);
|
||||
}
|
||||
|
||||
function hasInventoryMarginRankingAddressReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!toNonEmptyString(input.currentReply)) {
|
||||
return false;
|
||||
}
|
||||
if (hasMetadataDiscoveryPriority(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||
const capabilityId =
|
||||
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
|
||||
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id);
|
||||
return Boolean(
|
||||
detectedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
selectedRecipe === "address_inventory_margin_ranking_for_nomenclature_v1" ||
|
||||
capabilityId === "inventory_inventory_margin_ranking_for_nomenclature"
|
||||
);
|
||||
}
|
||||
|
||||
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
@@ -834,6 +859,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
entryPoint
|
||||
);
|
||||
const exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||
const inventoryMarginRankingAddressReply = hasInventoryMarginRankingAddressReply(input, entryPoint);
|
||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
@@ -917,6 +943,9 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
if (exactBankOperationsAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||
}
|
||||
if (inventoryMarginRankingAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_inventory_margin_ranking_address_reply");
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
@@ -952,6 +981,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
||||
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
||||
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
||||
!exactBankOperationsAddressReply &&
|
||||
!inventoryMarginRankingAddressReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
|
||||
@@ -397,6 +397,24 @@ export function createAssistantRoutePolicy(deps) {
|
||||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||
return hasRequestCue && hasTemporalCue;
|
||||
}
|
||||
function hasInventoryMarginRankingContinuationSignal(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const wantsFoundRows =
|
||||
/(?:покажи|показать|выведи|дай|раскрой|show|list)/iu.test(normalized) &&
|
||||
/(?:найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||
/(?:себестоимостн|реализац|марж|прибыл|номенклатур)/iu.test(normalized);
|
||||
const account41Not01 =
|
||||
/\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/(?:\bне\b|вместо|а\s+не|not|instead)/iu.test(normalized);
|
||||
const periodExpansion =
|
||||
/(?:расширь|расширить|возьми|давай|покажи|за|на|до|весь|год|квартал|месяц|expand|period)/iu.test(normalized) &&
|
||||
/(?:январ|феврал|март|апрел|ма[йяе]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||
return wantsFoundRows || account41Not01 || periodExpansion;
|
||||
}
|
||||
function hasOrganizationClarificationTextCue(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
@@ -565,6 +583,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage);
|
||||
const followupPreviousIntent = toNonEmptyString(followupContext?.previous_intent);
|
||||
const followupRootIntent = toNonEmptyString(followupContext?.root_intent);
|
||||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||
? followupContext.previous_filters
|
||||
: null;
|
||||
@@ -599,6 +618,16 @@ export function createAssistantRoutePolicy(deps) {
|
||||
hasShortInventoryObjectFollowupSignal(repairedRawUserMessage) ||
|
||||
hasShortInventoryObjectFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
|
||||
const protectedInventoryMarginRankingFollowup = Boolean(followupContext &&
|
||||
(followupPreviousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
followupRootIntent === "inventory_margin_ranking_for_nomenclature") &&
|
||||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
(hasInventoryMarginRankingContinuationSignal(rawUserMessage) ||
|
||||
hasInventoryMarginRankingContinuationSignal(repairedRawUserMessage) ||
|
||||
hasInventoryMarginRankingContinuationSignal(effectiveAddressUserMessage) ||
|
||||
hasInventoryMarginRankingContinuationSignal(repairedEffectiveAddressUserMessage)));
|
||||
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
|
||||
lastOrganizationClarificationDebug &&
|
||||
explicitOrganizationClarificationSelection &&
|
||||
@@ -656,6 +685,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||
!baseToolGatePreservesAddressLane &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!protectedInventoryShortFollowup &&
|
||||
!protectedInventoryMarginRankingFollowup &&
|
||||
!organizationClarificationContinuationDetected &&
|
||||
!routeCandidateOrganizationClarificationDetected);
|
||||
const lastAddressAssistantDebug = sessionItems
|
||||
@@ -1167,6 +1197,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||
hasShortDebtMirrorFollowupSignal(effectiveAddressUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedRawUserMessage) ||
|
||||
hasShortDebtMirrorFollowupSignal(repairedEffectiveAddressUserMessage) ||
|
||||
protectedInventoryMarginRankingFollowup ||
|
||||
inventoryRootRestatementFollowupDetected);
|
||||
const deepAnalysisPreferenceDetected = Boolean(hasDeepAnalysisPreferenceSignal(rawUserMessage) ||
|
||||
hasDeepAnalysisPreferenceSignal(repairedRawUserMessage) ||
|
||||
@@ -1204,7 +1235,9 @@ export function createAssistantRoutePolicy(deps) {
|
||||
(!llmContractIntent || llmContractIntent === "unknown"));
|
||||
const exactAddressIntentProtectedFromSemanticDeepHint = laneProtectionArbitration.exactAddressIntentProtectedFromSemanticDeepHint;
|
||||
const protectAddressLaneFromFallback = Boolean(
|
||||
laneProtectionArbitration.protectAddressLaneFromFallback || customerValueRankingAddressSignal
|
||||
laneProtectionArbitration.protectAddressLaneFromFallback ||
|
||||
customerValueRankingAddressSignal ||
|
||||
protectedInventoryMarginRankingFollowup
|
||||
);
|
||||
const vatExplainFollowupSignal = Boolean(followupContext &&
|
||||
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
|
||||
@@ -1285,7 +1318,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
|
||||
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
|
||||
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
|
||||
supportedAddressRouteCandidateDetected &&
|
||||
(supportedAddressRouteCandidateDetected || protectedInventoryMarginRankingFollowup) &&
|
||||
!deepAnalysisPreferenceDetected &&
|
||||
!unsupportedAddressIntentFallbackToDeep &&
|
||||
!deepAnalysisSignalFallbackToDeep &&
|
||||
@@ -1294,7 +1327,9 @@ export function createAssistantRoutePolicy(deps) {
|
||||
if (semanticAddressLaneRecovery) {
|
||||
runAddressLane = true;
|
||||
toolGateDecision = "run_address_lane";
|
||||
toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent
|
||||
toolGateReason = protectedInventoryMarginRankingFollowup
|
||||
? "followup_context_detected"
|
||||
: resolvedIntentResolution.intent !== "unknown" || llmContractIntent
|
||||
? "address_intent_resolver_detected"
|
||||
: "address_signal_detected";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user