АРЧ АП11 - Архитектура: убрать технический мусор из inventory reply presentation
This commit is contained in:
parent
7f42d8ab50
commit
29721d16cd
|
|
@ -2,6 +2,7 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.composeInventoryReply = composeInventoryReply;
|
||||
const replyContracts_1 = require("./replyContracts");
|
||||
const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation");
|
||||
function composeInventoryReply(intent, rows, options, deps) {
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
|
|
@ -15,21 +16,26 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
: `На ${deps.formatDateRu(asOfDate)} подтвержденных товарных остатков по счету 41.01 не найдено.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (positions.length > 0) {
|
||||
lines.push("", "Позиции:");
|
||||
lines.push(...positions.slice(0, 20).map((item, index) => {
|
||||
const warehouseLabel = item.warehouse ?? "склад не определен";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | стоимость: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
|
||||
}));
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции:", positions.slice(0, 20).map((item, index) => (0, inventoryReplyPresentation_1.formatInventorySnapshotPositionLine)(item, index, {
|
||||
formatDateRu: deps.formatDateRu,
|
||||
formatNumberWithDots: deps.formatNumberWithDots,
|
||||
formatMoneyRub: deps.formatMoneyRub
|
||||
})));
|
||||
}
|
||||
else {
|
||||
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции:", [
|
||||
"- На дату среза товары с ненулевым остатком не найдены."
|
||||
]);
|
||||
}
|
||||
lines.push("", "Подтверждение:", `- Дата среза: ${deps.formatDateRu(asOfDate)}.`, "- Контур: остатки по счету 41.01 «Товары на складах».", `- Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`, `- Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`, `- Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
||||
]);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Строк в подтвержденной выборке: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
return positions.length > 0
|
||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong"))
|
||||
|
|
@ -41,12 +47,13 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const directAnswerLine = purchaseRows.length <= 0
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступных данных не найдены.`
|
||||
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
|
||||
const lines = [directAnswerLine];
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`);
|
||||
lines.push(`- Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
||||
`Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик: ${summary.counterparties[0]}.`);
|
||||
}
|
||||
|
|
@ -58,7 +65,7 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
lines.push(...deps.formatInventoryTraceRows(purchaseRows, 12));
|
||||
}
|
||||
else {
|
||||
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
|
||||
lines.push("- По выбранному товару не найдено проводок поступления в доступных данных.");
|
||||
}
|
||||
return purchaseRows.length > 0
|
||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("strong", true))
|
||||
|
|
@ -75,7 +82,7 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
const firstPurchaseDate = deps.inventoryTraceDateLabel(summary.firstPeriod);
|
||||
const lastPurchaseDate = deps.inventoryTraceDateLabel(summary.lastPeriod);
|
||||
const directAnswerLine = purchaseRows.length <= 0 || !summary.firstPeriod
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступных данных не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: boundedAsOfLabel
|
||||
|
|
@ -147,7 +154,7 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Опорные документы:", ...deps.formatInventoryTraceRows(purchaseRows, 8));
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length === 1 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
|
|
@ -161,36 +168,32 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
|
||||
const lines = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез supplier overlap для складского остатка до ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
|
||||
"- Важно: без партионности этот контур не доказывает конкретного владельца каждой партии, а показывает наблюдаемый закупочный след текущего остатка.",
|
||||
"",
|
||||
"Блок 2. Подтверждение",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Первая найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в доступных данных не выделен.`;
|
||||
const lines = [directAnswerLine];
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
|
||||
]);
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
lines.push(`- Найденные поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
lines.push("- Закупочные движения найдены, но поставщик не выделен отдельным полем в доступных данных.");
|
||||
}
|
||||
else {
|
||||
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
|
||||
lines.push("- В доступных данных не найдено закупочных движений по выбранному складскому срезу.");
|
||||
}
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`- Операций без явно материализованного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
lines.push(`- Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Блок 3. Опорные документы", ...deps.formatInventoryTraceRows(purchaseRows, 10));
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 10));
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (summary.counterparties.length > 0 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
|
||||
}
|
||||
|
|
@ -208,23 +211,18 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
const directAnswerLine = agingItems.length > 0
|
||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
const lines = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез старых закупок для складского остатка на ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
|
||||
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
|
||||
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`- Самая поздняя найденная закупка в наблюдаемом следе: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Позиции в aging-срезе: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`- Закупочных документов в наблюдаемом следе: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в наблюдаемом следе: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
const lines = [directAnswerLine];
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Сводка:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
||||
]);
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
}
|
||||
|
|
@ -232,10 +230,12 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (agingItems.length > 0) {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", ...deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
}
|
||||
else {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции от самых старых закупок:", [
|
||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
||||
]);
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(agingItems.length > 0 ? "strong" : "medium", agingItems.length > 0));
|
||||
}
|
||||
|
|
@ -251,7 +251,7 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
|
||||
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
|
|
@ -264,14 +264,14 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
|
||||
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
}
|
||||
else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
|
||||
}
|
||||
return saleRows.length > 0
|
||||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(summary.counterparties.length > 0 ? "strong" : "medium", true))
|
||||
|
|
@ -290,13 +290,13 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
}
|
||||
else if (purchaseRows.length > 0) {
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в доступных данных не подтверждено.");
|
||||
}
|
||||
else if (saleRows.length > 0) {
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в доступных данных не подтверждена.");
|
||||
}
|
||||
else {
|
||||
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
|
||||
|
|
|
|||
23
llm_normalizer/backend/dist/services/address_runtime/inventoryReplyPresentation.js
vendored
Normal file
23
llm_normalizer/backend/dist/services/address_runtime/inventoryReplyPresentation.js
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.appendInventorySection = appendInventorySection;
|
||||
exports.appendInventoryBulletSection = appendInventoryBulletSection;
|
||||
exports.formatInventorySnapshotPositionLine = formatInventorySnapshotPositionLine;
|
||||
function appendInventorySection(lines, title, entries) {
|
||||
const visibleEntries = entries.filter((entry) => String(entry ?? "").trim().length > 0);
|
||||
if (visibleEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
lines.push("", title, ...visibleEntries);
|
||||
}
|
||||
function appendInventoryBulletSection(lines, title, bullets) {
|
||||
appendInventorySection(lines, title, bullets
|
||||
.filter((bullet) => String(bullet ?? "").trim().length > 0)
|
||||
.map((bullet) => (bullet.startsWith("- ") ? bullet : `- ${bullet}`)));
|
||||
}
|
||||
function formatInventorySnapshotPositionLine(item, index, deps) {
|
||||
const warehouseLabel = item.warehouse ?? "склад не указан";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const lastSeenLabel = item.lastPeriod ? ` | последняя дата: ${deps.formatDateRu(item.lastPeriod)}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | сумма: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${lastSeenLabel}`;
|
||||
}
|
||||
|
|
@ -4,6 +4,11 @@ import {
|
|||
buildFactualListReply,
|
||||
buildFactualSummaryReply
|
||||
} from "./replyContracts";
|
||||
import {
|
||||
appendInventoryBulletSection,
|
||||
appendInventorySection,
|
||||
formatInventorySnapshotPositionLine
|
||||
} from "./inventoryReplyPresentation";
|
||||
import type { ComposeReplyResult } from "./replyPackaging";
|
||||
import type { ComposeStageRow } from "./composeStage";
|
||||
|
||||
|
|
@ -90,31 +95,32 @@ export function composeInventoryReply(
|
|||
const lines: string[] = [directAnswerLine];
|
||||
|
||||
if (positions.length > 0) {
|
||||
lines.push("", "Позиции:");
|
||||
lines.push(
|
||||
...positions.slice(0, 20).map((item, index) => {
|
||||
const warehouseLabel = item.warehouse ?? "склад не определен";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const periodLabel = item.lastPeriod ? ` | дата строки: ${item.lastPeriod}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | стоимость: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
|
||||
appendInventorySection(
|
||||
lines,
|
||||
"Позиции:",
|
||||
positions.slice(0, 20).map((item, index) =>
|
||||
formatInventorySnapshotPositionLine(item, index, {
|
||||
formatDateRu: deps.formatDateRu,
|
||||
formatNumberWithDots: deps.formatNumberWithDots,
|
||||
formatMoneyRub: deps.formatMoneyRub
|
||||
})
|
||||
)
|
||||
);
|
||||
} else {
|
||||
lines.push("", "Позиции:", "- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
appendInventorySection(lines, "Позиции:", [
|
||||
"- На дату среза товары с ненулевым остатком не найдены."
|
||||
]);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"",
|
||||
"Подтверждение:",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
"- Контур: остатки по счету 41.01 «Товары на складах».",
|
||||
`- Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`- Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`- Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
||||
);
|
||||
appendInventoryBulletSection(lines, "Сводка:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Позиции с остатком: ${deps.formatNumberWithDots(positions.length)}.`,
|
||||
`Уникальных товаров: ${deps.formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`Уникальных складов: ${deps.formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`Суммарное количество: ${deps.formatNumberWithDots(totalQuantity, 3)}.`
|
||||
]);
|
||||
if (rows.length !== positions.length) {
|
||||
lines.push(`- Строк в подтвержденной выборке: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Проверенных строк движения: ${deps.formatNumberWithDots(rows.length)}.`);
|
||||
}
|
||||
|
||||
return positions.length > 0
|
||||
|
|
@ -129,12 +135,13 @@ export function composeInventoryReply(
|
|||
const itemLabel = summary.item ?? "товар не определен";
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступном контуре не найдены.`
|
||||
? `По позиции ${itemLabel} подтвержденные документы закупки в доступных данных не найдены.`
|
||||
: `По позиции ${itemLabel} найдено ${deps.formatNumberWithDots(summary.documents.length)} подтвержденных документов закупки до ${deps.formatDateRu(asOfDate)}.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
lines.push("", "Подтверждение:");
|
||||
lines.push(`- Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`);
|
||||
lines.push(`- Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
appendInventoryBulletSection(lines, "Сводка:", [
|
||||
`Дата верхней границы: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Операций поступления в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
if (summary.counterparties.length === 1) {
|
||||
lines.push(`- Поставщик: ${summary.counterparties[0]}.`);
|
||||
} else if (summary.counterparties.length > 1) {
|
||||
|
|
@ -144,7 +151,7 @@ export function composeInventoryReply(
|
|||
if (purchaseRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(purchaseRows, 12));
|
||||
} else {
|
||||
lines.push("- По выбранному товару не найдено проводок поступления на 41.01 в доступном контуре.");
|
||||
lines.push("- По выбранному товару не найдено проводок поступления в доступных данных.");
|
||||
}
|
||||
return purchaseRows.length > 0
|
||||
? buildFactualListReply(lines, buildConfirmedBalanceSemantics("strong", true))
|
||||
|
|
@ -163,7 +170,7 @@ export function composeInventoryReply(
|
|||
const lastPurchaseDate = deps.inventoryTraceDateLabel(summary.lastPeriod);
|
||||
const directAnswerLine =
|
||||
purchaseRows.length <= 0 || !summary.firstPeriod
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступном контуре не найдена.`
|
||||
? `По позиции ${itemLabel} подтвержденная дата закупки в доступных данных не найдена.`
|
||||
: summary.firstPeriod === summary.lastPeriod
|
||||
? `Позиция ${itemLabel} куплена ${firstPurchaseDate}.`
|
||||
: boundedAsOfLabel
|
||||
|
|
@ -240,7 +247,7 @@ export function composeInventoryReply(
|
|||
lines.push(`- Для ответа проверены закупочные документы не позже ${boundedAsOfLabel}.`);
|
||||
}
|
||||
if (summary.documents.length > 0) {
|
||||
lines.push("", "Опорные документы:", ...deps.formatInventoryTraceRows(purchaseRows, 8));
|
||||
appendInventorySection(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 8));
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
|
|
@ -262,34 +269,30 @@ export function composeInventoryReply(
|
|||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По складскому остатку ${warehouseLabel} найдено несколько поставщиков: ${summary.counterparties.slice(0, 6).join("; ")}.`
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в текущем exact-контуре не материализован.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез supplier overlap для складского остатка до ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
`- Контур: подтвержденные закупочные движения на 41.01, связанные со складом ${warehouseLabel}.`,
|
||||
"- Важно: без партионности этот контур не доказывает конкретного владельца каждой партии, а показывает наблюдаемый закупочный след текущего остатка.",
|
||||
"",
|
||||
"Блок 2. Подтверждение",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Первая найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`- Последняя найденная дата закупочного движения: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
: `По складскому остатку ${warehouseLabel} поставщик в доступных данных не выделен.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
appendInventoryBulletSection(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Первая найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`,
|
||||
`Последняя найденная дата закупки: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`Закупочных документов в выборке: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
appendInventoryBulletSection(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
|
||||
]);
|
||||
if (summary.counterparties.length > 0) {
|
||||
lines.push(`- Найденные поставщики в наблюдаемом контуре: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
lines.push(`- Найденные поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
} else if (purchaseRows.length > 0) {
|
||||
lines.push("- Закупочные движения найдены, но поставщик не материализован отдельным полем в текущем exact-контуре.");
|
||||
lines.push("- Закупочные движения найдены, но поставщик не выделен отдельным полем в доступных данных.");
|
||||
} else {
|
||||
lines.push("- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного складского среза.");
|
||||
lines.push("- В доступных данных не найдено закупочных движений по выбранному складскому срезу.");
|
||||
}
|
||||
if (unresolvedRows.length > 0) {
|
||||
lines.push(`- Операций без явно материализованного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
lines.push(`- Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`);
|
||||
}
|
||||
if (purchaseRows.length > 0) {
|
||||
lines.push("", "Блок 3. Опорные документы", ...deps.formatInventoryTraceRows(purchaseRows, 10));
|
||||
appendInventorySection(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, 10));
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
|
|
@ -315,23 +318,18 @@ export function composeInventoryReply(
|
|||
agingItems.length > 0
|
||||
? `К старым закупкам на ${deps.formatDateRu(asOfDate)} в первую очередь относятся позиции с самой ранней первой закупкой: ${oldestAnswerPreview}.`
|
||||
: `По доступному закупочному следу на ${deps.formatDateRu(asOfDate)} позиции старых закупок не материализованы.`;
|
||||
const lines: string[] = [
|
||||
directAnswerLine,
|
||||
`Собран exact-срез старых закупок для складского остатка на ${deps.formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Контур: показан item-level список товарных позиций с самым ранним документально наблюдаемым закупочным следом на 41.01.",
|
||||
"- Порядок: позиции отсортированы от самой старой первой закупки к более новым.",
|
||||
"- Важно: без партионности этот контур не доказывает возраст конкретного лота, а показывает документально наблюдаемый возраст закупочного следа по товарной позиции.",
|
||||
"",
|
||||
"Блок 2. Сводка",
|
||||
`- Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`- Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`- Самая поздняя найденная закупка в наблюдаемом следе: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`- Позиции в aging-срезе: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`- Закупочных документов в наблюдаемом следе: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`- Закупочных операций в наблюдаемом следе: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
];
|
||||
const lines: string[] = [directAnswerLine];
|
||||
appendInventoryBulletSection(lines, "Сводка:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Самая ранняя первая закупка среди позиций: ${deps.inventoryTraceDateLabel(oldestPurchaseDate)}.`,
|
||||
`Самая поздняя найденная закупка: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`,
|
||||
`Позиций в выборке: ${deps.formatNumberWithDots(agingItems.length)}.`,
|
||||
`Закупочных документов: ${deps.formatNumberWithDots(summary.documents.length)}.`,
|
||||
`Закупочных операций: ${deps.formatNumberWithDots(purchaseRows.length)}.`
|
||||
]);
|
||||
appendInventoryBulletSection(lines, "Ограничения:", [
|
||||
"Без партионного учета этот ответ показывает возраст закупочного следа по товарной позиции, а не возраст конкретного лота."
|
||||
]);
|
||||
if (oldestPurchaseAgeDays !== null) {
|
||||
lines.push(`- Между самой ранней первой закупкой и датой среза прошло ${deps.formatNumberWithDots(oldestPurchaseAgeDays)} дн.`);
|
||||
}
|
||||
|
|
@ -339,9 +337,11 @@ export function composeInventoryReply(
|
|||
lines.push(`- Поставщики, встречающиеся в наблюдаемом закупочном следе: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
}
|
||||
if (agingItems.length > 0) {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", ...deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
appendInventorySection(lines, "Позиции от самых старых закупок:", deps.formatInventoryAgingRows(agingItems, asOfDate, 12));
|
||||
} else {
|
||||
lines.push("", "Блок 3. Позиции от самых старых закупок", "- В доступном exact-контуре не найдено закупочных движений по 41.01 для выбранного среза.");
|
||||
appendInventorySection(lines, "Позиции от самых старых закупок:", [
|
||||
"- В доступных данных не найдено закупочных движений для выбранного среза."
|
||||
]);
|
||||
}
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
|
|
@ -362,7 +362,7 @@ export function composeInventoryReply(
|
|||
? `По товару ${itemLabel} покупатель определен: ${summary.counterparties[0]}.`
|
||||
: summary.counterparties.length > 1
|
||||
? `По товару ${itemLabel} найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`
|
||||
: `По товару ${itemLabel} покупатель в текущем exact-контуре не материализован.`;
|
||||
: `По товару ${itemLabel} покупатель в доступных данных не выделен.`;
|
||||
const lines: string[] = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Первая найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.firstPeriod)}.`);
|
||||
lines.push(`- Последняя найденная дата выбытия: ${deps.inventoryTraceDateLabel(summary.lastPeriod)}.`);
|
||||
|
|
@ -373,13 +373,13 @@ export function composeInventoryReply(
|
|||
} else if (summary.counterparties.length > 1) {
|
||||
lines.push(`- По доступным движениям найдено несколько покупателей: ${summary.counterparties.slice(0, 4).join("; ")}.`);
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Документы выбытия найдены, но покупатель не материализован отдельным полем в текущем exact-контуре.");
|
||||
lines.push("- Документы выбытия найдены, но покупатель не выделен отдельным полем в доступных данных.");
|
||||
}
|
||||
lines.push("", "Документы выбытия:");
|
||||
if (saleRows.length > 0) {
|
||||
lines.push(...deps.formatInventoryTraceRows(saleRows, 12, excludedCounterpartyTokens));
|
||||
} else {
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия со счета 41.01 в доступном контуре.");
|
||||
lines.push("- По выбранному товару не найдено проводок выбытия в доступных данных.");
|
||||
}
|
||||
return saleRows.length > 0
|
||||
? buildFactualListReply(
|
||||
|
|
@ -403,11 +403,11 @@ export function composeInventoryReply(
|
|||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
if (purchaseRows.length > 0 && saleRows.length > 0) {
|
||||
lines.push("- В текущем контуре найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
lines.push("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
} else if (purchaseRows.length > 0) {
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в текущем exact-контуре не подтверждено.");
|
||||
lines.push("- Найдена только закупочная часть цепочки; выбытие в доступных данных не подтверждено.");
|
||||
} else if (saleRows.length > 0) {
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в текущем exact-контуре не подтверждена.");
|
||||
lines.push("- Найдена только часть выбытия; закупочная часть цепочки в доступных данных не подтверждена.");
|
||||
} else {
|
||||
lines.push("- Для выбранного товара не найдено движений по 41.01, из которых можно собрать цепочку.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
interface InventorySnapshotPositionLike {
|
||||
item: string;
|
||||
warehouse: string | null;
|
||||
organization: string | null;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
lastPeriod: string | null;
|
||||
}
|
||||
|
||||
interface InventoryPresentationDeps {
|
||||
formatDateRu: (isoDate: string) => string;
|
||||
formatNumberWithDots: (value: number, fractionDigits?: number) => string;
|
||||
formatMoneyRub: (value: number) => string;
|
||||
}
|
||||
|
||||
export function appendInventorySection(lines: string[], title: string, entries: string[]): void {
|
||||
const visibleEntries = entries.filter((entry) => String(entry ?? "").trim().length > 0);
|
||||
if (visibleEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
lines.push("", title, ...visibleEntries);
|
||||
}
|
||||
|
||||
export function appendInventoryBulletSection(lines: string[], title: string, bullets: string[]): void {
|
||||
appendInventorySection(
|
||||
lines,
|
||||
title,
|
||||
bullets
|
||||
.filter((bullet) => String(bullet ?? "").trim().length > 0)
|
||||
.map((bullet) => (bullet.startsWith("- ") ? bullet : `- ${bullet}`))
|
||||
);
|
||||
}
|
||||
|
||||
export function formatInventorySnapshotPositionLine(
|
||||
item: InventorySnapshotPositionLike,
|
||||
index: number,
|
||||
deps: InventoryPresentationDeps
|
||||
): string {
|
||||
const warehouseLabel = item.warehouse ?? "склад не указан";
|
||||
const organizationLabel = item.organization ? ` | организация: ${item.organization}` : "";
|
||||
const lastSeenLabel = item.lastPeriod ? ` | последняя дата: ${deps.formatDateRu(item.lastPeriod)}` : "";
|
||||
return `${index + 1}. ${item.item} | склад: ${warehouseLabel} | количество: ${deps.formatNumberWithDots(item.quantity, 3)} | сумма: ${deps.formatMoneyRub(item.amount)}${organizationLabel}${lastSeenLabel}`;
|
||||
}
|
||||
|
|
@ -64,8 +64,10 @@ describe("inventory aging follow-up", () => {
|
|||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("К старым закупкам на 30.09.2021");
|
||||
expect(reply.text).toContain("Блок 3. Позиции от самых старых закупок");
|
||||
expect(reply.text).not.toContain("Блок 3. Опорные документы");
|
||||
expect(reply.text).toContain("Позиции от самых старых закупок:");
|
||||
expect(reply.text).toContain("Ограничения:");
|
||||
expect(reply.text).not.toContain("Блок 1");
|
||||
expect(reply.text).not.toContain("exact-контур");
|
||||
expect(reply.text.indexOf("1. Четки Пост (84*117)")).toBeGreaterThan(-1);
|
||||
expect(reply.text.indexOf("2. Зеркало для инвалидов поворотное травмобезопасное")).toBeGreaterThan(
|
||||
reply.text.indexOf("1. Четки Пост (84*117)")
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ describe("address query shape classifier", () => {
|
|||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
expect(reply.text.split("\n")[0]).toContain("поставщиком");
|
||||
expect(reply.text.split("\n")[0]).toContain("подтвержден поставщик");
|
||||
expect(reply.text).toContain("Подтверждение");
|
||||
expect(reply.text).not.toContain("Блок 1");
|
||||
expect(reply.text).toContain("Гамма-мебель, ООО");
|
||||
|
|
@ -5149,14 +5149,46 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
|
|||
|
||||
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||
expect(reply.text.split("\n")[0]).toContain("На 31.03.2020 на складе подтверждено");
|
||||
expect(reply.text).toContain("Контур: остатки по счету 41.01");
|
||||
expect(reply.text).toContain("Сводка:");
|
||||
expect(reply.text).not.toContain("Блок 1");
|
||||
expect(reply.text).not.toContain("Контур:");
|
||||
expect(reply.text).not.toContain("source refs");
|
||||
expect(reply.text).toContain("Шкаф картотечный");
|
||||
expect(reply.text).toContain("Основной склад");
|
||||
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps supplier-overlap reply business-first without exact contour leakage", () => {
|
||||
const reply = composeFactualReply(
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
[
|
||||
{
|
||||
period: "2020-02-12T00:00:00Z",
|
||||
registrator: "Поступление товаров и услуг 00000000003 от 12.02.2020 0:00:00",
|
||||
account_dt: "41.01",
|
||||
account_kt: "60.01",
|
||||
amount: 3724.17,
|
||||
analytics: ["Столешница 600*3050*26 дуб ниагара", "Основной склад", "Торговый дом \\Союз\\"],
|
||||
item: "Столешница 600*3050*26 дуб ниагара",
|
||||
warehouse: "Основной склад",
|
||||
organization: 'ООО "Альтернатива Плюс"'
|
||||
}
|
||||
],
|
||||
{
|
||||
asOfDate: "2020-03-31",
|
||||
useRubCurrency: true
|
||||
}
|
||||
);
|
||||
|
||||
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
|
||||
expect(reply.text).toContain("Что проверили:");
|
||||
expect(reply.text).toContain("Опорные документы:");
|
||||
expect(reply.text).not.toContain("exact-контур");
|
||||
expect(reply.text).not.toContain("Блок 1");
|
||||
expect(reply.text).not.toContain("Контур:");
|
||||
});
|
||||
|
||||
it("routes inventory provenance questions to a dedicated intent", () => {
|
||||
const result = resolveAddressIntent("От какого поставщика куплен товар Шкаф картоотечный?");
|
||||
expect(result.intent).toBe("inventory_purchase_provenance_for_item");
|
||||
|
|
|
|||
Loading…
Reference in New Issue