Закрыть phase96 reviewed-route складских резервов и ликвидности
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user