Inventory breadth: закрепить stock provenance и sale-trace контуры
This commit is contained in:
@@ -848,6 +848,16 @@ function extractInventoryCounterpartyCandidates(row, excludedTokens = []) {
|
||||
}
|
||||
candidates.push(normalized);
|
||||
}
|
||||
const explicitCounterparty = normalizeCounterpartyDisplayLabel(row.counterparty);
|
||||
const explicitComparable = normalizeEntityToken(explicitCounterparty);
|
||||
if (explicitCounterparty &&
|
||||
explicitComparable &&
|
||||
explicitComparable !== itemToken &&
|
||||
explicitComparable !== warehouseToken &&
|
||||
explicitComparable !== organizationToken &&
|
||||
!excludedComparableTokens.includes(explicitComparable)) {
|
||||
candidates.unshift(explicitCounterparty);
|
||||
}
|
||||
return uniqueStrings(candidates);
|
||||
}
|
||||
function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
|
||||
@@ -883,6 +893,7 @@ function summarizeInventoryTraceRows(rows, excludedCounterpartyTokens = []) {
|
||||
function formatInventoryTraceRows(rows, limit = 10, excludedCounterpartyTokens = []) {
|
||||
return rows.slice(0, limit).map((row, index) => {
|
||||
const parties = extractInventoryCounterpartyCandidates(row, excludedCounterpartyTokens);
|
||||
const item = extractInventoryItemName(row);
|
||||
const warehouse = extractInventoryWarehouseName(row);
|
||||
const organization = extractInventoryOrganizationName(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? formatMoneyRub(row.amount) : "сумма не указана";
|
||||
@@ -891,6 +902,9 @@ function formatInventoryTraceRows(rows, limit = 10, excludedCounterpartyTokens =
|
||||
`дата: ${inventoryTraceDateLabel(row.period)}`,
|
||||
`сумма: ${amount}`
|
||||
];
|
||||
if (item) {
|
||||
parts.push(`товар: ${item}`);
|
||||
}
|
||||
if (warehouse) {
|
||||
parts.push(`склад: ${warehouse}`);
|
||||
}
|
||||
|
||||
+94
-5
@@ -3,6 +3,59 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.composeInventoryReply = composeInventoryReply;
|
||||
const replyContracts_1 = require("./replyContracts");
|
||||
const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation");
|
||||
function cleanupInventoryRequestedParty(value) {
|
||||
const cleaned = String(value ?? "")
|
||||
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|покупател|buyer|customer|item|product|sku)[\s\S]*$/iu, "")
|
||||
.replace(/\s+(?:на\s+дату|по\s+состоянию|за\s+период)\b[\s\S]*$/iu, "")
|
||||
.replace(/[«»"]/gu, "")
|
||||
.replace(/[.,;:\s]+$/u, "")
|
||||
.trim();
|
||||
return cleaned.length > 0 ? cleaned : null;
|
||||
}
|
||||
function extractRequestedInventoryParty(userMessage, role) {
|
||||
const text = String(userMessage ?? "");
|
||||
const patterns = role === "supplier"
|
||||
? [
|
||||
/(?:от\s+поставщика|у\s+поставщика|поставщик(?:а|у|ом)?|supplier|vendor)\s+([^\r\n?]+?)(?=$|[?]|(?:\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku|покупател|buyer|customer)))/iu
|
||||
]
|
||||
: [
|
||||
/(?:покупател(?:ь|я|ю|ем)?|buyer|customer|client)\s+([^\r\n?]+?)(?=$|[?]|(?:\s*(?:->|=>|→))|(?:\s+на\s+дату))/iu
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
const candidate = match?.[1] ? cleanupInventoryRequestedParty(match[1]) : null;
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function inventoryPartyComparableTokens(value) {
|
||||
const stopWords = new Set(["ооо", "ао", "пао", "зао", "ип", "llc", "ltd", "inc", "corp"]);
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/gu, "е")
|
||||
.replace(/[^a-zа-я0-9]+/giu, " ")
|
||||
.split(/\s+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3 && !stopWords.has(token));
|
||||
}
|
||||
function inventoryRequestedPartyMatches(requested, actualParties) {
|
||||
if (!requested) {
|
||||
return true;
|
||||
}
|
||||
const requestedTokens = inventoryPartyComparableTokens(requested);
|
||||
if (requestedTokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return actualParties.some((actual) => {
|
||||
const actualTokens = inventoryPartyComparableTokens(actual);
|
||||
return requestedTokens.every((token) => actualTokens.includes(token));
|
||||
});
|
||||
}
|
||||
function inventoryPartyListOrUnknown(parties) {
|
||||
return parties.length > 0 ? parties.slice(0, 4).join("; ") : "не выделен отдельным полем";
|
||||
}
|
||||
function composeInventoryReply(intent, rows, options, deps) {
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
@@ -163,6 +216,29 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const summary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const unresolvedRows = purchaseRows.filter((row) => deps.extractInventoryCounterpartyCandidates(row).length === 0);
|
||||
const unresolvedSupplierQuestion = /(?:\u0431\u0435\u0437\s+\u043f\u043e\u043d\u044f\u0442\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0431\u0435\u0437\s+(?:\u044f\u0432\u043d[^\s]*\s+)?\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\s+\u0438\u043c\u0435\u044e\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043d\u0435\u0442\s+\u044f\u0432\u043d[^\s]*\s+\u043f\u0440\u0438\u0432\u044f\u0437\u043a[^\s]*\s+\u043a\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|unresolved\s+supplier\s+link)/iu.test(String(options.userMessage ?? ""));
|
||||
if (unresolvedSupplierQuestion) {
|
||||
const directAnswerLine = unresolvedRows.length > 0
|
||||
? `В текущем складском срезе найдено операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`
|
||||
: "В текущем складском срезе товары без явно выделенной привязки к поставщику в доступных данных не найдены.";
|
||||
const lines = [directAnswerLine];
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
|
||||
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
|
||||
`Закупочных операций в выборке: ${deps.formatNumberWithDots(purchaseRows.length)}.`,
|
||||
`Операций без явно выделенного поставщика: ${deps.formatNumberWithDots(unresolvedRows.length)}.`,
|
||||
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(summary.counterparties.length)}.`
|
||||
]);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
|
||||
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии."
|
||||
]);
|
||||
if (unresolvedRows.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Позиции без явно выделенного поставщика:", deps.formatInventoryTraceRows(unresolvedRows, 12));
|
||||
}
|
||||
else if (summary.counterparties.length > 0) {
|
||||
lines.push(`- В доступном закупочном следе встречаются поставщики: ${summary.counterparties.slice(0, 6).join("; ")}.`);
|
||||
}
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(unresolvedRows.length > 0 ? "medium" : "strong", true));
|
||||
}
|
||||
const warehouseLabel = summary.warehouses[0] ?? "не указанного склада";
|
||||
const directAnswerLine = summary.counterparties.length === 1
|
||||
? `По складскому остатку ${warehouseLabel} выявлен поставщик: ${summary.counterparties[0]}.`
|
||||
@@ -283,12 +359,25 @@ function composeInventoryReply(intent, rows, options, deps) {
|
||||
const purchaseSummary = deps.summarizeInventoryTraceRows(purchaseRows);
|
||||
const saleSummary = deps.summarizeInventoryTraceRows(saleRows);
|
||||
const itemLabel = purchaseSummary.item ?? saleSummary.item ?? "товар не определен";
|
||||
const directAnswerLine = purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const requestedSupplier = extractRequestedInventoryParty(options.userMessage, "supplier");
|
||||
const requestedBuyer = extractRequestedInventoryParty(options.userMessage, "buyer");
|
||||
const supplierMatches = inventoryRequestedPartyMatches(requestedSupplier, purchaseSummary.counterparties);
|
||||
const buyerMatches = inventoryRequestedPartyMatches(requestedBuyer, saleSummary.counterparties);
|
||||
const mismatchParts = [];
|
||||
if (requestedSupplier && purchaseRows.length > 0 && !supplierMatches) {
|
||||
mismatchParts.push(`запрошенный поставщик ${requestedSupplier} не совпал с найденным поставщиком: ${inventoryPartyListOrUnknown(purchaseSummary.counterparties)}`);
|
||||
}
|
||||
if (requestedBuyer && saleRows.length > 0 && !buyerMatches) {
|
||||
mismatchParts.push(`запрошенный покупатель ${requestedBuyer} не совпал с найденным покупателем: ${inventoryPartyListOrUnknown(saleSummary.counterparties)}`);
|
||||
}
|
||||
const directAnswerLine = mismatchParts.length > 0
|
||||
? `Запрошенная цепочка по товару ${itemLabel} полностью не подтверждена: ${mismatchParts.join("; ")}.`
|
||||
: purchaseSummary.counterparties.length === 1 && saleSummary.counterparties.length === 1
|
||||
? `По товару ${itemLabel} цепочка поставки и продажи связана с поставщиком ${purchaseSummary.counterparties[0]} и покупателем ${saleSummary.counterparties[0]}.`
|
||||
: `По товару ${itemLabel} цепочка поставки и продажи подтверждена частично или разнообразно: детали идут следом.`;
|
||||
const lines = [directAnswerLine, "", "Подтверждение:"];
|
||||
lines.push(`- Закупочных движений на 41.01: ${deps.formatNumberWithDots(purchaseRows.length)}.`);
|
||||
lines.push(`- Движений выбытия со счета 41.01: ${deps.formatNumberWithDots(saleRows.length)}.`);
|
||||
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("- В доступных данных найдены обе стороны цепочки: поступление и последующее выбытие.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user