fix(treasury): apply a full receipt or payment to the party's open balance
A settled document of the same amount no longer leaves an existing remaining unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -129,6 +129,29 @@ export class TreasuryController {
|
||||
return this.treasury.partyBalances(user, kind === 'expense' ? 'expense' : 'income');
|
||||
}
|
||||
|
||||
@Post('party-balances/collect')
|
||||
collectPartyBalance(
|
||||
@CurrentUser() user: Actor,
|
||||
@Body()
|
||||
body: {
|
||||
kind?: string;
|
||||
partyKind?: 'person' | 'company' | 'user';
|
||||
partyId?: string;
|
||||
amount?: string | number;
|
||||
accountId?: string;
|
||||
occurredAt?: string;
|
||||
},
|
||||
) {
|
||||
return this.treasury.collectPartyBalance(user, {
|
||||
kind: body.kind === 'expense' ? 'expense' : 'income',
|
||||
partyKind: body.partyKind ?? 'person',
|
||||
partyId: body.partyId ?? '',
|
||||
amount: body.amount ?? 0,
|
||||
accountId: body.accountId ?? '',
|
||||
occurredAt: body.occurredAt,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('documents')
|
||||
documents(
|
||||
@CurrentUser() user: Actor,
|
||||
|
||||
@@ -561,7 +561,69 @@ export class TreasuryService {
|
||||
}
|
||||
}
|
||||
const bookedAt = startOfDay(input.bookedAt) ?? new Date();
|
||||
const payExisting =
|
||||
party.partyKind !== 'other' && settle.equals(amount) && settle.greaterThan(0);
|
||||
const doc = await this.prisma.$transaction(async (tx) => {
|
||||
let amountLeft = amount;
|
||||
let settleLeft = settle;
|
||||
let touchedId = '';
|
||||
if (payExisting && input.accountId) {
|
||||
const scope = await this.accountScope(actor);
|
||||
const openDocs = await tx.financialDocument.findMany({
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
kind: input.kind,
|
||||
voidedAt: null,
|
||||
status: { in: ['open', 'partial'] },
|
||||
partyKind: party.partyKind,
|
||||
...(party.personId ? { personId: party.personId } : {}),
|
||||
...(party.companyId ? { companyId: party.companyId } : {}),
|
||||
...(party.userId ? { userId: party.userId } : {}),
|
||||
...(scope ? { OR: [{ accountId: null }, { accountId: { in: scope } }] } : {}),
|
||||
},
|
||||
include: { movements: { select: { amount: true, voidedAt: true } } },
|
||||
orderBy: [{ bookedAt: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
for (const row of openDocs) {
|
||||
if (!settleLeft.greaterThan(0)) break;
|
||||
const remaining = row.amount.minus(this.settledOf(row.movements));
|
||||
if (!remaining.greaterThan(0)) continue;
|
||||
const slice = settleLeft.lessThan(remaining) ? settleLeft : remaining;
|
||||
const status = slice.equals(remaining) ? 'settled' : 'partial';
|
||||
await tx.financialMovement.create({
|
||||
data: {
|
||||
organizationId: actor.organizationId,
|
||||
accountId: input.accountId,
|
||||
documentId: row.id,
|
||||
direction: input.kind === 'income' ? 'in' : 'out',
|
||||
amount: slice,
|
||||
kind: input.kind === 'income' ? 'receipt' : 'payment',
|
||||
occurredAt: startOfDay(input.settledAt ?? undefined) ?? bookedAt,
|
||||
memo: title,
|
||||
createdById: actor.userId,
|
||||
},
|
||||
});
|
||||
await tx.financialDocument.update({
|
||||
where: { id: row.id },
|
||||
data: { status, accountId: input.accountId },
|
||||
});
|
||||
if (status === 'settled') {
|
||||
await tx.financialRecurringOccurrence.updateMany({
|
||||
where: { documentId: row.id, status: 'pending' },
|
||||
data: { status: 'confirmed' },
|
||||
});
|
||||
}
|
||||
touchedId = row.id;
|
||||
settleLeft = settleLeft.minus(slice);
|
||||
amountLeft = amountLeft.minus(slice);
|
||||
}
|
||||
}
|
||||
if (!amountLeft.greaterThan(0)) {
|
||||
return tx.financialDocument.findUniqueOrThrow({
|
||||
where: { id: touchedId },
|
||||
include: { category: true, account: true, movements: true },
|
||||
});
|
||||
}
|
||||
const reference = await this.nextDocumentReference(
|
||||
tx,
|
||||
actor.organizationId,
|
||||
@@ -574,7 +636,7 @@ export class TreasuryService {
|
||||
organizationId: actor.organizationId,
|
||||
kind: input.kind,
|
||||
title,
|
||||
amount,
|
||||
amount: amountLeft,
|
||||
bookedAt,
|
||||
dueAt: input.dueAt ? startOfDay(input.dueAt) : null,
|
||||
categoryId: input.categoryId || null,
|
||||
@@ -585,20 +647,20 @@ export class TreasuryService {
|
||||
partyName: party.partyName,
|
||||
accountId: input.accountId || null,
|
||||
method: input.method || null,
|
||||
status: settle.equals(0) ? 'open' : settle.equals(amount) ? 'settled' : 'partial',
|
||||
status: settleLeft.equals(0) ? 'open' : settleLeft.equals(amountLeft) ? 'settled' : 'partial',
|
||||
reference,
|
||||
notes: input.notes?.trim() || null,
|
||||
createdById: actor.userId,
|
||||
},
|
||||
});
|
||||
if (settle.greaterThan(0) && input.accountId) {
|
||||
if (settleLeft.greaterThan(0) && input.accountId) {
|
||||
await tx.financialMovement.create({
|
||||
data: {
|
||||
organizationId: actor.organizationId,
|
||||
accountId: input.accountId,
|
||||
documentId: created.id,
|
||||
direction: input.kind === 'income' ? 'in' : 'out',
|
||||
amount: settle,
|
||||
amount: settleLeft,
|
||||
kind: input.kind === 'income' ? 'receipt' : 'payment',
|
||||
occurredAt: startOfDay(input.settledAt ?? undefined) ?? bookedAt,
|
||||
memo: title,
|
||||
@@ -728,6 +790,7 @@ export class TreasuryService {
|
||||
amount: Prisma.Decimal;
|
||||
settled: Prisma.Decimal;
|
||||
remaining: Prisma.Decimal;
|
||||
documentRemaining: Prisma.Decimal;
|
||||
}>();
|
||||
for (const row of rows) {
|
||||
if (row.partyKind === 'other') continue;
|
||||
@@ -748,10 +811,12 @@ export class TreasuryService {
|
||||
amount: new Prisma.Decimal(0),
|
||||
settled: new Prisma.Decimal(0),
|
||||
remaining: new Prisma.Decimal(0),
|
||||
documentRemaining: new Prisma.Decimal(0),
|
||||
};
|
||||
current.amount = current.amount.plus(row.amount);
|
||||
current.settled = current.settled.plus(settled);
|
||||
current.remaining = current.remaining.plus(rawRemaining);
|
||||
current.documentRemaining = current.documentRemaining.plus(rawRemaining);
|
||||
buckets.set(key, current);
|
||||
}
|
||||
|
||||
@@ -770,6 +835,7 @@ export class TreasuryService {
|
||||
amount: new Prisma.Decimal(0),
|
||||
settled: new Prisma.Decimal(0),
|
||||
remaining: new Prisma.Decimal(0),
|
||||
documentRemaining: new Prisma.Decimal(0),
|
||||
};
|
||||
current.amount = current.amount.plus(amount);
|
||||
current.settled = current.settled.plus(settled);
|
||||
@@ -790,9 +856,122 @@ export class TreasuryService {
|
||||
amount: money(row.amount),
|
||||
settledAmount: money(row.settled),
|
||||
remaining: money(row.remaining),
|
||||
documentRemaining: money(row.documentRemaining),
|
||||
}));
|
||||
}
|
||||
|
||||
/** دریافت یا پرداختِ بخشی از ماندهٔ سندهای باز یک طرف حساب، بدون ساخت سند جدید. */
|
||||
async collectPartyBalance(
|
||||
actor: Actor,
|
||||
input: {
|
||||
kind: 'income' | 'expense';
|
||||
partyKind: 'person' | 'company' | 'user';
|
||||
partyId: string;
|
||||
amount: string | number;
|
||||
accountId: string;
|
||||
occurredAt?: string;
|
||||
},
|
||||
): Promise<{ applied: string; documentCount: number }> {
|
||||
const amount = dec(input.amount);
|
||||
if (!amount.greaterThan(0)) throw new BadRequestException('مبلغ باید بزرگتر از صفر باشد.');
|
||||
const partyId = input.partyId.trim();
|
||||
if (!partyId) throw new BadRequestException('طرف حساب را انتخاب کنید.');
|
||||
if (input.partyKind !== 'person' && input.partyKind !== 'company' && input.partyKind !== 'user') {
|
||||
throw new BadRequestException('این مانده به سند بازی وصل نیست.');
|
||||
}
|
||||
if (!input.accountId.trim()) throw new BadRequestException('حساب را انتخاب کنید.');
|
||||
const account = await this.requireAccount(actor.organizationId, input.accountId, actor);
|
||||
if (!account.isActive) throw new BadRequestException('حساب غیرفعال است.');
|
||||
const scope = await this.accountScope(actor);
|
||||
const docs = await this.prisma.financialDocument.findMany({
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
kind: input.kind,
|
||||
voidedAt: null,
|
||||
status: { in: ['open', 'partial'] },
|
||||
partyKind: input.partyKind,
|
||||
...(input.partyKind === 'person' ? { personId: partyId } : {}),
|
||||
...(input.partyKind === 'company' ? { companyId: partyId } : {}),
|
||||
...(input.partyKind === 'user' ? { userId: partyId } : {}),
|
||||
...(scope ? { OR: [{ accountId: null }, { accountId: { in: scope } }] } : {}),
|
||||
},
|
||||
include: { movements: { select: { amount: true, voidedAt: true } } },
|
||||
orderBy: [{ bookedAt: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
const open = docs
|
||||
.map((doc) => {
|
||||
const remaining = doc.amount.minus(this.settledOf(doc.movements));
|
||||
return { doc, remaining };
|
||||
})
|
||||
.filter((row) => row.remaining.greaterThan(0));
|
||||
const available = open.reduce((sum, row) => sum.plus(row.remaining), new Prisma.Decimal(0));
|
||||
if (!available.greaterThan(0)) {
|
||||
throw new BadRequestException(
|
||||
'سند بازی برای کم کردن این مانده نیست. اگر مانده از فاکتور است، دریافت را روی خود فاکتور ثبت کنید.',
|
||||
);
|
||||
}
|
||||
if (amount.greaterThan(available)) {
|
||||
throw new BadRequestException('مبلغ از ماندهٔ سندهای باز این طرف حساب بیشتر است.');
|
||||
}
|
||||
if (input.kind === 'expense') {
|
||||
const balance = await this.accountBalance(account.id);
|
||||
if (balance.lessThan(amount)) {
|
||||
throw new BadRequestException('موجودی حساب برای این پرداخت کافی نیست.');
|
||||
}
|
||||
}
|
||||
const occurredAt = startOfDay(input.occurredAt) ?? new Date();
|
||||
let left = amount;
|
||||
const touched: string[] = [];
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
for (const row of open) {
|
||||
if (!left.greaterThan(0)) break;
|
||||
const slice = left.lessThan(row.remaining) ? left : row.remaining;
|
||||
const status = slice.equals(row.remaining) ? 'settled' : 'partial';
|
||||
await tx.financialMovement.create({
|
||||
data: {
|
||||
organizationId: actor.organizationId,
|
||||
accountId: account.id,
|
||||
documentId: row.doc.id,
|
||||
direction: input.kind === 'income' ? 'in' : 'out',
|
||||
amount: slice,
|
||||
kind: input.kind === 'income' ? 'receipt' : 'payment',
|
||||
occurredAt,
|
||||
memo: input.kind === 'income' ? 'دریافت از مانده' : 'پرداخت از مانده',
|
||||
createdById: actor.userId,
|
||||
},
|
||||
});
|
||||
await tx.financialDocument.update({
|
||||
where: { id: row.doc.id },
|
||||
data: { status, accountId: account.id },
|
||||
});
|
||||
if (status === 'settled') {
|
||||
await tx.financialRecurringOccurrence.updateMany({
|
||||
where: { documentId: row.doc.id, status: 'pending' },
|
||||
data: { status: 'confirmed' },
|
||||
});
|
||||
}
|
||||
touched.push(row.doc.id);
|
||||
left = left.minus(slice);
|
||||
}
|
||||
});
|
||||
await this.prisma.financialAuditLog.create({
|
||||
data: {
|
||||
organizationId: actor.organizationId,
|
||||
entityType: 'document',
|
||||
entityId: touched[0] ?? partyId,
|
||||
action: 'settle',
|
||||
after: {
|
||||
amount: amount.toFixed(2),
|
||||
accountId: account.id,
|
||||
documentIds: touched,
|
||||
partyId,
|
||||
},
|
||||
actorUserId: actor.userId,
|
||||
},
|
||||
}).catch(() => undefined);
|
||||
return { applied: money(amount), documentCount: touched.length };
|
||||
}
|
||||
|
||||
private async resolveParty(
|
||||
actor: Actor,
|
||||
input: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
@@ -142,6 +142,19 @@ async function createDoc() {
|
||||
error.value = 'برای سایر، مبلغ پرداختشده باید با مبلغ کل یکی باشد.';
|
||||
return;
|
||||
}
|
||||
const partyKey =
|
||||
form.value.partyKind && form.value.partyKind !== 'other' && form.value.partyId
|
||||
? `${form.value.partyKind}:${form.value.partyId}`
|
||||
: '';
|
||||
const openBalance = partyKey
|
||||
? Number(
|
||||
balances.value.find((row) => row.key === partyKey)?.documentRemaining ||
|
||||
balances.value.find((row) => row.key === partyKey)?.remaining ||
|
||||
0,
|
||||
)
|
||||
: 0;
|
||||
const collectsBalance =
|
||||
Boolean(partyKey) && settleAmount === amount && settleAmount > 0 && openBalance > 0;
|
||||
const { data } = await api.post<FinancialDocumentDto>('/treasury/documents', {
|
||||
kind,
|
||||
title: form.value.title,
|
||||
@@ -168,9 +181,11 @@ async function createDoc() {
|
||||
method: 'transfer',
|
||||
notes: '',
|
||||
};
|
||||
ok.value = data.reference
|
||||
? `سند ثبت شد. شماره پیگیری: ${data.reference}`
|
||||
: 'سند ثبت شد.';
|
||||
ok.value = collectsBalance
|
||||
? `${toman(settleAmount)} از ماندهٔ طرف حساب کم شد.`
|
||||
: data.reference
|
||||
? `سند ثبت شد. شماره پیگیری: ${data.reference}`
|
||||
: 'سند ثبت شد.';
|
||||
await load();
|
||||
} catch (e) {
|
||||
error.value = apiMessage(e);
|
||||
@@ -179,6 +194,25 @@ async function createDoc() {
|
||||
}
|
||||
}
|
||||
|
||||
const partyRules = [
|
||||
{
|
||||
validator: async () => {
|
||||
if (!form.value.partyKind) throw new Error('طرف حساب الزامی است');
|
||||
if (form.value.partyKind !== 'other' && !form.value.partyId) {
|
||||
throw new Error('طرف حساب را انتخاب کنید');
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const partyRemainingLabel = computed(() => {
|
||||
const kind = form.value.partyKind;
|
||||
const id = form.value.partyId;
|
||||
if (!kind || kind === 'other' || !id) return '';
|
||||
const row = balances.value.find((item) => item.key === `${kind}:${id}`);
|
||||
return `مانده ${toman(row?.remaining || 0)}`;
|
||||
});
|
||||
|
||||
async function voidDoc(id: string) {
|
||||
if (!window.confirm('این سند و گردش متصل به آن باطل شود؟')) return;
|
||||
try {
|
||||
@@ -210,6 +244,18 @@ onMounted(() => {
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="طرف حساب" name="partyKind" :rules="partyRules">
|
||||
<a-row :gutter="8" align="middle">
|
||||
<a-col :span="partyRemainingLabel ? 16 : 24">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
</a-col>
|
||||
<a-col v-if="partyRemainingLabel" :span="8">
|
||||
<a-typography-text type="secondary">{{ partyRemainingLabel }}</a-typography-text>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item
|
||||
label="عنوان"
|
||||
@@ -247,11 +293,6 @@ onMounted(() => {
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="طرف حساب" name="partyKind">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="حساب" name="accountId">
|
||||
<a-select show-search option-filter-prop="label"
|
||||
|
||||
@@ -121,6 +121,25 @@ const formTabOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const partyRules = [
|
||||
{
|
||||
validator: async () => {
|
||||
if (!form.value.partyKind) throw new Error('طرف حساب الزامی است');
|
||||
if (form.value.partyKind !== 'other' && !form.value.partyId) {
|
||||
throw new Error('طرف حساب را انتخاب کنید');
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const partyRemainingLabel = computed(() => {
|
||||
const kind = form.value.partyKind;
|
||||
const id = form.value.partyId;
|
||||
if (!kind || kind === 'other' || !id) return '';
|
||||
const row = balances.value.find((item) => item.key === `${kind}:${id}`);
|
||||
return `مانده ${toman(row?.remaining || 0)}`;
|
||||
});
|
||||
|
||||
const remainingAmount = computed(() => {
|
||||
const total = parseMoneyNumber(String(form.value.amount ?? ''), 0) ?? 0;
|
||||
const settled = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
@@ -203,6 +222,19 @@ async function createDoc() {
|
||||
error.value = 'برای سایر، مبلغ دریافتشده باید با مبلغ کل یکی باشد.';
|
||||
return;
|
||||
}
|
||||
const partyKey =
|
||||
form.value.partyKind && form.value.partyKind !== 'other' && form.value.partyId
|
||||
? `${form.value.partyKind}:${form.value.partyId}`
|
||||
: '';
|
||||
const openBalance = partyKey
|
||||
? Number(
|
||||
balances.value.find((row) => row.key === partyKey)?.documentRemaining ||
|
||||
balances.value.find((row) => row.key === partyKey)?.remaining ||
|
||||
0,
|
||||
)
|
||||
: 0;
|
||||
const collectsBalance =
|
||||
Boolean(partyKey) && settleAmount === amount && settleAmount > 0 && openBalance > 0;
|
||||
const { data } = await api.post<FinancialDocumentDto>('/treasury/documents', {
|
||||
kind,
|
||||
title: form.value.title,
|
||||
@@ -219,9 +251,11 @@ async function createDoc() {
|
||||
bookedAt: form.value.bookedAt || undefined,
|
||||
});
|
||||
resetForm();
|
||||
ok.value = data.reference
|
||||
? `سند ثبت شد. شماره پیگیری: ${data.reference}`
|
||||
: 'سند ثبت شد.';
|
||||
ok.value = collectsBalance
|
||||
? `${toman(settleAmount)} از ماندهٔ طرف حساب کم شد.`
|
||||
: data.reference
|
||||
? `سند ثبت شد. شماره پیگیری: ${data.reference}`
|
||||
: 'سند ثبت شد.';
|
||||
await load();
|
||||
} catch (e) {
|
||||
error.value = apiMessage(e);
|
||||
@@ -358,6 +392,16 @@ onMounted(() => {
|
||||
<a-card :bordered="false">
|
||||
<a-form ref="mobileFormRef" layout="vertical" :model="form" @finish="createDoc">
|
||||
<div v-show="formTab === 'main'">
|
||||
<a-form-item label="طرف حساب" name="partyKind" :rules="partyRules">
|
||||
<a-row :gutter="8" align="middle">
|
||||
<a-col :span="partyRemainingLabel ? 16 : 24">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
</a-col>
|
||||
<a-col v-if="partyRemainingLabel" :span="8">
|
||||
<a-typography-text type="secondary">{{ partyRemainingLabel }}</a-typography-text>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="عنوان درآمد"
|
||||
name="title"
|
||||
@@ -372,12 +416,7 @@ onMounted(() => {
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="[12, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="طرف حساب" name="partyKind">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-col :span="24">
|
||||
<a-form-item name="method">
|
||||
<template #label>
|
||||
<a-space :size="6">
|
||||
@@ -599,6 +638,18 @@ onMounted(() => {
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item label="طرف حساب" name="partyKind" :rules="partyRules">
|
||||
<a-row :gutter="8" align="middle">
|
||||
<a-col :span="partyRemainingLabel ? 16 : 24">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
</a-col>
|
||||
<a-col v-if="partyRemainingLabel" :span="8">
|
||||
<a-typography-text type="secondary">{{ partyRemainingLabel }}</a-typography-text>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-form-item
|
||||
label="عنوان"
|
||||
@@ -636,11 +687,6 @@ onMounted(() => {
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="طرف حساب" name="partyKind">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="حساب" name="accountId">
|
||||
<a-select show-search option-filter-prop="label"
|
||||
|
||||
@@ -6630,6 +6630,8 @@ export interface TreasuryPartyBalanceDto {
|
||||
amount: string;
|
||||
settledAmount: string;
|
||||
remaining: string;
|
||||
/** بخشی از مانده که با دریافت/پرداخت روی سندهای باز کم میشود */
|
||||
documentRemaining: string;
|
||||
}
|
||||
|
||||
export interface TreasuryDashboardDto {
|
||||
|
||||
Reference in New Issue
Block a user