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:
2026-11-10 10:45:59 +03:30
co-authored by Cursor
parent 96105468d4
commit ba7f942059
5 changed files with 318 additions and 27 deletions
@@ -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"