Закрыть phase96 reviewed-route складских резервов и ликвидности
This commit is contained in:
+145
-35
@@ -283,6 +283,61 @@ const INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE = `
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Количество __ORDER_DIRECTION__
|
||||
`;
|
||||
const INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Списание.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Ссылка) КАК Регистратор,
|
||||
"Списание товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Склад) КАК Склад,
|
||||
Списание.СуммаДокумента КАК Сумма,
|
||||
Списание.Основание КАК Основание,
|
||||
Списание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеТоваров КАК Списание
|
||||
__WHERE_WRITE_OFF__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Оприходование.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Ссылка) КАК Регистратор,
|
||||
"Оприходование товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Склад) КАК Склад,
|
||||
Оприходование.СуммаДокумента КАК Сумма,
|
||||
Оприходование.Основание КАК Основание,
|
||||
Оприходование.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ОприходованиеТоваров КАК Оприходование
|
||||
__WHERE_RECEIPT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Инвентаризация.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Ссылка) КАК Регистратор,
|
||||
"Инвентаризация товаров на складе" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
Инвентаризация.ПричинаПроведенияИнвентаризации КАК Основание,
|
||||
Инвентаризация.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ИнвентаризацияТоваровНаСкладе КАК Инвентаризация
|
||||
__WHERE_INVENTORY_COUNT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Переоценка.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Ссылка) КАК Регистратор,
|
||||
"Переоценка товаров в рознице" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
"" КАК Основание,
|
||||
Переоценка.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПереоценкаТоваровВРознице КАК Переоценка
|
||||
__WHERE_REVALUATION__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Период __ORDER_DIRECTION__
|
||||
`;
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
@@ -933,6 +988,16 @@ const BASE_RECIPES = [
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_aging_by_purchase_date_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_quality_events_for_organization_v1",
|
||||
intent: "inventory_quality_events_for_organization",
|
||||
purpose: "Check posted inventory quality event documents: write-offs, stocktaking, receipt adjustments, and retail revaluation",
|
||||
required_filters: [],
|
||||
optional_filters: ["as_of_date", "period_from", "period_to", "organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "inventory_quality_events_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_open_contracts_confirmed_as_of_date_v1",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
@@ -1326,6 +1391,48 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
||||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
function buildWarehouseReferenceCondition(filters, fieldPaths) {
|
||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||
if (!warehouse) {
|
||||
return null;
|
||||
}
|
||||
const tokens = Array.from(new Set(warehouse
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["склад", "warehouse"].includes(token.toLowerCase()))));
|
||||
const effectiveTokens = tokens.length > 0 ? tokens : [warehouse];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = effectiveTokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function buildInventoryQualityDocumentWhereClause(filters, dateFieldPath, organizationFieldPath, warehouseFieldPath) {
|
||||
return buildWhereClause(filters, dateFieldPath, [
|
||||
`${dateFieldPath.replace(/\.Дата$/u, ".Проведен")} = ИСТИНА`,
|
||||
buildOrganizationReferenceCondition(filters, [organizationFieldPath]),
|
||||
buildWarehouseReferenceCondition(filters, [warehouseFieldPath])
|
||||
].filter((item) => Boolean(item)));
|
||||
}
|
||||
function buildInventoryQualityEventsQuery(filters, resolvedLimit) {
|
||||
return INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_WRITE_OFF__", buildInventoryQualityDocumentWhereClause(filters, "Списание.Дата", "Списание.Организация", "Списание.Склад"))
|
||||
.replace("__WHERE_RECEIPT__", buildInventoryQualityDocumentWhereClause(filters, "Оприходование.Дата", "Оприходование.Организация", "Оприходование.Склад"))
|
||||
.replace("__WHERE_INVENTORY_COUNT__", buildInventoryQualityDocumentWhereClause(filters, "Инвентаризация.Дата", "Инвентаризация.Организация", "Инвентаризация.Склад"))
|
||||
.replace("__WHERE_REVALUATION__", buildInventoryQualityDocumentWhereClause(filters, "Переоценка.Дата", "Переоценка.Организация", "Переоценка.Склад"))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
function buildInventoryItemReferenceCondition(filters, fieldPaths) {
|
||||
const item = typeof filters.item === "string" ? filters.item.trim() : "";
|
||||
if (!item) {
|
||||
@@ -1599,6 +1706,7 @@ function maxLimitForIntent(intent) {
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
intent === "inventory_quality_events_for_organization" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
@@ -1790,27 +1898,11 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: 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"
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: 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)
|
||||
@@ -1825,10 +1917,10 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
: recipe.query_template === "payables_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)
|
||||
@@ -1843,20 +1935,38 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
: recipe.query_template === "receivables_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("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
return {
|
||||
recipe,
|
||||
query,
|
||||
|
||||
+50
-1
@@ -393,6 +393,9 @@ function isVendorRiskBoundaryTurn(pilot) {
|
||||
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
|
||||
}
|
||||
function businessOverviewInventoryUnknownLabel(overview) {
|
||||
if (overview.inventory_quality_events) {
|
||||
return "рыночная ликвидационная стоимость и управленческий резерв склада";
|
||||
}
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
return "резервы/списания/ликвидационная стоимость склада";
|
||||
}
|
||||
@@ -621,6 +624,24 @@ function businessOverviewVendorProcurementQualityText(overview) {
|
||||
}
|
||||
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
|
||||
}
|
||||
function businessOverviewInventoryQualityEventsText(overview) {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
const period = quality.period_scope ?? "проверенное окно";
|
||||
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
|
||||
const eventWindow = quality.first_event_date && quality.latest_event_date
|
||||
? ` Окно найденных событий: ${quality.first_event_date} - ${quality.latest_event_date}.`
|
||||
: "";
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
return `Коротко: проверил складские документы списания, оприходования, инвентаризации и переоценки${organization} за ${period}; подтвержденных событий списания/корректировки/инвентаризации/переоценки не найдено. Это сильный отрицательный сигнал по доступным документам 1С, но не рыночная ликвидационная стоимость и не управленческий резерв под неликвиды.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_inventory_control_events_only") {
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний и оприходований/корректировок с суммой не найдено, но есть инвентаризации ${quality.inventory_count_rows} и переоценки ${quality.revaluation_rows}.${eventWindow} Это контрольные складские документы, а не подтвержденный резерв или рыночная ликвидационная оценка.`;
|
||||
}
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С по складским событиям, но не самостоятельная рыночная ликвидационная стоимость и не расчет управленческого резерва.`;
|
||||
}
|
||||
function headlineFor(mode, pilot) {
|
||||
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||||
@@ -653,6 +674,10 @@ function headlineFor(mode, pilot) {
|
||||
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
|
||||
}
|
||||
if (isInventoryReserveBoundaryTurn(pilot)) {
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
return inventoryQualityEventsText;
|
||||
}
|
||||
const inventoryBasis = overview.inventory_staleness_risk_proxy
|
||||
? "есть только складской staleness-risk proxy по найденным строкам"
|
||||
: overview.inventory_position || overview.inventory_turnover_proxy
|
||||
@@ -724,6 +749,9 @@ function headlineFor(mode, pilot) {
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy склада");
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
families.push("складские quality-события");
|
||||
}
|
||||
const unknownFamilies = overview.accounting_financial_result
|
||||
? ["аудированная/юридически подтвержденная прибыль"]
|
||||
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
@@ -946,6 +974,9 @@ function buildMustNotClaim(pilot) {
|
||||
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
|
||||
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
|
||||
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
claims.push("Do not present reviewed inventory quality events as confirmed obsolete stock, reserve policy, market liquidation value, management reserve, or full inventory health.");
|
||||
}
|
||||
if (pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
|
||||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
@@ -1436,6 +1467,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
||||
const proxy = overview.inventory_staleness_risk_proxy;
|
||||
lines.push(`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`);
|
||||
}
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
lines.push(inventoryQualityEventsText.replace(/^Коротко:\s*/u, ""));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
function businessOverviewCashSynthesisLine(overview) {
|
||||
@@ -1603,6 +1638,15 @@ function businessOverviewRiskSynthesisLine(overview) {
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
signals.push(`staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_age_days} дн.`);
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
signals.push("складские quality-события: документы списания, оприходования, инвентаризации и переоценки проверены, подтвержденных событий не найдено");
|
||||
}
|
||||
else {
|
||||
signals.push(`складские quality-события: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}`);
|
||||
}
|
||||
}
|
||||
return signals.length > 0
|
||||
? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.`
|
||||
: null;
|
||||
@@ -1616,7 +1660,8 @@ function businessOverviewExecutiveVerdictLine(overview) {
|
||||
overview.debt_staleness_risk_proxy ||
|
||||
overview.inventory_position ||
|
||||
overview.inventory_turnover_proxy ||
|
||||
overview.inventory_staleness_risk_proxy);
|
||||
overview.inventory_staleness_risk_proxy ||
|
||||
overview.inventory_quality_events);
|
||||
const hasOperationalProfileSignal = Boolean(overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile);
|
||||
const hasExtraSignals = hasTaxDebtInventorySignals || hasOperationalProfileSignal;
|
||||
if (!hasCash && !hasExtraSignals) {
|
||||
@@ -1750,6 +1795,10 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
||||
if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_quality_events");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_inventory_quality_events_${pilot.derived_business_overview.inventory_quality_events.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_missing_proof_ledger");
|
||||
}
|
||||
|
||||
+144
-9
@@ -211,6 +211,16 @@ function shouldRunDebtDueDateAgingProbe(planner) {
|
||||
.join(" ");
|
||||
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
|
||||
}
|
||||
function shouldRunInventoryQualityEventsProbe(planner) {
|
||||
const actionFamily = toNonEmptyString(planner.data_need_graph?.action_family);
|
||||
const turnActionFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family);
|
||||
const proofExpectation = toNonEmptyString(planner.data_need_graph?.proof_expectation);
|
||||
const combined = [actionFamily, turnActionFamily, unsupportedFamily, proofExpectation]
|
||||
.filter((item) => Boolean(item))
|
||||
.join(" ");
|
||||
return /(?:inventory_reserve|reserve_liquidation|liquidation|write[-_ ]?off|obsolete|obsolescence|inventory_reserve_liquidation_quality|резерв|списан|ликвидац|неликвид|обесцен)/iu.test(combined);
|
||||
}
|
||||
function buildBusinessOverviewInventoryFilters(planner) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
@@ -3021,6 +3031,69 @@ function deriveBusinessOverviewInventoryStalenessRiskProxy(input) {
|
||||
inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
function rowInventoryQualityEventType(row) {
|
||||
return rowTextValue(row, ["ТипСобытия", "EventType", "event_type", "Регистратор", "Registrator", "registrator"]) ?? "";
|
||||
}
|
||||
function deriveBusinessOverviewInventoryQualityEvents(input) {
|
||||
const result = input.inventoryQualityEventsResult;
|
||||
if (!result || result.error) {
|
||||
return null;
|
||||
}
|
||||
let writeoffRows = 0;
|
||||
let writeoffAmount = 0;
|
||||
let receiptAdjustmentRows = 0;
|
||||
let receiptAdjustmentAmount = 0;
|
||||
let inventoryCountRows = 0;
|
||||
let revaluationRows = 0;
|
||||
const eventDates = [];
|
||||
for (const row of result.rows) {
|
||||
const eventType = rowInventoryQualityEventType(row);
|
||||
const amount = rowAmountValue(row) ?? 0;
|
||||
const date = rowDateValue(row);
|
||||
if (date) {
|
||||
eventDates.push(date);
|
||||
}
|
||||
if (/списан|write[-_ ]?off/iu.test(eventType)) {
|
||||
writeoffRows += 1;
|
||||
writeoffAmount += amount;
|
||||
continue;
|
||||
}
|
||||
if (/оприход|receipt|positive/i.test(eventType)) {
|
||||
receiptAdjustmentRows += 1;
|
||||
receiptAdjustmentAmount += amount;
|
||||
continue;
|
||||
}
|
||||
if (/инвентаризац|stocktaking|inventory count/i.test(eventType)) {
|
||||
inventoryCountRows += 1;
|
||||
continue;
|
||||
}
|
||||
if (/переоцен|revaluation/i.test(eventType)) {
|
||||
revaluationRows += 1;
|
||||
}
|
||||
}
|
||||
const sortedDates = eventDates.sort((left, right) => left.localeCompare(right));
|
||||
const evidenceStatus = writeoffRows > 0 || receiptAdjustmentRows > 0
|
||||
? "reviewed_writeoff_or_adjustment_events_found"
|
||||
: inventoryCountRows > 0 || revaluationRows > 0
|
||||
? "reviewed_inventory_control_events_only"
|
||||
: "reviewed_no_quality_events_found";
|
||||
return {
|
||||
period_scope: input.periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
writeoff_rows: writeoffRows,
|
||||
writeoff_amount: writeoffAmount,
|
||||
writeoff_amount_human_ru: formatAmountHumanRu(writeoffAmount),
|
||||
receipt_adjustment_rows: receiptAdjustmentRows,
|
||||
receipt_adjustment_amount: receiptAdjustmentAmount,
|
||||
receipt_adjustment_amount_human_ru: formatAmountHumanRu(receiptAdjustmentAmount),
|
||||
inventory_count_rows: inventoryCountRows,
|
||||
revaluation_rows: revaluationRows,
|
||||
first_event_date: sortedDates[0] ?? null,
|
||||
latest_event_date: sortedDates[sortedDates.length - 1] ?? null,
|
||||
evidence_status: evidenceStatus,
|
||||
inference_basis: "inventory_quality_documents_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
function deriveBusinessOverviewVendorProcurementQuality(input) {
|
||||
if (!input.rankedOutgoing ||
|
||||
input.rankedOutgoing.ranked_values.length <= 0 ||
|
||||
@@ -3109,10 +3182,10 @@ function buildBusinessOverviewMissingProofFamilies(input) {
|
||||
must_not_claim: "confirmed_overdue_debt_credit_risk_or_due_date_aging"
|
||||
});
|
||||
}
|
||||
if (missing.has("inventory_position") ||
|
||||
if ((missing.has("inventory_position") ||
|
||||
missing.has("inventory_turnover_quality") ||
|
||||
missing.has("inventory_liquidity_quality") ||
|
||||
missing.has("inventory_reserve_liquidation_quality")) {
|
||||
missing.has("inventory_reserve_liquidation_quality")) && !input.inventoryQualityEvents) {
|
||||
pushUnique({
|
||||
family: "inventory_reserve_liquidation_quality",
|
||||
current_status: input.inventoryStalenessRiskProxy
|
||||
@@ -3195,6 +3268,10 @@ function deriveBusinessOverview(input) {
|
||||
inventoryPosition,
|
||||
inventoryTurnoverProxy
|
||||
});
|
||||
const inventoryQualityEvents = deriveBusinessOverviewInventoryQualityEvents({
|
||||
inventoryQualityEventsResult: input.inventoryQualityEventsResult,
|
||||
periodScope: input.periodScope
|
||||
});
|
||||
const vendorProcurementQuality = deriveBusinessOverviewVendorProcurementQuality({
|
||||
rankedOutgoing,
|
||||
outgoing,
|
||||
@@ -3219,6 +3296,7 @@ function deriveBusinessOverview(input) {
|
||||
Boolean(inventoryPosition),
|
||||
Boolean(inventoryTurnoverProxy),
|
||||
Boolean(inventoryStalenessRiskProxy),
|
||||
Boolean(inventoryQualityEvents),
|
||||
Boolean(vendorProcurementQuality)
|
||||
].filter(Boolean).length;
|
||||
if (checkedSignalCount <= 0) {
|
||||
@@ -3232,11 +3310,13 @@ function deriveBusinessOverview(input) {
|
||||
debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
taxPosition ? null : "tax_position",
|
||||
inventoryPosition
|
||||
? inventoryStalenessRiskProxy
|
||||
? "inventory_reserve_liquidation_quality"
|
||||
: inventoryTurnoverProxy
|
||||
? "inventory_liquidity_quality"
|
||||
: "inventory_turnover_quality"
|
||||
? inventoryQualityEvents
|
||||
? null
|
||||
: inventoryStalenessRiskProxy
|
||||
? "inventory_reserve_liquidation_quality"
|
||||
: inventoryTurnoverProxy
|
||||
? "inventory_liquidity_quality"
|
||||
: "inventory_turnover_quality"
|
||||
: "inventory_position",
|
||||
inventoryPosition?.aging_signal ? null : "inventory_aging_quality"
|
||||
].filter((item) => Boolean(item));
|
||||
@@ -3250,6 +3330,7 @@ function deriveBusinessOverview(input) {
|
||||
inventoryPosition,
|
||||
inventoryTurnoverProxy,
|
||||
inventoryStalenessRiskProxy,
|
||||
inventoryQualityEvents,
|
||||
vendorProcurementQuality,
|
||||
hasSupplierConcentrationSignal: (rankedOutgoing?.ranked_values.length ?? 0) > 0
|
||||
});
|
||||
@@ -3275,6 +3356,7 @@ function deriveBusinessOverview(input) {
|
||||
inventory_position: inventoryPosition,
|
||||
inventory_turnover_proxy: inventoryTurnoverProxy,
|
||||
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
||||
inventory_quality_events: inventoryQualityEvents,
|
||||
document_activity_profile: documentActivityProfile,
|
||||
counterparty_profile: counterpartyProfile,
|
||||
contract_usage_profile: contractUsageProfile,
|
||||
@@ -3283,7 +3365,7 @@ function deriveBusinessOverview(input) {
|
||||
checked_signal_count: checkedSignalCount,
|
||||
missing_signal_families: missingSignalFamilies,
|
||||
missing_proof_families: missingProofFamilies,
|
||||
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
|
||||
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || inventoryQualityEvents || accountingFinancialResult
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: debtOpenSettlementQuality || debtDueDateAging
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
@@ -3343,6 +3425,9 @@ function summarizeBusinessOverviewRows(input) {
|
||||
if (input.inventoryAgingResult && !input.inventoryAgingResult.error) {
|
||||
parts.push(`${input.inventoryAgingResult.fetched_rows} inventory aging rows fetched, ${input.inventoryAgingResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.inventoryQualityEventsResult && !input.inventoryQualityEventsResult.error) {
|
||||
parts.push(`${input.inventoryQualityEventsResult.fetched_rows} inventory quality-event rows fetched, ${input.inventoryQualityEventsResult.matched_rows} matched`);
|
||||
}
|
||||
return parts.length > 0 ? parts.join("; ") : null;
|
||||
}
|
||||
function buildBusinessOverviewConfirmedFacts(derived) {
|
||||
@@ -3511,6 +3596,18 @@ function buildBusinessOverviewConfirmedFacts(derived) {
|
||||
const proxy = derived.inventory_staleness_risk_proxy;
|
||||
facts.push(`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`);
|
||||
}
|
||||
if (derived.inventory_quality_events) {
|
||||
const quality = derived.inventory_quality_events;
|
||||
const eventWindow = quality.first_event_date && quality.latest_event_date
|
||||
? ` Окно найденных событий: ${quality.first_event_date} — ${quality.latest_event_date}.`
|
||||
: "";
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
facts.push(`Reviewed inventory quality route проверил складские документы списания, оприходования, инвентаризации и переоценки${period}: подтвержденных событий не найдено. Это проверенный отрицательный результат по доступным документам, но не рыночная ликвидационная оценка и не управленческий резерв.`);
|
||||
}
|
||||
else {
|
||||
facts.push(`Reviewed inventory quality route проверил складские документы${period}: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С, но не самостоятельная рыночная ликвидационная стоимость.`);
|
||||
}
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
function buildBusinessOverviewInferredFacts(derived) {
|
||||
@@ -4218,6 +4315,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
let contractUsageProfileResult = null;
|
||||
let inventoryOnHandResult = null;
|
||||
let inventoryAgingResult = null;
|
||||
let inventoryQualityEventsResult = null;
|
||||
const valueFilters = buildValueFlowFilters(planner);
|
||||
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
||||
@@ -4227,6 +4325,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
|
||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||
const inventoryQualityEventsProbeEnabled = shouldRunInventoryQualityEventsProbe(planner);
|
||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
||||
const incomingSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("customer_revenue_and_payments", valueFilters);
|
||||
@@ -4262,6 +4361,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const inventoryAgingSelection = inventoryFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_aging_by_purchase_date", inventoryFilters)
|
||||
: null;
|
||||
const inventoryQualityEventsSelection = inventoryQualityEventsProbeEnabled
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_quality_events_for_organization", inventoryFilters ?? buildBusinessOverviewProfileFilters(planner))
|
||||
: null;
|
||||
if (!incomingSelection.selected_recipe || !outgoingSelection.selected_recipe || !lifecycleSelection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_recipe_not_available");
|
||||
const missing = [
|
||||
@@ -4389,6 +4491,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview inventory-position probe requires an executable inventory on-hand as-of-date recipe");
|
||||
}
|
||||
if (inventoryQualityEventsSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_recipe_selected");
|
||||
}
|
||||
else if (!inventoryQualityEventsProbeEnabled) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_probe_skipped_without_boundary_need");
|
||||
}
|
||||
else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview inventory quality probe requires an executable inventory quality-events recipe");
|
||||
}
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id === "query_movements") {
|
||||
const incomingExecution = await executeCoverageAwareValueFlowQuery({
|
||||
@@ -4622,6 +4734,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
});
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, contractUsageProfileResult));
|
||||
}
|
||||
if (inventoryQualityEventsSelection?.selected_recipe) {
|
||||
const inventoryQualityEventsFilters = inventoryFilters ?? buildBusinessOverviewProfileFilters(planner);
|
||||
const inventoryQualityEventsPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryQualityEventsSelection.selected_recipe, inventoryQualityEventsFilters);
|
||||
inventoryQualityEventsResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: inventoryQualityEventsPlan.query,
|
||||
limit: inventoryQualityEventsPlan.limit,
|
||||
account_scope: inventoryQualityEventsPlan.account_scope
|
||||
});
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryQualityEventsResult));
|
||||
}
|
||||
if (lifecycleResult.error) {
|
||||
pushUnique(queryLimitations, lifecycleResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error");
|
||||
@@ -4657,6 +4779,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
else if (contractUsageProfileResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_query_mcp_executed");
|
||||
}
|
||||
if (inventoryQualityEventsResult?.error) {
|
||||
pushUnique(queryLimitations, inventoryQualityEventsResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_query_mcp_error");
|
||||
}
|
||||
else if (inventoryQualityEventsResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_query_mcp_executed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
skippedPrimitives.push(step.primitive_id);
|
||||
@@ -4679,6 +4808,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
debtAsOfDate,
|
||||
inventoryOnHandResult,
|
||||
inventoryAgingResult,
|
||||
inventoryQualityEventsResult,
|
||||
inventoryAsOfDate,
|
||||
organizationScope,
|
||||
periodScope: dateScope
|
||||
@@ -4744,6 +4874,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
if (derivedBusinessOverview.inventory_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.inventory_quality_events) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_quality_events_from_reviewed_rows");
|
||||
pushReason(reasonCodes, `pilot_derived_business_overview_inventory_quality_events_${derivedBusinessOverview.inventory_quality_events.evidence_status}`);
|
||||
}
|
||||
if (derivedBusinessOverview.missing_proof_families.length > 0) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_missing_proof_families_recorded");
|
||||
}
|
||||
@@ -4763,7 +4897,8 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
inventoryOnHandResult,
|
||||
inventoryAgingResult
|
||||
inventoryAgingResult,
|
||||
inventoryQualityEventsResult
|
||||
});
|
||||
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
|
||||
plan: planner.discovery_plan,
|
||||
|
||||
+8
@@ -830,10 +830,18 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
}
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя.");
|
||||
if (inventoryQualityEvents) {
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
...toStringList(draft.limitation_lines)
|
||||
|
||||
@@ -296,6 +296,62 @@ const INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE = `
|
||||
Количество __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Списание.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Ссылка) КАК Регистратор,
|
||||
"Списание товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Склад) КАК Склад,
|
||||
Списание.СуммаДокумента КАК Сумма,
|
||||
Списание.Основание КАК Основание,
|
||||
Списание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеТоваров КАК Списание
|
||||
__WHERE_WRITE_OFF__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Оприходование.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Ссылка) КАК Регистратор,
|
||||
"Оприходование товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Склад) КАК Склад,
|
||||
Оприходование.СуммаДокумента КАК Сумма,
|
||||
Оприходование.Основание КАК Основание,
|
||||
Оприходование.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ОприходованиеТоваров КАК Оприходование
|
||||
__WHERE_RECEIPT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Инвентаризация.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Ссылка) КАК Регистратор,
|
||||
"Инвентаризация товаров на складе" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
Инвентаризация.ПричинаПроведенияИнвентаризации КАК Основание,
|
||||
Инвентаризация.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ИнвентаризацияТоваровНаСкладе КАК Инвентаризация
|
||||
__WHERE_INVENTORY_COUNT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Переоценка.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Ссылка) КАК Регистратор,
|
||||
"Переоценка товаров в рознице" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
"" КАК Основание,
|
||||
Переоценка.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПереоценкаТоваровВРознице КАК Переоценка
|
||||
__WHERE_REVALUATION__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Период __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
@@ -958,6 +1014,16 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_aging_by_purchase_date_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_quality_events_for_organization_v1",
|
||||
intent: "inventory_quality_events_for_organization",
|
||||
purpose: "Check posted inventory quality event documents: write-offs, stocktaking, receipt adjustments, and retail revaluation",
|
||||
required_filters: [],
|
||||
optional_filters: ["as_of_date", "period_from", "period_to", "organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "inventory_quality_events_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_open_contracts_confirmed_as_of_date_v1",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
@@ -1432,6 +1498,77 @@ function buildInventoryMovementQuery(
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildWarehouseReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||
if (!warehouse) {
|
||||
return null;
|
||||
}
|
||||
const tokens = Array.from(
|
||||
new Set(
|
||||
warehouse
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["склад", "warehouse"].includes(token.toLowerCase()))
|
||||
)
|
||||
);
|
||||
const effectiveTokens = tokens.length > 0 ? tokens : [warehouse];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = effectiveTokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function buildInventoryQualityDocumentWhereClause(
|
||||
filters: AddressFilterSet,
|
||||
dateFieldPath: string,
|
||||
organizationFieldPath: string,
|
||||
warehouseFieldPath: string
|
||||
): string {
|
||||
return buildWhereClause(filters, dateFieldPath, [
|
||||
`${dateFieldPath.replace(/\.Дата$/u, ".Проведен")} = ИСТИНА`,
|
||||
buildOrganizationReferenceCondition(filters, [organizationFieldPath]),
|
||||
buildWarehouseReferenceCondition(filters, [warehouseFieldPath])
|
||||
].filter((item): item is string => Boolean(item)));
|
||||
}
|
||||
|
||||
function buildInventoryQualityEventsQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
return INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replace(
|
||||
"__WHERE_WRITE_OFF__",
|
||||
buildInventoryQualityDocumentWhereClause(filters, "Списание.Дата", "Списание.Организация", "Списание.Склад")
|
||||
)
|
||||
.replace(
|
||||
"__WHERE_RECEIPT__",
|
||||
buildInventoryQualityDocumentWhereClause(filters, "Оприходование.Дата", "Оприходование.Организация", "Оприходование.Склад")
|
||||
)
|
||||
.replace(
|
||||
"__WHERE_INVENTORY_COUNT__",
|
||||
buildInventoryQualityDocumentWhereClause(
|
||||
filters,
|
||||
"Инвентаризация.Дата",
|
||||
"Инвентаризация.Организация",
|
||||
"Инвентаризация.Склад"
|
||||
)
|
||||
)
|
||||
.replace(
|
||||
"__WHERE_REVALUATION__",
|
||||
buildInventoryQualityDocumentWhereClause(filters, "Переоценка.Дата", "Переоценка.Организация", "Переоценка.Склад")
|
||||
)
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildInventoryItemReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const item = typeof filters.item === "string" ? filters.item.trim() : "";
|
||||
if (!item) {
|
||||
@@ -1783,6 +1920,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
intent === "inventory_quality_events_for_organization" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
@@ -2037,6 +2175,8 @@ export function buildAddressRecipePlan(
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: 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"
|
||||
|
||||
@@ -510,6 +510,9 @@ function isVendorRiskBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionCont
|
||||
}
|
||||
|
||||
function businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string {
|
||||
if (overview.inventory_quality_events) {
|
||||
return "рыночная ликвидационная стоимость и управленческий резерв склада";
|
||||
}
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
return "резервы/списания/ликвидационная стоимость склада";
|
||||
}
|
||||
@@ -764,6 +767,26 @@ function businessOverviewVendorProcurementQualityText(overview: BusinessOverview
|
||||
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
|
||||
}
|
||||
|
||||
function businessOverviewInventoryQualityEventsText(overview: BusinessOverview): string | null {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
const period = quality.period_scope ?? "проверенное окно";
|
||||
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
|
||||
const eventWindow =
|
||||
quality.first_event_date && quality.latest_event_date
|
||||
? ` Окно найденных событий: ${quality.first_event_date} - ${quality.latest_event_date}.`
|
||||
: "";
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
return `Коротко: проверил складские документы списания, оприходования, инвентаризации и переоценки${organization} за ${period}; подтвержденных событий списания/корректировки/инвентаризации/переоценки не найдено. Это сильный отрицательный сигнал по доступным документам 1С, но не рыночная ликвидационная стоимость и не управленческий резерв под неликвиды.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_inventory_control_events_only") {
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний и оприходований/корректировок с суммой не найдено, но есть инвентаризации ${quality.inventory_count_rows} и переоценки ${quality.revaluation_rows}.${eventWindow} Это контрольные складские документы, а не подтвержденный резерв или рыночная ликвидационная оценка.`;
|
||||
}
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С по складским событиям, но не самостоятельная рыночная ликвидационная стоимость и не расчет управленческого резерва.`;
|
||||
}
|
||||
|
||||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const askedMonthlyBreakdown =
|
||||
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
@@ -797,6 +820,10 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
|
||||
}
|
||||
if (isInventoryReserveBoundaryTurn(pilot)) {
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
return inventoryQualityEventsText;
|
||||
}
|
||||
const inventoryBasis = overview.inventory_staleness_risk_proxy
|
||||
? "есть только складской staleness-risk proxy по найденным строкам"
|
||||
: overview.inventory_position || overview.inventory_turnover_proxy
|
||||
@@ -870,6 +897,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy склада");
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
families.push("складские quality-события");
|
||||
}
|
||||
const unknownFamilies = overview.accounting_financial_result
|
||||
? ["аудированная/юридически подтвержденная прибыль"]
|
||||
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
@@ -1101,6 +1131,9 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
||||
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
|
||||
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
|
||||
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
claims.push("Do not present reviewed inventory quality events as confirmed obsolete stock, reserve policy, market liquidation value, management reserve, or full inventory health.");
|
||||
}
|
||||
if (
|
||||
pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
|
||||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)
|
||||
@@ -1676,6 +1709,10 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
||||
`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`
|
||||
);
|
||||
}
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
lines.push(inventoryQualityEventsText.replace(/^Коротко:\s*/u, ""));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -1858,6 +1895,16 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
||||
`staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_age_days} дн.`
|
||||
);
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
signals.push("складские quality-события: документы списания, оприходования, инвентаризации и переоценки проверены, подтвержденных событий не найдено");
|
||||
} else {
|
||||
signals.push(
|
||||
`складские quality-события: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return signals.length > 0
|
||||
? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.`
|
||||
: null;
|
||||
@@ -1873,7 +1920,8 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin
|
||||
overview.debt_staleness_risk_proxy ||
|
||||
overview.inventory_position ||
|
||||
overview.inventory_turnover_proxy ||
|
||||
overview.inventory_staleness_risk_proxy
|
||||
overview.inventory_staleness_risk_proxy ||
|
||||
overview.inventory_quality_events
|
||||
);
|
||||
const hasOperationalProfileSignal = Boolean(
|
||||
overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile
|
||||
@@ -2020,6 +2068,10 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
||||
if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_quality_events");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_inventory_quality_events_${pilot.derived_business_overview.inventory_quality_events.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_missing_proof_ledger");
|
||||
}
|
||||
|
||||
@@ -265,6 +265,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
|
||||
inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
||||
inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
||||
inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
|
||||
inventory_quality_events: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents | null;
|
||||
document_activity_profile: AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile | null;
|
||||
counterparty_profile: AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null;
|
||||
contract_usage_profile: AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null;
|
||||
@@ -521,6 +522,26 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessR
|
||||
inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents {
|
||||
period_scope: string | null;
|
||||
rows_matched: number;
|
||||
writeoff_rows: number;
|
||||
writeoff_amount: number;
|
||||
writeoff_amount_human_ru: string;
|
||||
receipt_adjustment_rows: number;
|
||||
receipt_adjustment_amount: number;
|
||||
receipt_adjustment_amount_human_ru: string;
|
||||
inventory_count_rows: number;
|
||||
revaluation_rows: number;
|
||||
first_event_date: string | null;
|
||||
latest_event_date: string | null;
|
||||
evidence_status:
|
||||
| "reviewed_no_quality_events_found"
|
||||
| "reviewed_writeoff_or_adjustment_events_found"
|
||||
| "reviewed_inventory_control_events_only";
|
||||
inference_basis: "inventory_quality_documents_confirmed_1c_rows";
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryDerivedMetadataSurface {
|
||||
metadata_scope: string | null;
|
||||
requested_meta_types: string[];
|
||||
@@ -829,6 +850,17 @@ function shouldRunDebtDueDateAgingProbe(planner: AssistantMcpDiscoveryPlannerCon
|
||||
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
|
||||
}
|
||||
|
||||
function shouldRunInventoryQualityEventsProbe(planner: AssistantMcpDiscoveryPlannerContract): boolean {
|
||||
const actionFamily = toNonEmptyString(planner.data_need_graph?.action_family);
|
||||
const turnActionFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family);
|
||||
const proofExpectation = toNonEmptyString(planner.data_need_graph?.proof_expectation);
|
||||
const combined = [actionFamily, turnActionFamily, unsupportedFamily, proofExpectation]
|
||||
.filter((item): item is string => Boolean(item))
|
||||
.join(" ");
|
||||
return /(?:inventory_reserve|reserve_liquidation|liquidation|write[-_ ]?off|obsolete|obsolescence|inventory_reserve_liquidation_quality|резерв|списан|ликвидац|неликвид|обесцен)/iu.test(combined);
|
||||
}
|
||||
|
||||
function buildBusinessOverviewInventoryFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
@@ -4148,6 +4180,79 @@ function deriveBusinessOverviewInventoryStalenessRiskProxy(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function rowInventoryQualityEventType(row: Record<string, unknown>): string {
|
||||
return rowTextValue(row, ["ТипСобытия", "EventType", "event_type", "Регистратор", "Registrator", "registrator"]) ?? "";
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewInventoryQualityEvents(input: {
|
||||
inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null;
|
||||
periodScope: string | null;
|
||||
}): AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents | null {
|
||||
const result = input.inventoryQualityEventsResult;
|
||||
if (!result || result.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let writeoffRows = 0;
|
||||
let writeoffAmount = 0;
|
||||
let receiptAdjustmentRows = 0;
|
||||
let receiptAdjustmentAmount = 0;
|
||||
let inventoryCountRows = 0;
|
||||
let revaluationRows = 0;
|
||||
const eventDates: string[] = [];
|
||||
|
||||
for (const row of result.rows) {
|
||||
const eventType = rowInventoryQualityEventType(row);
|
||||
const amount = rowAmountValue(row) ?? 0;
|
||||
const date = rowDateValue(row);
|
||||
if (date) {
|
||||
eventDates.push(date);
|
||||
}
|
||||
if (/списан|write[-_ ]?off/iu.test(eventType)) {
|
||||
writeoffRows += 1;
|
||||
writeoffAmount += amount;
|
||||
continue;
|
||||
}
|
||||
if (/оприход|receipt|positive/i.test(eventType)) {
|
||||
receiptAdjustmentRows += 1;
|
||||
receiptAdjustmentAmount += amount;
|
||||
continue;
|
||||
}
|
||||
if (/инвентаризац|stocktaking|inventory count/i.test(eventType)) {
|
||||
inventoryCountRows += 1;
|
||||
continue;
|
||||
}
|
||||
if (/переоцен|revaluation/i.test(eventType)) {
|
||||
revaluationRows += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const sortedDates = eventDates.sort((left, right) => left.localeCompare(right));
|
||||
const evidenceStatus: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents["evidence_status"] =
|
||||
writeoffRows > 0 || receiptAdjustmentRows > 0
|
||||
? "reviewed_writeoff_or_adjustment_events_found"
|
||||
: inventoryCountRows > 0 || revaluationRows > 0
|
||||
? "reviewed_inventory_control_events_only"
|
||||
: "reviewed_no_quality_events_found";
|
||||
|
||||
return {
|
||||
period_scope: input.periodScope,
|
||||
rows_matched: result.matched_rows,
|
||||
writeoff_rows: writeoffRows,
|
||||
writeoff_amount: writeoffAmount,
|
||||
writeoff_amount_human_ru: formatAmountHumanRu(writeoffAmount),
|
||||
receipt_adjustment_rows: receiptAdjustmentRows,
|
||||
receipt_adjustment_amount: receiptAdjustmentAmount,
|
||||
receipt_adjustment_amount_human_ru: formatAmountHumanRu(receiptAdjustmentAmount),
|
||||
inventory_count_rows: inventoryCountRows,
|
||||
revaluation_rows: revaluationRows,
|
||||
first_event_date: sortedDates[0] ?? null,
|
||||
latest_event_date: sortedDates[sortedDates.length - 1] ?? null,
|
||||
evidence_status: evidenceStatus,
|
||||
inference_basis: "inventory_quality_documents_confirmed_1c_rows"
|
||||
};
|
||||
}
|
||||
|
||||
function deriveBusinessOverviewVendorProcurementQuality(input: {
|
||||
rankedOutgoing: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
|
||||
outgoing: AssistantMcpDiscoveryValueFlowSideSummary;
|
||||
@@ -4226,6 +4331,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
|
||||
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
|
||||
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
|
||||
inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
|
||||
inventoryQualityEvents: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryQualityEvents | null;
|
||||
vendorProcurementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null;
|
||||
hasSupplierConcentrationSignal: boolean;
|
||||
}): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily[] {
|
||||
@@ -4267,12 +4373,12 @@ function buildBusinessOverviewMissingProofFamilies(input: {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
if ((
|
||||
missing.has("inventory_position") ||
|
||||
missing.has("inventory_turnover_quality") ||
|
||||
missing.has("inventory_liquidity_quality") ||
|
||||
missing.has("inventory_reserve_liquidation_quality")
|
||||
) {
|
||||
) && !input.inventoryQualityEvents) {
|
||||
pushUnique({
|
||||
family: "inventory_reserve_liquidation_quality",
|
||||
current_status: input.inventoryStalenessRiskProxy
|
||||
@@ -4322,6 +4428,7 @@ function deriveBusinessOverview(input: {
|
||||
debtAsOfDate: string | null;
|
||||
inventoryOnHandResult: AddressMcpQueryExecutorResult | null;
|
||||
inventoryAgingResult: AddressMcpQueryExecutorResult | null;
|
||||
inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null;
|
||||
inventoryAsOfDate: string | null;
|
||||
organizationScope: string | null;
|
||||
periodScope: string | null;
|
||||
@@ -4390,6 +4497,10 @@ function deriveBusinessOverview(input: {
|
||||
inventoryPosition,
|
||||
inventoryTurnoverProxy
|
||||
});
|
||||
const inventoryQualityEvents = deriveBusinessOverviewInventoryQualityEvents({
|
||||
inventoryQualityEventsResult: input.inventoryQualityEventsResult,
|
||||
periodScope: input.periodScope
|
||||
});
|
||||
const vendorProcurementQuality = deriveBusinessOverviewVendorProcurementQuality({
|
||||
rankedOutgoing,
|
||||
outgoing,
|
||||
@@ -4414,6 +4525,7 @@ function deriveBusinessOverview(input: {
|
||||
Boolean(inventoryPosition),
|
||||
Boolean(inventoryTurnoverProxy),
|
||||
Boolean(inventoryStalenessRiskProxy),
|
||||
Boolean(inventoryQualityEvents),
|
||||
Boolean(vendorProcurementQuality)
|
||||
].filter(Boolean).length;
|
||||
if (checkedSignalCount <= 0) {
|
||||
@@ -4430,7 +4542,9 @@ function deriveBusinessOverview(input: {
|
||||
debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
|
||||
taxPosition ? null : "tax_position",
|
||||
inventoryPosition
|
||||
? inventoryStalenessRiskProxy
|
||||
? inventoryQualityEvents
|
||||
? null
|
||||
: inventoryStalenessRiskProxy
|
||||
? "inventory_reserve_liquidation_quality"
|
||||
: inventoryTurnoverProxy
|
||||
? "inventory_liquidity_quality"
|
||||
@@ -4448,6 +4562,7 @@ function deriveBusinessOverview(input: {
|
||||
inventoryPosition,
|
||||
inventoryTurnoverProxy,
|
||||
inventoryStalenessRiskProxy,
|
||||
inventoryQualityEvents,
|
||||
vendorProcurementQuality,
|
||||
hasSupplierConcentrationSignal: (rankedOutgoing?.ranked_values.length ?? 0) > 0
|
||||
});
|
||||
@@ -4473,6 +4588,7 @@ function deriveBusinessOverview(input: {
|
||||
inventory_position: inventoryPosition,
|
||||
inventory_turnover_proxy: inventoryTurnoverProxy,
|
||||
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
||||
inventory_quality_events: inventoryQualityEvents,
|
||||
document_activity_profile: documentActivityProfile,
|
||||
counterparty_profile: counterpartyProfile,
|
||||
contract_usage_profile: contractUsageProfile,
|
||||
@@ -4483,7 +4599,7 @@ function deriveBusinessOverview(input: {
|
||||
missing_signal_families: missingSignalFamilies,
|
||||
missing_proof_families: missingProofFamilies,
|
||||
inference_basis:
|
||||
hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
|
||||
hasBusinessOverviewProfileSignal || inventoryPosition || inventoryQualityEvents || accountingFinancialResult
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: debtOpenSettlementQuality || debtDueDateAging
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
@@ -4513,6 +4629,7 @@ function summarizeBusinessOverviewRows(input: {
|
||||
contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
|
||||
inventoryOnHandResult: AddressMcpQueryExecutorResult | null;
|
||||
inventoryAgingResult: AddressMcpQueryExecutorResult | null;
|
||||
inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null;
|
||||
}): string | null {
|
||||
const parts: string[] = [];
|
||||
if (input.incomingResult && !input.incomingResult.error) {
|
||||
@@ -4560,6 +4677,9 @@ function summarizeBusinessOverviewRows(input: {
|
||||
if (input.inventoryAgingResult && !input.inventoryAgingResult.error) {
|
||||
parts.push(`${input.inventoryAgingResult.fetched_rows} inventory aging rows fetched, ${input.inventoryAgingResult.matched_rows} matched`);
|
||||
}
|
||||
if (input.inventoryQualityEventsResult && !input.inventoryQualityEventsResult.error) {
|
||||
parts.push(`${input.inventoryQualityEventsResult.fetched_rows} inventory quality-event rows fetched, ${input.inventoryQualityEventsResult.matched_rows} matched`);
|
||||
}
|
||||
return parts.length > 0 ? parts.join("; ") : null;
|
||||
}
|
||||
|
||||
@@ -4780,6 +4900,22 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
|
||||
`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`
|
||||
);
|
||||
}
|
||||
if (derived.inventory_quality_events) {
|
||||
const quality = derived.inventory_quality_events;
|
||||
const eventWindow =
|
||||
quality.first_event_date && quality.latest_event_date
|
||||
? ` Окно найденных событий: ${quality.first_event_date} — ${quality.latest_event_date}.`
|
||||
: "";
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
facts.push(
|
||||
`Reviewed inventory quality route проверил складские документы списания, оприходования, инвентаризации и переоценки${period}: подтвержденных событий не найдено. Это проверенный отрицательный результат по доступным документам, но не рыночная ликвидационная оценка и не управленческий резерв.`
|
||||
);
|
||||
} else {
|
||||
facts.push(
|
||||
`Reviewed inventory quality route проверил складские документы${period}: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С, но не самостоятельная рыночная ликвидационная стоимость.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return facts;
|
||||
}
|
||||
|
||||
@@ -5616,6 +5752,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let inventoryOnHandResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let inventoryAgingResult: AddressMcpQueryExecutorResult | null = null;
|
||||
let inventoryQualityEventsResult: AddressMcpQueryExecutorResult | null = null;
|
||||
const valueFilters = buildValueFlowFilters(planner);
|
||||
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
||||
@@ -5625,6 +5762,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
|
||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||
const inventoryQualityEventsProbeEnabled = shouldRunInventoryQualityEventsProbe(planner);
|
||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
||||
const incomingSelection = selectAddressRecipe("customer_revenue_and_payments", valueFilters);
|
||||
@@ -5660,6 +5798,9 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
const inventoryAgingSelection = inventoryFilters
|
||||
? selectAddressRecipe("inventory_aging_by_purchase_date", inventoryFilters)
|
||||
: null;
|
||||
const inventoryQualityEventsSelection = inventoryQualityEventsProbeEnabled
|
||||
? selectAddressRecipe("inventory_quality_events_for_organization", inventoryFilters ?? buildBusinessOverviewProfileFilters(planner))
|
||||
: null;
|
||||
|
||||
if (!incomingSelection.selected_recipe || !outgoingSelection.selected_recipe || !lifecycleSelection.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_recipe_not_available");
|
||||
@@ -5771,6 +5912,14 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview inventory-position probe requires an executable inventory on-hand as-of-date recipe");
|
||||
}
|
||||
if (inventoryQualityEventsSelection?.selected_recipe) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_recipe_selected");
|
||||
} else if (!inventoryQualityEventsProbeEnabled) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_probe_skipped_without_boundary_need");
|
||||
} else {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_recipe_not_available");
|
||||
pushUnique(queryLimitations, "Business overview inventory quality probe requires an executable inventory quality-events recipe");
|
||||
}
|
||||
for (const step of dryRun.execution_steps) {
|
||||
if (step.primitive_id === "query_movements") {
|
||||
const incomingExecution = await executeCoverageAwareValueFlowQuery({
|
||||
@@ -6007,6 +6156,19 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
});
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, contractUsageProfileResult));
|
||||
}
|
||||
if (inventoryQualityEventsSelection?.selected_recipe) {
|
||||
const inventoryQualityEventsFilters = inventoryFilters ?? buildBusinessOverviewProfileFilters(planner);
|
||||
const inventoryQualityEventsPlan = buildAddressRecipePlan(
|
||||
inventoryQualityEventsSelection.selected_recipe,
|
||||
inventoryQualityEventsFilters
|
||||
);
|
||||
inventoryQualityEventsResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
query: inventoryQualityEventsPlan.query,
|
||||
limit: inventoryQualityEventsPlan.limit,
|
||||
account_scope: inventoryQualityEventsPlan.account_scope
|
||||
});
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryQualityEventsResult));
|
||||
}
|
||||
if (lifecycleResult.error) {
|
||||
pushUnique(queryLimitations, lifecycleResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error");
|
||||
@@ -6037,6 +6199,12 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
} else if (contractUsageProfileResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_query_mcp_executed");
|
||||
}
|
||||
if (inventoryQualityEventsResult?.error) {
|
||||
pushUnique(queryLimitations, inventoryQualityEventsResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_query_mcp_error");
|
||||
} else if (inventoryQualityEventsResult) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_quality_events_query_mcp_executed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -6061,6 +6229,7 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
debtAsOfDate,
|
||||
inventoryOnHandResult,
|
||||
inventoryAgingResult,
|
||||
inventoryQualityEventsResult,
|
||||
inventoryAsOfDate,
|
||||
organizationScope,
|
||||
periodScope: dateScope
|
||||
@@ -6126,6 +6295,10 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
if (derivedBusinessOverview.inventory_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows");
|
||||
}
|
||||
if (derivedBusinessOverview.inventory_quality_events) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_quality_events_from_reviewed_rows");
|
||||
pushReason(reasonCodes, `pilot_derived_business_overview_inventory_quality_events_${derivedBusinessOverview.inventory_quality_events.evidence_status}`);
|
||||
}
|
||||
if (derivedBusinessOverview.missing_proof_families.length > 0) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_missing_proof_families_recorded");
|
||||
}
|
||||
@@ -6145,7 +6318,8 @@ export async function executeAssistantMcpDiscoveryPilot(
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
inventoryOnHandResult,
|
||||
inventoryAgingResult
|
||||
inventoryAgingResult,
|
||||
inventoryQualityEventsResult
|
||||
});
|
||||
const evidence = resolveAssistantMcpDiscoveryEvidence({
|
||||
plan: planner.discovery_plan,
|
||||
|
||||
@@ -997,12 +997,20 @@ function buildCompactBusinessOverviewReply(
|
||||
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."
|
||||
);
|
||||
if (inventoryQualityEvents) {
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
...toStringList(draft.limitation_lines)
|
||||
|
||||
@@ -36,6 +36,7 @@ export type AddressIntent =
|
||||
| "inventory_profitability_for_item"
|
||||
| "inventory_purchase_to_sale_chain"
|
||||
| "inventory_aging_by_purchase_date"
|
||||
| "inventory_quality_events_for_organization"
|
||||
| "account_balance_snapshot"
|
||||
| "open_items_by_counterparty_or_contract"
|
||||
| "list_documents_by_counterparty"
|
||||
@@ -204,7 +205,8 @@ export interface AddressRecipeDefinition {
|
||||
| "inventory_trading_margin_proxy_profile"
|
||||
| "inventory_profitability_profile"
|
||||
| "inventory_purchase_to_sale_chain_profile"
|
||||
| "inventory_aging_by_purchase_date_profile";
|
||||
| "inventory_aging_by_purchase_date_profile"
|
||||
| "inventory_quality_events_profile";
|
||||
required_filters: Array<keyof AddressFilterSet>;
|
||||
optional_filters: Array<keyof AddressFilterSet>;
|
||||
default_limit: number;
|
||||
|
||||
@@ -710,10 +710,14 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
|
||||
expect(draft.headline).toContain(
|
||||
"\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432"
|
||||
"\u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043b \u0441\u043a\u043b\u0430\u0434\u0441\u043a\u0438\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b"
|
||||
);
|
||||
expect(draft.headline).toContain(
|
||||
"\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439"
|
||||
);
|
||||
expect(draft.headline).toContain(
|
||||
"\u043d\u0435 \u0440\u044b\u043d\u043e\u0447\u043d\u0430\u044f \u043b\u0438\u043a\u0432\u0438\u0434\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c"
|
||||
);
|
||||
expect(draft.headline).toContain("\u043d\u0435\u043b\u044c\u0437\u044f");
|
||||
expect(draft.headline).toContain("staleness-risk proxy");
|
||||
expect(draft.headline).not.toContain("бизнес-обзор");
|
||||
expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
});
|
||||
|
||||
@@ -475,7 +475,7 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
expect(userFacing).not.toContain("MCP discovery pilot");
|
||||
});
|
||||
|
||||
it("marks exact business-overview proof gaps as route enablement instead of reviewed execution", async () => {
|
||||
it("promotes inventory reserve boundary after reviewed quality-event route executes", async () => {
|
||||
const deps = buildSequentialDeps([
|
||||
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Client A" }] },
|
||||
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Supplier A" }] },
|
||||
@@ -548,20 +548,21 @@ describe("assistant MCP discovery runtime bridge", () => {
|
||||
expect(result.bridge_status).toBe("answer_draft_ready");
|
||||
expect(result.business_fact_answer_allowed).toBe(true);
|
||||
expect(result.route_candidate).toMatchObject({
|
||||
candidate_status: "needs_route_enablement",
|
||||
candidate_status: "ready_for_reviewed_execution",
|
||||
selected_chain_id: "business_overview",
|
||||
business_fact_family: "business_overview",
|
||||
action_family: "inventory_reserve_boundary",
|
||||
executable_now: false
|
||||
executable_now: true,
|
||||
enablement_reason: null
|
||||
});
|
||||
expect(result.route_candidate.enablement_reason).toContain("inventory_reserve_liquidation_quality");
|
||||
expect(result.route_candidate.enablement_reason).toContain(
|
||||
"reviewed_inventory_quality_route_with_reserves_writeoffs_obsolescence_and_liquidation_value"
|
||||
expect(result.pilot.derived_business_overview?.inventory_quality_events?.evidence_status).toBe(
|
||||
"reviewed_no_quality_events_found"
|
||||
);
|
||||
expect(result.route_candidate.forbidden_overclaim_flags).toContain(
|
||||
"confirmed_obsolete_stock_reserve_writeoff_or_liquidation_value"
|
||||
expect(result.pilot.derived_business_overview?.missing_proof_families.map((item) => item.family)).not.toContain(
|
||||
"inventory_reserve_liquidation_quality"
|
||||
);
|
||||
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_needs_route_enablement");
|
||||
expect(result.answer_draft.reason_codes).toContain("answer_contains_business_overview_inventory_quality_events");
|
||||
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
|
||||
});
|
||||
|
||||
it("promotes profit-margin boundary when accounting 90/91/99 proof is available", async () => {
|
||||
|
||||
Reference in New Issue
Block a user