ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Протянуть exact capability складских остатков товаров на дату
This commit is contained in:
@@ -12,6 +12,10 @@ export interface ComposeStageRow {
|
||||
account_kt: string | null;
|
||||
amount: number | null;
|
||||
analytics: string[];
|
||||
quantity?: number | null;
|
||||
item?: string | null;
|
||||
warehouse?: string | null;
|
||||
organization?: string | null;
|
||||
}
|
||||
|
||||
export interface VatDirectSourceProbeItem {
|
||||
@@ -750,6 +754,162 @@ function extractCounterpartyName(row: ComposeStageRow): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractInventoryItemName(row: ComposeStageRow): string | null {
|
||||
const direct = String(row.item ?? "").trim();
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (/(?:склад|warehouse|ооо|ао|пао|зао|ип|организац)/iu.test(normalized)) {
|
||||
continue;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractInventoryWarehouseName(row: ComposeStageRow): string | null {
|
||||
const direct = String(row.warehouse ?? "").trim();
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
if (/(?:склад|warehouse)/iu.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractInventoryOrganizationName(row: ComposeStageRow): string | null {
|
||||
const direct = String(row.organization ?? "").trim();
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
if (/(?:(?:^|[\s"'«»„“()\\\/])(?:ооо|ао|пао|зао|оао|ип|гку)(?=$|[\s"'«»„“()\\\/.,;:]))|организац|комитет|департамент|министерств|служб|управлени|казенн|администрац/iu.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractInventoryQuantity(row: ComposeStageRow): number | null {
|
||||
return typeof row.quantity === "number" && Number.isFinite(row.quantity) ? row.quantity : null;
|
||||
}
|
||||
|
||||
interface InventoryOnHandAggregate {
|
||||
item: string;
|
||||
warehouse: string | null;
|
||||
organization: string | null;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
operations: number;
|
||||
firstPeriod: string | null;
|
||||
lastPeriod: string | null;
|
||||
sourceRefs: string[];
|
||||
}
|
||||
|
||||
function buildInventoryOnHandAggregate(rows: ComposeStageRow[], asOfDate: string): InventoryOnHandAggregate[] {
|
||||
const byPosition = new Map<
|
||||
string,
|
||||
{
|
||||
item: string;
|
||||
warehouse: string | null;
|
||||
organization: string | null;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
operations: number;
|
||||
firstPeriod: string | null;
|
||||
lastPeriod: string | null;
|
||||
sourceRefs: Set<string>;
|
||||
}
|
||||
>();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
|
||||
for (const row of rows) {
|
||||
const item = extractInventoryItemName(row);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const rowTimestamp = toUtcDayTimestamp(row.period);
|
||||
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
|
||||
continue;
|
||||
}
|
||||
const quantity = extractInventoryQuantity(row);
|
||||
if (quantity === null || quantity <= 0) {
|
||||
continue;
|
||||
}
|
||||
const warehouse = extractInventoryWarehouseName(row);
|
||||
const organization = extractInventoryOrganizationName(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? row.amount : 0;
|
||||
const key = [normalizeEntityToken(item), normalizeEntityToken(warehouse), normalizeEntityToken(organization)].join("|");
|
||||
const registrator = String(row.registrator ?? "").trim();
|
||||
const current = byPosition.get(key);
|
||||
if (!current) {
|
||||
byPosition.set(key, {
|
||||
item,
|
||||
warehouse,
|
||||
organization,
|
||||
quantity,
|
||||
amount,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
sourceRefs: new Set(registrator && registrator !== "Остатки на дату" ? [registrator] : [])
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.quantity += quantity;
|
||||
current.amount += amount;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
if (registrator && registrator !== "Остатки на дату") {
|
||||
current.sourceRefs.add(registrator);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byPosition.values())
|
||||
.map((item) => ({
|
||||
item: item.item,
|
||||
warehouse: item.warehouse,
|
||||
organization: item.organization,
|
||||
quantity: item.quantity,
|
||||
amount: item.amount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
|
||||
}))
|
||||
.filter((item) => item.quantity > 0)
|
||||
.sort((left, right) => {
|
||||
if (right.quantity !== left.quantity) {
|
||||
return right.quantity - left.quantity;
|
||||
}
|
||||
if (right.amount !== left.amount) {
|
||||
return right.amount - left.amount;
|
||||
}
|
||||
return left.item.localeCompare(right.item, "ru");
|
||||
});
|
||||
}
|
||||
|
||||
interface CounterpartyRiskAggregate {
|
||||
name: string;
|
||||
totalAmount: number;
|
||||
@@ -3502,6 +3662,63 @@ export function composeFactualReply(
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const positions = buildInventoryOnHandAggregate(rows, asOfDate);
|
||||
const uniqueItems = uniqueStrings(positions.map((item) => item.item));
|
||||
const uniqueWarehouses = uniqueStrings(
|
||||
positions.map((item) => String(item.warehouse ?? "").trim()).filter((item) => item.length > 0)
|
||||
);
|
||||
const totalQuantity = positions.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalAmount = positions.reduce((sum, item) => sum + item.amount, 0);
|
||||
|
||||
const lines: string[] = [
|
||||
`Собран подтвержденный срез товаров на складах на ${formatDateRu(asOfDate)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный список товарных остатков на дату.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
`- Дата среза: ${formatDateRu(asOfDate)}.`,
|
||||
"- Контур: остатки по счету 41.01 «Товары на складах».",
|
||||
"- Базовая единица детализации: одна строка = товар, склад и организация на дату.",
|
||||
"",
|
||||
"Блок 3. Сводка",
|
||||
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||
`- Позиции с ненулевым остатком: ${formatNumberWithDots(positions.length)}.`,
|
||||
`- Уникальных товаров: ${formatNumberWithDots(uniqueItems.length)}.`,
|
||||
`- Уникальных складов: ${formatNumberWithDots(uniqueWarehouses.length)}.`,
|
||||
`- Суммарное количество: ${formatNumberWithDots(totalQuantity, 3)}.`,
|
||||
`- Суммарная стоимость: ${formatMoneyRub(totalAmount)}.`,
|
||||
"",
|
||||
"Блок 4. Подтвержденные позиции"
|
||||
];
|
||||
|
||||
if (positions.length > 0) {
|
||||
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} | количество: ${formatNumberWithDots(item.quantity, 3)} | стоимость: ${formatMoneyRub(item.amount)}${organizationLabel}${periodLabel}${refsLabel}`;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
lines.push("- На дату среза товары с ненулевым остатком по счету 41.01 не найдены.");
|
||||
}
|
||||
|
||||
return {
|
||||
responseType: positions.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: positions.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
const asOfDate = resolvePayablesAsOfDate(options);
|
||||
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
|
||||
|
||||
@@ -367,13 +367,22 @@ function mergeFollowupFilters(
|
||||
const merged: AddressFilterSet = { ...current };
|
||||
const reasons: string[] = [];
|
||||
if (!followupContext) {
|
||||
if ((intent === "list_open_contracts" || intent === "open_contracts_confirmed_as_of_date") && !toNonEmptyString(merged.as_of_date)) {
|
||||
if (
|
||||
(intent === "list_open_contracts" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "inventory_on_hand_as_of_date") &&
|
||||
!toNonEmptyString(merged.as_of_date)
|
||||
) {
|
||||
const periodToForOpenContracts = toNonEmptyString(merged.period_to);
|
||||
const periodFromForOpenContracts = toNonEmptyString(merged.period_from);
|
||||
const derivedAsOfDate = periodToForOpenContracts ?? periodFromForOpenContracts;
|
||||
if (derivedAsOfDate) {
|
||||
merged.as_of_date = derivedAsOfDate;
|
||||
reasons.push("as_of_date_derived_from_period_for_open_contracts");
|
||||
reasons.push(
|
||||
intent === "inventory_on_hand_as_of_date"
|
||||
? "as_of_date_derived_from_period_for_inventory"
|
||||
: "as_of_date_derived_from_period_for_open_contracts"
|
||||
);
|
||||
}
|
||||
}
|
||||
return { filters: merged, reasons };
|
||||
@@ -480,6 +489,7 @@ function mergeFollowupFilters(
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_confirmed_as_of_date"
|
||||
@@ -561,6 +571,7 @@ function mergeFollowupFilters(
|
||||
intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_confirmed_as_of_date";
|
||||
@@ -598,13 +609,22 @@ function mergeFollowupFilters(
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
|
||||
if ((intent === "list_open_contracts" || intent === "open_contracts_confirmed_as_of_date") && !toNonEmptyString(merged.as_of_date)) {
|
||||
if (
|
||||
(intent === "list_open_contracts" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "inventory_on_hand_as_of_date") &&
|
||||
!toNonEmptyString(merged.as_of_date)
|
||||
) {
|
||||
const periodToForOpenContracts = toNonEmptyString(merged.period_to);
|
||||
const periodFromForOpenContracts = toNonEmptyString(merged.period_from);
|
||||
const derivedAsOfDate = periodToForOpenContracts ?? periodFromForOpenContracts;
|
||||
if (derivedAsOfDate) {
|
||||
merged.as_of_date = derivedAsOfDate;
|
||||
reasons.push("as_of_date_derived_from_period_for_open_contracts");
|
||||
reasons.push(
|
||||
intent === "inventory_on_hand_as_of_date"
|
||||
? "as_of_date_derived_from_period_for_inventory"
|
||||
: "as_of_date_derived_from_period_for_open_contracts"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,6 +635,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||
const requiredByIntent: Record<string, Array<keyof AddressFilterSet>> = {
|
||||
account_balance_snapshot: ["account", "as_of_date"],
|
||||
documents_forming_balance: ["account", "as_of_date"],
|
||||
inventory_on_hand_as_of_date: ["as_of_date"],
|
||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||
payables_confirmed_as_of_date: ["as_of_date"],
|
||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||
|
||||
Reference in New Issue
Block a user