fix(treasury): show a party's net balance as debtor or creditor
Income and expense forms now use the same receivable-minus-payable figure beside the party. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,7 +30,7 @@ import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
|
||||
import TreasuryPartySelect from '@/components/treasury/TreasuryPartySelect.vue';
|
||||
import { useSettleDefault } from '@/components/treasury/useSettleDefault';
|
||||
import { toman } from './treasury-format';
|
||||
import { partyNetPosition, toman } from './treasury-format';
|
||||
import { parseMoneyNumber } from '@/lib/money';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
@@ -41,6 +41,7 @@ const ASelectOption = ASelect.Option;
|
||||
const kind = 'expense' as const;
|
||||
const rows = ref<FinancialDocumentDto[]>([]);
|
||||
const balances = ref<TreasuryPartyBalanceDto[]>([]);
|
||||
const receivableBalances = ref<TreasuryPartyBalanceDto[]>([]);
|
||||
const categories = ref<FinancialCategoryDto[]>([]);
|
||||
const accounts = ref<FinancialAccountDto[]>([]);
|
||||
const error = ref('');
|
||||
@@ -94,7 +95,7 @@ function docLine(item: FinancialDocumentDto) {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const [docs, cats, acc, bals] = await Promise.all([
|
||||
const [docs, cats, acc, bals, receivables] = await Promise.all([
|
||||
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
|
||||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
|
||||
@@ -112,9 +113,14 @@ async function load() {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
||||
throw e;
|
||||
}),
|
||||
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind: 'income' } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
||||
throw e;
|
||||
}),
|
||||
]);
|
||||
rows.value = docs.data;
|
||||
balances.value = bals.data;
|
||||
receivableBalances.value = receivables.data;
|
||||
categories.value = cats.data.filter((row) => row.isActive);
|
||||
accounts.value = acc.data.filter((row) => row.isActive);
|
||||
}
|
||||
@@ -205,12 +211,14 @@ const partyRules = [
|
||||
},
|
||||
];
|
||||
|
||||
const partyRemainingLabel = computed(() => {
|
||||
const kind = form.value.partyKind;
|
||||
const partyPosition = computed(() => {
|
||||
const partyKind = 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)}`;
|
||||
if (!partyKind || partyKind === 'other' || !id) return null;
|
||||
const key = `${partyKind}:${id}`;
|
||||
const receivable = Number(receivableBalances.value.find((item) => item.key === key)?.remaining || 0);
|
||||
const payable = Number(balances.value.find((item) => item.key === key)?.remaining || 0);
|
||||
return partyNetPosition(receivable, payable);
|
||||
});
|
||||
|
||||
async function voidDoc(id: string) {
|
||||
@@ -247,11 +255,11 @@ onMounted(() => {
|
||||
<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">
|
||||
<a-col :span="partyPosition ? 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 v-if="partyPosition" :span="8">
|
||||
<a-typography-text :type="partyPosition.type">{{ partyPosition.text }}</a-typography-text>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
|
||||
@@ -50,7 +50,7 @@ import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
|
||||
import TreasuryPartySelect from '@/components/treasury/TreasuryPartySelect.vue';
|
||||
import { useSettleDefault } from '@/components/treasury/useSettleDefault';
|
||||
import { toman } from './treasury-format';
|
||||
import { partyNetPosition, toman } from './treasury-format';
|
||||
import { parseMoneyNumber } from '@/lib/money';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
@@ -64,6 +64,7 @@ const isMobile = computed(() => !screens.value.md);
|
||||
const kind = 'income' as const;
|
||||
const rows = ref<FinancialDocumentDto[]>([]);
|
||||
const balances = ref<TreasuryPartyBalanceDto[]>([]);
|
||||
const payableBalances = ref<TreasuryPartyBalanceDto[]>([]);
|
||||
const categories = ref<FinancialCategoryDto[]>([]);
|
||||
const accounts = ref<FinancialAccountDto[]>([]);
|
||||
const dashboard = ref<TreasuryDashboardDto | null>(null);
|
||||
@@ -132,12 +133,14 @@ const partyRules = [
|
||||
},
|
||||
];
|
||||
|
||||
const partyRemainingLabel = computed(() => {
|
||||
const kind = form.value.partyKind;
|
||||
const partyPosition = computed(() => {
|
||||
const partyKind = 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)}`;
|
||||
if (!partyKind || partyKind === 'other' || !id) return null;
|
||||
const key = `${partyKind}:${id}`;
|
||||
const receivable = Number(balances.value.find((item) => item.key === key)?.remaining || 0);
|
||||
const payable = Number(payableBalances.value.find((item) => item.key === key)?.remaining || 0);
|
||||
return partyNetPosition(receivable, payable);
|
||||
});
|
||||
|
||||
const remainingAmount = computed(() => {
|
||||
@@ -168,7 +171,7 @@ function docLine(item: FinancialDocumentDto) {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const [docs, cats, acc, dash, bals] = await Promise.all([
|
||||
const [docs, cats, acc, dash, bals, payables] = await Promise.all([
|
||||
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
|
||||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
|
||||
@@ -187,9 +190,14 @@ async function load() {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
||||
throw e;
|
||||
}),
|
||||
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind: 'expense' } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
||||
throw e;
|
||||
}),
|
||||
]);
|
||||
rows.value = docs.data;
|
||||
balances.value = bals.data;
|
||||
payableBalances.value = payables.data;
|
||||
categories.value = cats.data.filter((row) => row.isActive);
|
||||
accounts.value = acc.data.filter((row) => row.isActive);
|
||||
if (dash) dashboard.value = dash.data;
|
||||
@@ -394,11 +402,11 @@ onMounted(() => {
|
||||
<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">
|
||||
<a-col :span="partyPosition ? 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 v-if="partyPosition" :span="8">
|
||||
<a-typography-text :type="partyPosition.type">{{ partyPosition.text }}</a-typography-text>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
@@ -641,11 +649,11 @@ onMounted(() => {
|
||||
<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">
|
||||
<a-col :span="partyPosition ? 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 v-if="partyPosition" :span="8">
|
||||
<a-typography-text :type="partyPosition.type">{{ partyPosition.text }}</a-typography-text>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
|
||||
@@ -3,3 +3,11 @@ export function toman(value: string | number | null | undefined) {
|
||||
if (!Number.isFinite(n)) return '۰';
|
||||
return `${new Intl.NumberFormat('fa-IR').format(Math.round(n))} تومان`;
|
||||
}
|
||||
|
||||
/** ماندهٔ دریافتنی منهای ماندهٔ پرداختنی: بد قرمز، بس سبز، صفر خاکستری. */
|
||||
export function partyNetPosition(receivable: number, payable: number) {
|
||||
const net = receivable - payable;
|
||||
if (net > 0) return { text: `بد ${toman(net)}`, type: 'danger' as const };
|
||||
if (net < 0) return { text: `بس ${toman(-net)}`, type: 'success' as const };
|
||||
return { text: `مانده ${toman(0)}`, type: 'secondary' as const };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user