ДОМЕНЫ - ВОПРОСЫ - СКЛАД - Протянуть exact capability складских остатков товаров на дату
This commit is contained in:
@@ -565,6 +565,130 @@ function extractCounterpartyName(row) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function extractInventoryItemName(row) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
return typeof row.quantity === "number" && Number.isFinite(row.quantity) ? row.quantity : null;
|
||||
}
|
||||
function buildInventoryOnHandAggregate(rows, asOfDate) {
|
||||
const byPosition = new Map();
|
||||
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");
|
||||
});
|
||||
}
|
||||
function liabilityCategoryLabel(category) {
|
||||
if (category === "supplier_or_contractor") {
|
||||
return "поставщики/подрядчики";
|
||||
@@ -1212,6 +1336,455 @@ function contractCandidatesFromRows(rows) {
|
||||
}
|
||||
return uniqueStrings(candidates);
|
||||
}
|
||||
function isFinancialContractLike(value) {
|
||||
return /(?:кредит|кред\.?|loan|overdraft|овердрафт|лизинг|leasing|займ|guarantee|гарант|банк|bank)/iu.test(value);
|
||||
}
|
||||
function hasStrongContractIdentitySignal(value) {
|
||||
return /(?:договор|contract|дог\.|№|\d{1,4}[\\/.-]\d{1,4}|\d{1,4}\sот\s\d{2}\.\d{2}\.\d{2,4}|[A-ZА-Я]{1,6}-\d+)/iu.test(value);
|
||||
}
|
||||
function isLikelyOrganizationName(value) {
|
||||
return /(?:(?:^|[\s"'«»„“()\\\/])(?:ооо|ао|пао|зао|оао|ип|гку)(?=$|[\s"'«»„“()\\\/.,;:]))|комитет|департамент|министерств|служб|управлени|казенн|администрац|bank|банк/iu.test(value);
|
||||
}
|
||||
function isContractLikeCounterparty(value) {
|
||||
return /(?:договор|дог[-.\s]?р|contract|кредитн|loan|овердрафт|лизинг|\b№\b)/iu.test(value);
|
||||
}
|
||||
function isLowQualityContractIdentity(contract, counterparty) {
|
||||
const normalizedContract = normalizeEntityToken(contract);
|
||||
if (!normalizedContract || normalizedContract.length < 3) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(contract.trim())) {
|
||||
return true;
|
||||
}
|
||||
if (counterparty && normalizeEntityToken(counterparty) === normalizedContract) {
|
||||
return true;
|
||||
}
|
||||
if (!hasStrongContractIdentitySignal(contract) && isLikelyOrganizationName(contract)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasStrongContractIdentitySignal(contract) && /^[A-ZА-Я]{2,6}$/u.test(contract.trim())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isLowQualityCounterpartyForContract(counterparty, contract) {
|
||||
if (!counterparty) {
|
||||
return true;
|
||||
}
|
||||
const normalizedCounterparty = normalizeEntityToken(counterparty);
|
||||
const normalizedContract = normalizeEntityToken(contract);
|
||||
if (!normalizedCounterparty) {
|
||||
return true;
|
||||
}
|
||||
if (normalizedCounterparty === normalizedContract) {
|
||||
return true;
|
||||
}
|
||||
if (isContractLikeCounterparty(counterparty)) {
|
||||
return true;
|
||||
}
|
||||
return normalizedCounterparty.length < 3;
|
||||
}
|
||||
function normalizeDisplayAccountToken(value) {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized || /^(?:0|<пусто>|пустая ссылка|-)$/iu.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function classifyOpenContractCategory(contract, counterparties, qualityFlags) {
|
||||
if (isFinancialContractLike(contract)) {
|
||||
return "financial";
|
||||
}
|
||||
if (counterparties.some((item) => isFinancialContractLike(item))) {
|
||||
return "financial";
|
||||
}
|
||||
if (qualityFlags.includes("counterparty_not_reliably_resolved") ||
|
||||
qualityFlags.includes("contract_identity_not_reliable") ||
|
||||
qualityFlags.includes("contract_identity_looks_like_counterparty") ||
|
||||
qualityFlags.includes("multiple_counterparties_for_contract")) {
|
||||
return "uncertain";
|
||||
}
|
||||
if (counterparties.length === 0) {
|
||||
return "uncertain";
|
||||
}
|
||||
return "commercial";
|
||||
}
|
||||
function classifyOpenContractSettlementKind(row) {
|
||||
const dt = extractAccountSectionCode(row.account_dt);
|
||||
const kt = extractAccountSectionCode(row.account_kt);
|
||||
if (dt === "62") {
|
||||
return "receivable";
|
||||
}
|
||||
if (kt === "60") {
|
||||
return "payable";
|
||||
}
|
||||
if (dt === "60") {
|
||||
return "advance_issued";
|
||||
}
|
||||
if (kt === "62") {
|
||||
return "advance_received";
|
||||
}
|
||||
if (dt === "76") {
|
||||
return "other_receivable";
|
||||
}
|
||||
if (kt === "76") {
|
||||
return "other_payable";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function openContractSettlementKindLabel(kind) {
|
||||
if (kind === "receivable") {
|
||||
return "дебиторская задолженность";
|
||||
}
|
||||
if (kind === "payable") {
|
||||
return "кредиторская задолженность";
|
||||
}
|
||||
if (kind === "advance_issued") {
|
||||
return "аванс выданный";
|
||||
}
|
||||
if (kind === "advance_received") {
|
||||
return "аванс полученный";
|
||||
}
|
||||
if (kind === "other_receivable") {
|
||||
return "прочий дебетовый остаток";
|
||||
}
|
||||
return "прочий кредитовый остаток";
|
||||
}
|
||||
function openContractSettlementKindSign(kind) {
|
||||
if (kind === "receivable" || kind === "advance_issued" || kind === "other_receivable") {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function classifyOpenContractReviewBucket(item) {
|
||||
if (item.category === "commercial") {
|
||||
return null;
|
||||
}
|
||||
if (item.qualityFlags.includes("counterparty_not_reliably_resolved") ||
|
||||
item.qualityFlags.includes("contract_identity_not_reliable") ||
|
||||
item.qualityFlags.includes("contract_identity_looks_like_counterparty") ||
|
||||
item.qualityFlags.includes("multiple_counterparties_for_contract")) {
|
||||
return "dirty_unresolved";
|
||||
}
|
||||
return "special_valid";
|
||||
}
|
||||
function openContractNetBalanceDirectionLabel(amount) {
|
||||
if (amount > 0.005) {
|
||||
return "к получению";
|
||||
}
|
||||
if (amount < -0.005) {
|
||||
return "к оплате";
|
||||
}
|
||||
return "нетто закрыт";
|
||||
}
|
||||
function formatOpenContractComponentsSummary(components) {
|
||||
const kindOrder = [
|
||||
"receivable",
|
||||
"payable",
|
||||
"advance_issued",
|
||||
"advance_received",
|
||||
"other_receivable",
|
||||
"other_payable"
|
||||
];
|
||||
const ordered = [...components].sort((left, right) => kindOrder.indexOf(left.kind) - kindOrder.indexOf(right.kind));
|
||||
return ordered
|
||||
.map((component) => `${openContractSettlementKindLabel(component.kind)} ${formatMoneyRub(component.amount)}`)
|
||||
.join("; ");
|
||||
}
|
||||
function summarizeOpenContractSpecialReason(item) {
|
||||
if (item.category === "financial") {
|
||||
return "похоже на финансовый договор (кредит/банк)";
|
||||
}
|
||||
if (item.qualityFlags.includes("contract_identity_looks_like_counterparty")) {
|
||||
return "в поле договора похоже попал контрагент или чужая аналитика";
|
||||
}
|
||||
if (item.qualityFlags.includes("contract_identity_not_reliable")) {
|
||||
return "договор не похож на устойчивый договорный реквизит";
|
||||
}
|
||||
if (item.qualityFlags.includes("multiple_counterparties_for_contract")) {
|
||||
return "по одному договору найдено несколько контрагентов";
|
||||
}
|
||||
if (item.qualityFlags.includes("counterparty_not_reliably_resolved")) {
|
||||
return "не удалось надежно определить контрагента";
|
||||
}
|
||||
return "требуется ручная проверка карточки договора";
|
||||
}
|
||||
function buildOpenContractConfirmedBalanceAggregate(rows, asOfDate) {
|
||||
const byContract = new Map();
|
||||
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
|
||||
for (const row of rows) {
|
||||
const rowTimestamp = toUtcDayTimestamp(row.period);
|
||||
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
|
||||
continue;
|
||||
}
|
||||
const contract = extractContractName(row);
|
||||
if (!contract) {
|
||||
continue;
|
||||
}
|
||||
const amount = row.amount;
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||
continue;
|
||||
}
|
||||
const settlementKind = classifyOpenContractSettlementKind(row);
|
||||
if (!settlementKind) {
|
||||
continue;
|
||||
}
|
||||
const counterpartyCandidate = extractCounterpartyName(row);
|
||||
const counterparty = isLowQualityCounterpartyForContract(counterpartyCandidate, contract) ? null : counterpartyCandidate;
|
||||
const sourceRefs = extractPayablesSourceRefs(row, counterparty ?? contract, contract);
|
||||
const accountToken = normalizeDisplayAccountToken(row.account_dt) ?? normalizeDisplayAccountToken(row.account_kt);
|
||||
const absAmount = Math.abs(amount);
|
||||
const contractKey = normalizeEntityToken(contract);
|
||||
const counterpartyKey = counterparty ? normalizeEntityToken(counterparty) : "__unknown_counterparty__";
|
||||
const aggregateKey = `${contractKey}::${counterpartyKey}::${settlementKind}`;
|
||||
const current = byContract.get(aggregateKey);
|
||||
if (!current) {
|
||||
const qualityFlags = new Set();
|
||||
if (!counterparty) {
|
||||
qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
if (isLowQualityContractIdentity(contract, counterparty)) {
|
||||
qualityFlags.add("contract_identity_not_reliable");
|
||||
}
|
||||
if (counterparty && normalizeEntityToken(counterparty) === normalizeEntityToken(contract)) {
|
||||
qualityFlags.add("contract_identity_looks_like_counterparty");
|
||||
}
|
||||
const counterparties = new Set();
|
||||
if (counterparty) {
|
||||
counterparties.add(counterparty);
|
||||
}
|
||||
const accounts = new Set();
|
||||
if (accountToken) {
|
||||
accounts.add(accountToken);
|
||||
}
|
||||
byContract.set(aggregateKey, {
|
||||
contract,
|
||||
counterparty,
|
||||
confirmedAmount: absAmount,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
counterparties,
|
||||
settlementKind,
|
||||
accounts,
|
||||
sourceRefs: new Set(sourceRefs),
|
||||
qualityFlags
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.confirmedAmount += absAmount;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
if (counterparty) {
|
||||
current.counterparty = current.counterparty ?? counterparty;
|
||||
current.counterparties.add(counterparty);
|
||||
}
|
||||
else {
|
||||
current.qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
if (accountToken) {
|
||||
current.accounts.add(accountToken);
|
||||
}
|
||||
for (const ref of sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
return Array.from(byContract.values())
|
||||
.map((item) => {
|
||||
const counterparties = Array.from(item.counterparties);
|
||||
if (counterparties.length > 1) {
|
||||
item.qualityFlags.add("multiple_counterparties_for_contract");
|
||||
}
|
||||
return {
|
||||
contract: item.contract,
|
||||
counterparty: item.counterparty,
|
||||
confirmedAmount: item.confirmedAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
category: classifyOpenContractCategory(item.contract, counterparties, Array.from(item.qualityFlags)),
|
||||
settlementKind: item.settlementKind,
|
||||
accounts: Array.from(item.accounts).slice(0, 3),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
|
||||
qualityFlags: Array.from(item.qualityFlags)
|
||||
};
|
||||
})
|
||||
.filter((item) => item.confirmedAmount > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.confirmedAmount !== left.confirmedAmount) {
|
||||
return right.confirmedAmount - left.confirmedAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.contract.localeCompare(right.contract);
|
||||
});
|
||||
}
|
||||
function buildOpenContractNetAggregate(items) {
|
||||
const byContract = new Map();
|
||||
const categoryPriority = (value) => {
|
||||
if (value === "financial") {
|
||||
return 2;
|
||||
}
|
||||
if (value === "uncertain") {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
for (const item of items) {
|
||||
const counterpartyKey = item.counterparty ? normalizeEntityToken(item.counterparty) : "__unknown_counterparty__";
|
||||
const aggregateKey = `${normalizeEntityToken(item.contract)}::${counterpartyKey}`;
|
||||
const current = byContract.get(aggregateKey);
|
||||
if (!current) {
|
||||
byContract.set(aggregateKey, {
|
||||
contract: item.contract,
|
||||
counterparty: item.counterparty,
|
||||
category: item.category,
|
||||
netOpenBalance: openContractSettlementKindSign(item.settlementKind) * item.confirmedAmount,
|
||||
grossOpenBalance: item.confirmedAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
accounts: new Set(item.accounts),
|
||||
sourceRefs: new Set(item.sourceRefs),
|
||||
qualityFlags: new Set(item.qualityFlags),
|
||||
componentAmounts: new Map([[item.settlementKind, item.confirmedAmount]])
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (categoryPriority(item.category) > categoryPriority(current.category)) {
|
||||
current.category = item.category;
|
||||
}
|
||||
current.netOpenBalance += openContractSettlementKindSign(item.settlementKind) * item.confirmedAmount;
|
||||
current.grossOpenBalance += item.confirmedAmount;
|
||||
current.operations += item.operations;
|
||||
if ((item.firstPeriod ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = item.firstPeriod;
|
||||
}
|
||||
if ((item.lastPeriod ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = item.lastPeriod;
|
||||
}
|
||||
for (const account of item.accounts) {
|
||||
current.accounts.add(account);
|
||||
}
|
||||
for (const ref of item.sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
for (const flag of item.qualityFlags) {
|
||||
current.qualityFlags.add(flag);
|
||||
}
|
||||
current.componentAmounts.set(item.settlementKind, (current.componentAmounts.get(item.settlementKind) ?? 0) + item.confirmedAmount);
|
||||
}
|
||||
return Array.from(byContract.values())
|
||||
.map((item) => {
|
||||
const qualityFlags = Array.from(item.qualityFlags);
|
||||
return {
|
||||
contract: item.contract,
|
||||
counterparty: item.counterparty,
|
||||
category: item.category,
|
||||
reviewBucket: classifyOpenContractReviewBucket({
|
||||
category: item.category,
|
||||
qualityFlags
|
||||
}),
|
||||
netOpenBalance: item.netOpenBalance,
|
||||
grossOpenBalance: item.grossOpenBalance,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
accounts: Array.from(item.accounts).slice(0, 4),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
|
||||
qualityFlags,
|
||||
componentAmounts: Array.from(item.componentAmounts.entries())
|
||||
.map(([kind, amount]) => ({ kind, amount }))
|
||||
.filter((component) => component.amount > 0.005)
|
||||
};
|
||||
})
|
||||
.filter((item) => item.grossOpenBalance > 0.005)
|
||||
.sort((left, right) => {
|
||||
if (right.grossOpenBalance !== left.grossOpenBalance) {
|
||||
return right.grossOpenBalance - left.grossOpenBalance;
|
||||
}
|
||||
if (Math.abs(right.netOpenBalance) !== Math.abs(left.netOpenBalance)) {
|
||||
return Math.abs(right.netOpenBalance) - Math.abs(left.netOpenBalance);
|
||||
}
|
||||
return left.contract.localeCompare(right.contract);
|
||||
});
|
||||
}
|
||||
function buildOpenContractRiskAggregate(rows) {
|
||||
const byContract = new Map();
|
||||
for (const row of rows) {
|
||||
const contract = extractContractName(row);
|
||||
if (!contract) {
|
||||
continue;
|
||||
}
|
||||
const amountRaw = row.amount ?? 0;
|
||||
const amount = Number.isFinite(amountRaw) ? Math.abs(amountRaw) : 0;
|
||||
const current = byContract.get(contract);
|
||||
const counterpartyCandidate = extractCounterpartyName(row);
|
||||
const counterparty = isLowQualityCounterpartyForContract(counterpartyCandidate, contract) ? null : counterpartyCandidate;
|
||||
const sourceRefs = extractPayablesSourceRefs(row, counterparty ?? contract, contract);
|
||||
if (!current) {
|
||||
const qualityFlags = new Set();
|
||||
if (!counterparty) {
|
||||
qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
byContract.set(contract, {
|
||||
contract,
|
||||
totalAmount: amount,
|
||||
operations: 1,
|
||||
firstPeriod: row.period,
|
||||
lastPeriod: row.period,
|
||||
counterparties: new Set(counterparty ? [counterparty] : []),
|
||||
sourceRefs: new Set(sourceRefs),
|
||||
qualityFlags
|
||||
});
|
||||
continue;
|
||||
}
|
||||
current.totalAmount += amount;
|
||||
current.operations += 1;
|
||||
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
|
||||
current.firstPeriod = row.period;
|
||||
}
|
||||
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
|
||||
current.lastPeriod = row.period;
|
||||
}
|
||||
if (counterparty) {
|
||||
current.counterparties.add(counterparty);
|
||||
}
|
||||
else {
|
||||
current.qualityFlags.add("counterparty_not_reliably_resolved");
|
||||
}
|
||||
for (const ref of sourceRefs) {
|
||||
current.sourceRefs.add(ref);
|
||||
}
|
||||
}
|
||||
return Array.from(byContract.values())
|
||||
.map((item) => ({
|
||||
contract: item.contract,
|
||||
totalAmount: item.totalAmount,
|
||||
operations: item.operations,
|
||||
firstPeriod: item.firstPeriod,
|
||||
lastPeriod: item.lastPeriod,
|
||||
counterparties: Array.from(item.counterparties).slice(0, 2),
|
||||
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
|
||||
category: classifyOpenContractCategory(item.contract, Array.from(item.counterparties), Array.from(item.qualityFlags)),
|
||||
qualityFlags: Array.from(item.qualityFlags)
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (right.totalAmount !== left.totalAmount) {
|
||||
return right.totalAmount - left.totalAmount;
|
||||
}
|
||||
if (right.operations !== left.operations) {
|
||||
return right.operations - left.operations;
|
||||
}
|
||||
return left.contract.localeCompare(right.contract);
|
||||
});
|
||||
}
|
||||
function composeFactualReply(intent, rows, options = {}) {
|
||||
const applyNumericEmphasis = (line) => (options.emphasizeNumbers ? emphasizeNumericTokens(line) : line);
|
||||
const joinLines = (lines) => lines.map(applyNumericEmphasis).join("\n");
|
||||
@@ -2250,31 +2823,259 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (intent === "list_open_contracts") {
|
||||
const contracts = contractCandidatesFromRows(rows);
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
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 = [
|
||||
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
|
||||
`Строк движения: ${rows.length}.`,
|
||||
`Договорных кандидатов: ${contracts.length}.`
|
||||
`Собран подтвержденный срез товаров на складах на ${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 (contracts.length > 0) {
|
||||
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
|
||||
}
|
||||
else if (counterparties.length > 0) {
|
||||
lines.push(`Контрагентов с сигналом незакрытых хвостов: ${counterparties.length}.`);
|
||||
lines.push(...counterparties
|
||||
.slice(0, 8)
|
||||
.map((item, index) => `${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`));
|
||||
lines.push("Договорные якоря в этом live-срезе не выделены, поэтому показан контрагентный рейтинг риска.");
|
||||
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("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
|
||||
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);
|
||||
const contractProfiles = buildOpenContractNetAggregate(confirmedContracts);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const commercialContracts = confirmedContracts.filter((item) => item.category === "commercial");
|
||||
const commercialProfiles = contractProfiles.filter((item) => item.category === "commercial");
|
||||
const specialProfiles = contractProfiles.filter((item) => item.reviewBucket === "special_valid");
|
||||
const dirtyProfiles = contractProfiles.filter((item) => item.reviewBucket === "dirty_unresolved");
|
||||
const uniqueContracts = uniqueStrings(contractProfiles.map((item) => item.contract));
|
||||
const commercialReceivables = commercialContracts.filter((item) => item.settlementKind === "receivable");
|
||||
const commercialPayables = commercialContracts.filter((item) => item.settlementKind === "payable");
|
||||
const commercialAdvances = commercialContracts.filter((item) => item.settlementKind === "advance_issued" || item.settlementKind === "advance_received");
|
||||
const commercialOther = commercialContracts.filter((item) => item.settlementKind === "other_receivable" || item.settlementKind === "other_payable");
|
||||
const sumConfirmedAmount = (items) => items.reduce((sum, item) => sum + item.confirmedAmount, 0);
|
||||
const sumNetAmount = (items) => items.reduce((sum, item) => sum + item.netOpenBalance, 0);
|
||||
const sumGrossAmount = (items) => items.reduce((sum, item) => sum + item.grossOpenBalance, 0);
|
||||
const commercialNetTotal = sumNetAmount(commercialProfiles);
|
||||
const commercialGrossTotal = sumGrossAmount(commercialProfiles);
|
||||
const specialTotal = sumGrossAmount(specialProfiles);
|
||||
const dirtyTotal = sumGrossAmount(dirtyProfiles);
|
||||
const periodScopeLine = !options.asOfDate && (periodFrom || periodTo)
|
||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||
: null;
|
||||
const renderContractProfileLines = (items, includeSpecialReason) => items.slice(0, 12).map((item, index) => {
|
||||
const counterpartyLabel = item.counterparty ?? "контрагент не определен";
|
||||
const accountsLabel = item.accounts.length > 0 ? ` | через счета: ${item.accounts.join("; ")}` : "";
|
||||
const evidenceLabel = item.sourceRefs.length > 0 ? ` | основное основание: ${item.sourceRefs[0]}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 1 ? ` | source refs: ${item.sourceRefs.slice(1, 3).join("; ")}` : "";
|
||||
const specialReasonLabel = includeSpecialReason
|
||||
? ` | причина вынесения: ${summarizeOpenContractSpecialReason(item)}`
|
||||
: "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartyLabel} | чистый остаток: ${openContractNetBalanceDirectionLabel(item.netOpenBalance)} ${formatMoneyRub(Math.abs(item.netOpenBalance))} | брутто компонентов: ${formatMoneyRub(item.grossOpenBalance)} | состав: ${formatOpenContractComponentsSummary(item.componentAmounts)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${accountsLabel}${evidenceLabel}${refsLabel}${specialReasonLabel}`;
|
||||
});
|
||||
const renderConfirmedContractLines = (items, includeSpecialReason) => items.slice(0, 12).map((item, index) => {
|
||||
const counterpartyLabel = item.counterparty ?? "контрагент не определен";
|
||||
const accountsLabel = item.accounts.length > 0 ? ` | счета: ${item.accounts.join("; ")}` : "";
|
||||
const evidenceLabel = item.sourceRefs.length > 0 ? ` | основное основание: ${item.sourceRefs[0]}` : "";
|
||||
const refsLabel = item.sourceRefs.length > 1 ? ` | source refs: ${item.sourceRefs.slice(1, 3).join("; ")}` : "";
|
||||
const specialReasonLabel = includeSpecialReason
|
||||
? ` | причина вынесения: ${summarizeOpenContractSpecialReason(item)}`
|
||||
: "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartyLabel} | подтвержденный открытый остаток: ${formatMoneyRub(item.confirmedAmount)} | тип остатка: ${openContractSettlementKindLabel(item.settlementKind)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${accountsLabel}${evidenceLabel}${refsLabel}${specialReasonLabel}`;
|
||||
});
|
||||
const lines = [
|
||||
`Собран подтвержденный срез открытых договоров на ${formatDateRu(asOfDate)}.`,
|
||||
`Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`,
|
||||
`Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`,
|
||||
`Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`,
|
||||
`Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
|
||||
"- База ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.",
|
||||
"- Управленческий вид: по каждому договору показаны чистый остаток и состав по типам открытых расчетов.",
|
||||
"- Базовая единица детализации: одна строка = один договор, один контрагент и один тип открытого остатка."
|
||||
];
|
||||
lines.push("");
|
||||
lines.push("Блок 2. Что учтено");
|
||||
lines.push(`- Дата среза: ${formatDateRu(asOfDate)}.`);
|
||||
if (periodScopeLine) {
|
||||
lines.push(periodScopeLine);
|
||||
}
|
||||
lines.push("- Дефолтная бизнес-дефиниция: открыт договор, по которому на дату есть ненулевой остаток взаиморасчетов.");
|
||||
lines.push("- Контур: остатки по счетам 60/62/76.");
|
||||
lines.push("- Смешанные экономические смыслы не склеиваются: дебиторка, кредиторка, авансы и прочие остатки показаны раздельно.");
|
||||
lines.push("");
|
||||
lines.push("Блок 3. Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Уникальных договоров: ${formatNumberWithDots(uniqueContracts.length)}.`);
|
||||
lines.push(`- Подтвержденных договор-контрагент профилей: ${formatNumberWithDots(contractProfiles.length)}.`);
|
||||
lines.push(`- Подтвержденных договорных компонентов: ${formatNumberWithDots(confirmedContracts.length)}.`);
|
||||
lines.push(`- Чистый коммерческий остаток: ${openContractNetBalanceDirectionLabel(commercialNetTotal)} ${formatMoneyRub(Math.abs(commercialNetTotal))}.`);
|
||||
lines.push(`- Брутто коммерческих компонентов: ${formatMoneyRub(commercialGrossTotal)}.`);
|
||||
lines.push(`- Коммерческая дебиторка: ${formatNumberWithDots(commercialReceivables.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialReceivables))}.`);
|
||||
lines.push(`- Коммерческая кредиторка: ${formatNumberWithDots(commercialPayables.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialPayables))}.`);
|
||||
lines.push(`- Коммерческие авансы: ${formatNumberWithDots(commercialAdvances.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialAdvances))}.`);
|
||||
lines.push(`- Прочие расчеты по 76: ${formatNumberWithDots(commercialOther.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialOther))}.`);
|
||||
lines.push(`- Специальные финансовые позиции: ${formatNumberWithDots(specialProfiles.length)} на ${formatMoneyRub(specialTotal)}.`);
|
||||
lines.push(`- Спорные/некачественно нормализованные позиции: ${formatNumberWithDots(dirtyProfiles.length)} на ${formatMoneyRub(dirtyTotal)}.`);
|
||||
if (commercialProfiles.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Чистый открытый остаток по договорам");
|
||||
lines.push(...renderContractProfileLines(commercialProfiles, false));
|
||||
}
|
||||
if (commercialReceivables.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Коммерческие дебиторские компоненты");
|
||||
lines.push(...renderConfirmedContractLines(commercialReceivables, false));
|
||||
}
|
||||
if (commercialPayables.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 6. Коммерческие кредиторские компоненты");
|
||||
lines.push(...renderConfirmedContractLines(commercialPayables, false));
|
||||
}
|
||||
if (commercialAdvances.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 7. Коммерческие авансовые компоненты");
|
||||
lines.push(...renderConfirmedContractLines(commercialAdvances, false));
|
||||
}
|
||||
if (commercialOther.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 8. Прочие компоненты по 76");
|
||||
lines.push(...renderConfirmedContractLines(commercialOther, false));
|
||||
}
|
||||
if (specialProfiles.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 9. Финансовые/специальные позиции");
|
||||
lines.push(...renderContractProfileLines(specialProfiles, true));
|
||||
}
|
||||
if (dirtyProfiles.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 10. Спорные/некачественно нормализованные позиции");
|
||||
lines.push(...renderContractProfileLines(dirtyProfiles, true));
|
||||
}
|
||||
if (confirmedContracts.length === 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Подтвержденные позиции");
|
||||
lines.push("- На дату среза подтвержденные договоры с открытыми взаиморасчетами не найдены.");
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "confirmed_balance",
|
||||
evidence_strength: "strong",
|
||||
balance_confirmed: true
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "list_open_contracts") {
|
||||
const contracts = buildOpenContractRiskAggregate(rows);
|
||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate ?? options.periodTo ?? options.periodFrom);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
const commercialContracts = contracts.filter((item) => item.category === "commercial");
|
||||
const specialContracts = contracts.filter((item) => item.category !== "commercial");
|
||||
const commercialTotal = commercialContracts.reduce((sum, item) => sum + item.totalAmount, 0);
|
||||
const lines = [
|
||||
`Итого по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""}: ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`,
|
||||
"",
|
||||
"Блок 1. Статус результата",
|
||||
"- Результат: предварительный список договоров с возможными незакрытыми расчетами.",
|
||||
"- Перед финансовым решением нужна сверка карточек договоров и взаиморасчетов в 1С.",
|
||||
"",
|
||||
"Блок 2. Что учтено",
|
||||
...(asOfDate
|
||||
? [`- Дата среза: ${formatDateRu(asOfDate)}.`]
|
||||
: periodFrom || periodTo
|
||||
? [`- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`]
|
||||
: []),
|
||||
"- Контур: движения по счетам 60/62/76 и договорная аналитика.",
|
||||
"",
|
||||
"Блок 3. Сводка",
|
||||
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||
`- Договоров-кандидатов всего: ${formatNumberWithDots(contracts.length)}.`,
|
||||
`- Основной список (коммерческие): ${formatNumberWithDots(commercialContracts.length)}.`,
|
||||
`- Вынесено в финансовые/спорные: ${formatNumberWithDots(specialContracts.length)}.`
|
||||
];
|
||||
if (commercialContracts.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Основной список (коммерческие договоры)");
|
||||
lines.push(...commercialContracts.slice(0, 10).map((item, index) => {
|
||||
const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
|
||||
const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartiesLabel} | сумма возможного открытого остатка: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""} | почему в списке: есть признаки незакрытых расчетов на дату${sourceRefsSuffix}`;
|
||||
}));
|
||||
if (specialContracts.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 5. Финансовые/спорные позиции (вынесены отдельно)");
|
||||
lines.push(...specialContracts.slice(0, 8).map((item, index) => {
|
||||
const counterpartiesLabel = item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
|
||||
const sourceRefsSuffix = item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
|
||||
return `${index + 1}. ${item.contract} | контрагент: ${counterpartiesLabel} | сумма сигнала: ${formatMoneyRub(item.totalAmount)} | причина вынесения: ${summarizeOpenContractSpecialReason(item)}${sourceRefsSuffix}`;
|
||||
}));
|
||||
}
|
||||
}
|
||||
else if (counterparties.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов");
|
||||
lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`);
|
||||
lines.push(...counterparties
|
||||
.slice(0, 8)
|
||||
.map((item, index) => `${index + 1}. ${item.name} | сумма сигнала: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`));
|
||||
lines.push("- Договорные реквизиты выделены недостаточно надежно, поэтому показан контрагентный список для проверки.");
|
||||
}
|
||||
else {
|
||||
lines.push("");
|
||||
lines.push("Блок 4. Позиции не выделены");
|
||||
lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации.");
|
||||
lines.push("Блок 5. Примеры исходных строк");
|
||||
lines.push(...formatTopRows(rows, 6));
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
text: joinLines(lines),
|
||||
semantics: {
|
||||
result_mode: "heuristic_candidates",
|
||||
evidence_strength: contracts.length > 0 || counterparties.length > 0 ? "medium" : "weak",
|
||||
balance_confirmed: false
|
||||
}
|
||||
};
|
||||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
|
||||
@@ -22,7 +22,7 @@ function hasAllTimeHint(text) {
|
||||
return /(?:за\s+вс[её]\s+время|за\s+весь\s+период|за\s+весь\s+срок|за\s+всю\s+истори(?:ю|и)|за\s+любой\s+период|за\s+любой\s+срок|for\s+all\s+time|all\s+time|for\s+entire\s+period|entire\s+period|for\s+any\s+period|any\s+period|for\s+full\s+history|full\s+history)/iu.test(normalized);
|
||||
}
|
||||
function hasSameDateHint(text) {
|
||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
function hasExplicitPeriodLiteral(text) {
|
||||
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
|
||||
@@ -286,6 +286,20 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const merged = { ...current };
|
||||
const reasons = [];
|
||||
if (!followupContext) {
|
||||
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(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 };
|
||||
}
|
||||
const previous = followupContext.previous_filters ?? {};
|
||||
@@ -374,6 +388,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
}
|
||||
if (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") {
|
||||
@@ -442,6 +458,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
|
||||
const asOfPrimaryIntent = 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";
|
||||
@@ -474,12 +492,28 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
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(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 };
|
||||
}
|
||||
function resolveMissingRequiredFilters(intent, filters) {
|
||||
const requiredByIntent = {
|
||||
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"],
|
||||
vat_payable_confirmed_as_of_date: ["as_of_date"],
|
||||
@@ -527,7 +561,8 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_followup_context"]
|
||||
};
|
||||
}
|
||||
if (hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
||||
const allowOpenItemsFollowupFallback = detectedIntent.intent === "unknown" && !isVatFollowup;
|
||||
if (allowOpenItemsFollowupFallback && hasOpenItemsHint(normalizedMessage) && hasAnyPartyAnchor) {
|
||||
return {
|
||||
intent: "open_items_by_counterparty_or_contract",
|
||||
confidence: "low",
|
||||
|
||||
@@ -93,6 +93,7 @@ function inferAggregationProfile(intent, shape) {
|
||||
}
|
||||
if (intent === "account_balance_snapshot" ||
|
||||
intent === "documents_forming_balance" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_confirmed_as_of_date" ||
|
||||
|
||||
Reference in New Issue
Block a user