feat(treasury): list debtors and creditors on the finance report

Show open receivable and payable balances on separate report tabs, and include each list in an Excel export.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-04 14:16:38 +03:30
co-authored by Cursor
parent 08a278e1c8
commit 5b18749005
3 changed files with 148 additions and 3 deletions
+38 -1
View File
@@ -2,6 +2,7 @@ import { BadRequestException, Injectable, Logger, NotFoundException } from '@nes
import { Cron } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import ExcelJS from 'exceljs';
import type { TreasuryPartyBalanceDto } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import type { Actor } from '../common/record-scope';
@@ -314,7 +315,12 @@ export class TreasuryOpsService {
return { from: from.toISOString(), to: to.toISOString(), current, previous, ...breakdown };
}
async reportExcel(actor: Actor, from?: string, to?: string) {
async reportExcel(
actor: Actor,
from?: string,
to?: string,
parties?: { debtors: TreasuryPartyBalanceDto[]; creditors: TreasuryPartyBalanceDto[] },
) {
const data = await this.report(actor, from, to);
const book = new ExcelJS.Workbook();
const sheet = book.addWorksheet('گزارش مالی');
@@ -331,10 +337,41 @@ export class TreasuryOpsService {
sheet.addRow([]);
sheet.addRow(['دسته هزینه', 'مبلغ', 'سهم']);
data.current.expenseCategories.forEach((row) => sheet.addRow([row.name, row.amount, row.share]));
if (parties) {
this.addPartySheet(book, 'بدهکاران', parties.debtors, 'دریافت‌شده');
this.addPartySheet(book, 'بستانکاران', parties.creditors, 'پرداخت‌شده');
}
const buffer = await book.xlsx.writeBuffer();
return Buffer.from(buffer);
}
private addPartySheet(
book: ExcelJS.Workbook,
title: string,
rows: TreasuryPartyBalanceDto[],
settledLabel: string,
) {
const sheet = book.addWorksheet(title);
const kindLabel: Record<string, string> = { person: 'مشتری', company: 'شرکت', user: 'کاربر' };
sheet.addRow(['طرف حساب', 'نوع', 'مبلغ کل', settledLabel, 'مانده']);
let amount = new Prisma.Decimal(0);
let settled = new Prisma.Decimal(0);
let remaining = new Prisma.Decimal(0);
for (const row of rows) {
sheet.addRow([
row.partyName,
(row.partyKind && kindLabel[row.partyKind]) || 'ثبت قبلی',
row.amount,
row.settledAmount,
row.remaining,
]);
amount = amount.plus(row.amount);
settled = settled.plus(row.settledAmount);
remaining = remaining.plus(row.remaining);
}
sheet.addRow(['جمع', '', money(amount), money(settled), money(remaining)]);
}
private async period(organizationId: string, from: Date, to: Date) {
const docs = await this.prisma.financialDocument.findMany({
where: {
+5 -1
View File
@@ -269,7 +269,11 @@ export class TreasuryController {
@Query('from') from?: string,
@Query('to') to?: string,
) {
const buffer = await this.ops.reportExcel(user, from, to);
const [debtors, creditors] = await Promise.all([
this.treasury.partyBalances(user, 'income'),
this.treasury.partyBalances(user, 'expense'),
]);
const buffer = await this.ops.reportExcel(user, from, to, { debtors, creditors });
return new StreamableFile(buffer, {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
disposition: 'attachment; filename="treasury-report.xlsx"',