feat(finance): show invoice balances in treasury and split payroll
Issued invoices feed debtors, creditors, and period profit without a second income document. Payroll is split into setup, staged payment, and a filtered report. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
import { LocalFileStorage } from '../storage/local-file.storage';
|
||||
import { StorageQuotaService } from '../billing/storage-quota.service';
|
||||
import type { Actor } from '../common/record-scope';
|
||||
import { loadInvoicePartyPositions, invoiceSalesInPeriod } from './invoice-ledger';
|
||||
import { TreasuryReceivablesService } from './treasury-receivables.service';
|
||||
|
||||
const INCOME_CATEGORIES = ['فروش خدمات', 'فروش کالا', 'سایر درآمدها'];
|
||||
@@ -753,7 +754,34 @@ export class TreasuryService {
|
||||
current.remaining = current.remaining.plus(rawRemaining);
|
||||
buckets.set(key, current);
|
||||
}
|
||||
|
||||
const invoiceParties = await loadInvoicePartyPositions(this.prisma, actor.organizationId);
|
||||
for (const party of invoiceParties) {
|
||||
const onDebtorSide = party.net.greaterThan(0);
|
||||
if (kind === 'income' && !onDebtorSide) continue;
|
||||
if (kind === 'expense' && onDebtorSide) continue;
|
||||
const remaining = party.net.abs();
|
||||
const amount = onDebtorSide ? party.billed : Prisma.Decimal.max(party.billed, remaining);
|
||||
const settled = Prisma.Decimal.max(amount.minus(remaining), 0);
|
||||
const current = buckets.get(party.key) ?? {
|
||||
key: party.key,
|
||||
partyKind: party.partyKind,
|
||||
partyName: party.partyName,
|
||||
amount: new Prisma.Decimal(0),
|
||||
settled: new Prisma.Decimal(0),
|
||||
remaining: new Prisma.Decimal(0),
|
||||
};
|
||||
current.amount = current.amount.plus(amount);
|
||||
current.settled = current.settled.plus(settled);
|
||||
current.remaining = current.remaining.plus(remaining);
|
||||
if (!current.partyName || current.partyName === 'بدون طرف حساب') {
|
||||
current.partyName = party.partyName;
|
||||
}
|
||||
buckets.set(party.key, current);
|
||||
}
|
||||
|
||||
return [...buckets.values()]
|
||||
.filter((row) => row.remaining.greaterThan(0))
|
||||
.sort((a, b) => b.remaining.comparedTo(a.remaining))
|
||||
.map((row) => ({
|
||||
key: row.key,
|
||||
@@ -1092,6 +1120,17 @@ export class TreasuryService {
|
||||
}
|
||||
const bookedIncome = dec(income._sum.amount);
|
||||
const bookedExpense = dec(expense._sum.amount);
|
||||
const invoiceSales = await invoiceSalesInPeriod(
|
||||
this.prisma,
|
||||
actor.organizationId,
|
||||
from,
|
||||
to,
|
||||
);
|
||||
const invoiceParties = await loadInvoicePartyPositions(this.prisma, actor.organizationId);
|
||||
for (const party of invoiceParties) {
|
||||
if (party.net.greaterThan(0)) receivables = receivables.plus(party.net);
|
||||
else payables = payables.plus(party.net.abs());
|
||||
}
|
||||
const totalBalance = accounts.reduce(
|
||||
(sum, account) => sum.plus(account.balance),
|
||||
new Prisma.Decimal(0),
|
||||
@@ -1101,7 +1140,7 @@ export class TreasuryService {
|
||||
actualReceipts: money(dec(receipts._sum.amount)),
|
||||
bookedExpense: money(bookedExpense),
|
||||
actualPayments: money(dec(payments._sum.amount)),
|
||||
operatingProfit: money(bookedIncome.minus(bookedExpense)),
|
||||
operatingProfit: money(bookedIncome.plus(invoiceSales.net).minus(bookedExpense)),
|
||||
receivables: money(receivables),
|
||||
payables: money(payables),
|
||||
totalBalance: money(totalBalance),
|
||||
|
||||
Reference in New Issue
Block a user