ДОМЕНЫ - ВОПРОСЫ - кто должен нам
This commit is contained in:
@@ -176,10 +176,7 @@ function formatMoneyRub(value: number): string {
|
||||
}
|
||||
|
||||
function emphasizeNumericTokens(line: string): string {
|
||||
if (!line) {
|
||||
return line;
|
||||
}
|
||||
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
|
||||
return line;
|
||||
}
|
||||
|
||||
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
|
||||
@@ -700,6 +697,19 @@ function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
|
||||
return "прочие";
|
||||
}
|
||||
|
||||
function receivablesCategoryLabel(category: PayablesLiabilityCategory): string {
|
||||
if (category === "supplier_or_contractor") {
|
||||
return "покупатели/заказчики";
|
||||
}
|
||||
if (category === "bank_or_credit") {
|
||||
return "банки/финансовые";
|
||||
}
|
||||
if (category === "tax_or_state") {
|
||||
return "бюджет/госорганы";
|
||||
}
|
||||
return "прочие";
|
||||
}
|
||||
|
||||
function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: string): {
|
||||
scores: Record<PayablesLiabilityCategory, number>;
|
||||
reasons: string[];
|
||||
@@ -784,6 +794,11 @@ function hasPayablesSectionPrefix(account: string | null): boolean {
|
||||
return section === "60" || section === "76";
|
||||
}
|
||||
|
||||
function hasReceivablesSectionPrefix(account: string | null): boolean {
|
||||
const section = extractAccountSectionCode(account);
|
||||
return section === "62" || section === "76";
|
||||
}
|
||||
|
||||
function resolvePayablesAsOfDate(options: ComposeFactualReplyOptions): string {
|
||||
const explicit = normalizeIsoDateOnly(options.asOfDate);
|
||||
if (explicit) {
|
||||
@@ -998,6 +1013,126 @@ function buildPayablesConfirmedBalanceAggregate(
|
||||
});
|
||||
}
|
||||
|
||||
function buildReceivablesConfirmedBalanceAggregate(
|
||||
rows: ComposeStageRow[],
|
||||
asOfDate: string
|
||||
): PayablesConfirmedBalanceAggregate[] {
|
||||
const byCounterparty = new Map<
|
||||
string,
|
||||
{
|
||||
outstandingAmount: number;
|
||||
operations: number;
|
||||
firstPeriod: string | null;
|
||||
lastPeriod: string | null;
|
||||
categoryScores: Record<PayablesLiabilityCategory, number>;
|
||||
reasons: Set<string>;
|
||||
contracts: Set<string>;
|
||||
documents: Set<string>;
|
||||
sourceRefs: Set<string>;
|
||||
}
|
||||
>();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
|
||||
for (const row of rows) {
|
||||
const name = extractCounterpartyName(row);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const rowTimestamp = toUtcDayTimestamp(row.period);
|
||||
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
|
||||
continue;
|
||||
}
|
||||
const amount = row.amount;
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||
continue;
|
||||
}
|
||||
const absAmount = Math.abs(amount);
|
||||
let delta = 0;
|
||||
if (hasReceivablesSectionPrefix(row.account_dt)) {
|
||||
delta += absAmount;
|
||||
}
|
||||
if (hasReceivablesSectionPrefix(row.account_kt)) {
|
||||
delta -= absAmount;
|
||||
}
|
||||
if (Math.abs(delta) <= 0.0000001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const classified = classifyPayablesLiabilityCategory(row, name);
|
||||
const contract = extractContractName(row);
|
||||
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
|
||||
const current = byCounterparty.get(name);
|
||||
if (!current) {
|
||||
byCounterparty.set(name, {
|
||||
outstandingAmount: delta,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
categoryScores: {
|
||||
supplier_or_contractor: classified.scores.supplier_or_contractor,
|
||||
bank_or_credit: classified.scores.bank_or_credit,
|
||||
tax_or_state: classified.scores.tax_or_state,
|
||||
other: classified.scores.other
|
||||
},
|
||||
reasons: new Set(classified.reasons),
|
||||
contracts: new Set(contract ? [contract] : []),
|
||||
documents: new Set(row.registrator ? [row.registrator] : []),
|
||||
sourceRefs: new Set(sourceRefs)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
current.outstandingAmount += delta;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
|
||||
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
|
||||
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
|
||||
current.categoryScores.other += classified.scores.other;
|
||||
for (const reason of classified.reasons) {
|
||||
current.reasons.add(reason);
|
||||
}
|
||||
if (contract) {
|
||||
current.contracts.add(contract);
|
||||
}
|
||||
if (row.registrator) {
|
||||
current.documents.add(row.registrator);
|
||||
}
|
||||
for (const ref of sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byCounterparty.entries())
|
||||
.map(([name, item]) => ({
|
||||
name,
|
||||
outstandingAmount: item.outstandingAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
category: resolvePayablesLiabilityCategory(item.categoryScores),
|
||||
categoryReasons: Array.from(item.reasons).slice(0, 2),
|
||||
contracts: Array.from(item.contracts).slice(0, 2),
|
||||
documents: Array.from(item.documents).slice(0, 2),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
|
||||
}))
|
||||
.filter((item) => item.outstandingAmount > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.outstandingAmount !== left.outstandingAmount) {
|
||||
return right.outstandingAmount - left.outstandingAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.name.localeCompare(right.name);
|
||||
});
|
||||
}
|
||||
|
||||
function buildCounterpartyRiskAggregate(rows: ComposeStageRow[]): CounterpartyRiskAggregate[] {
|
||||
const byCounterparty = new Map<string, CounterpartyRiskAggregate>();
|
||||
|
||||
@@ -2462,7 +2597,7 @@ export function composeFactualReply(
|
||||
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
|
||||
"- Результат: подтвержденный срез обязательств к оплате."
|
||||
];
|
||||
|
||||
lines.push("");
|
||||
@@ -2492,11 +2627,14 @@ export function composeFactualReply(
|
||||
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
||||
if (confirmedBalances.length > 0) {
|
||||
lines.push(
|
||||
...confirmedBalances.slice(0, 10).map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
||||
)
|
||||
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
||||
""
|
||||
])
|
||||
);
|
||||
if (lines[lines.length - 1] === "") {
|
||||
lines.pop();
|
||||
}
|
||||
} else {
|
||||
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
|
||||
}
|
||||
@@ -2512,6 +2650,86 @@ export function composeFactualReply(
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
|
||||
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
|
||||
const periodScopeLine =
|
||||
!asOfDate && (periodFrom || periodTo)
|
||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||
: null;
|
||||
const carryoverLine =
|
||||
asOfDate || periodFrom || periodTo
|
||||
? "- В срез могут входить задолженности, возникшие до периода, если они оставались открытыми на дату среза."
|
||||
: null;
|
||||
const categoryCounts = confirmedBalances.reduce<Record<PayablesLiabilityCategory, number>>(
|
||||
(acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
return acc;
|
||||
},
|
||||
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
||||
);
|
||||
|
||||
const lines: string[] = [
|
||||
`Итого подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный срез дебиторской задолженности."
|
||||
];
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 2. Что учтено");
|
||||
lines.push(`- Дата среза: ${formatDateRu(receivablesAsOfDate)}.`);
|
||||
if (periodScopeLine) {
|
||||
lines.push(periodScopeLine);
|
||||
}
|
||||
lines.push("- Контур: дебиторская задолженность по счетам 62/76.");
|
||||
if (carryoverLine) {
|
||||
lines.push(carryoverLine);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 3. Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Категории дебиторской задолженности");
|
||||
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
||||
lines.push(`- ${receivablesCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
|
||||
lines.push(`- ${receivablesCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
|
||||
lines.push(`- ${receivablesCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
|
||||
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Подтвержденные позиции к получению");
|
||||
if (confirmedBalances.length > 0) {
|
||||
lines.push(
|
||||
...confirmedBalances.slice(0, 10).flatMap((item, index) => [
|
||||
`${index + 1}. ${item.name} | категория: ${receivablesCategoryLabel(item.category)} | остаток к получению: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`,
|
||||
""
|
||||
])
|
||||
);
|
||||
if (lines[lines.length - 1] === "") {
|
||||
lines.pop();
|
||||
}
|
||||
} else {
|
||||
lines.push("- Подтвержденной открытой дебиторской задолженности на дату среза не найдено.");
|
||||
}
|
||||
|
||||
return {
|
||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (intent === "list_payables_counterparties") {
|
||||
const counterparties = buildPayablesCounterpartyRiskAggregate(rows);
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
|
||||
@@ -442,7 +442,8 @@ function mergeFollowupFilters(
|
||||
if (
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "list_open_contracts" ||
|
||||
intent === "payables_confirmed_as_of_date"
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date"
|
||||
) {
|
||||
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
|
||||
const currentContract = toNonEmptyString(merged.contract);
|
||||
@@ -537,6 +538,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||
account_balance_snapshot: ["account", "as_of_date"],
|
||||
documents_forming_balance: ["account", "as_of_date"],
|
||||
payables_confirmed_as_of_date: ["as_of_date"],
|
||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||
list_documents_by_counterparty: ["counterparty"],
|
||||
bank_operations_by_counterparty: ["counterparty"],
|
||||
list_contracts_by_counterparty: ["counterparty"],
|
||||
|
||||
@@ -192,7 +192,8 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
|
||||
if (
|
||||
intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "payables_confirmed_as_of_date"
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date"
|
||||
) {
|
||||
return "balance_snapshot";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user