feat(tahesab): PDF invoice export for asnad cards

Add per-document PDF print from account details, plus org invoice settings for logo and texts separate from CRM invoices.
This commit is contained in:
2026-10-24 12:34:02 +03:30
parent 7d9fc8447f
commit f50246dc46
11 changed files with 871 additions and 11 deletions
@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "TahesabInvoiceSettings" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"logoDataUrl" TEXT,
"companyName" TEXT NOT NULL DEFAULT '',
"companySubtitle" TEXT NOT NULL DEFAULT '',
"address" TEXT NOT NULL DEFAULT '',
"phone" TEXT NOT NULL DEFAULT '',
"headerText" TEXT NOT NULL DEFAULT '',
"footerText" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TahesabInvoiceSettings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "TahesabInvoiceSettings_organizationId_key" ON "TahesabInvoiceSettings"("organizationId");
-- AddForeignKey
ALTER TABLE "TahesabInvoiceSettings" ADD CONSTRAINT "TahesabInvoiceSettings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+18
View File
@@ -197,6 +197,7 @@ model Organization {
accountingConnections AccountingConnection[]
accountingSyncJobs AccountingSyncJob[]
accountingAsnadLines AccountingAsnadLine[]
tahesabInvoiceSettings TahesabInvoiceSettings?
warehouses Warehouse[]
stockMovements StockMovement[]
partyOpenings PartyOpeningBalance[]
@@ -1910,6 +1911,23 @@ model AccountingSyncJob {
@@index([organizationId, createdAt])
}
/// تنظیمات ظاهر فاکتور PDF اسناد ته‌حساب (جدا از فاکتور CRM)
model TahesabInvoiceSettings {
id String @id @default(cuid())
organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// data URL تصویر لوگو (مثلاً data:image/png;base64,...)
logoDataUrl String? @db.Text
companyName String @default("")
companySubtitle String @default("")
address String @default("")
phone String @default("")
headerText String @default("")
footerText String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// انبار داخلی — وقتی ته‌حساب وصل نیست
model Warehouse {
id String @id @default(cuid())
+36
View File
@@ -449,6 +449,42 @@ export class SyncAsnadReportDto {
to!: string;
}
export class UpdateTahesabInvoiceSettingsDto {
/** null = حذف لوگو */
@IsOptional()
logoDataUrl?: string | null;
@IsOptional()
@IsString()
@MaxLength(200)
companyName?: string;
@IsOptional()
@IsString()
@MaxLength(200)
companySubtitle?: string;
@IsOptional()
@IsString()
@MaxLength(500)
address?: string;
@IsOptional()
@IsString()
@MaxLength(80)
phone?: string;
@IsOptional()
@IsString()
@MaxLength(1000)
headerText?: string;
@IsOptional()
@IsString()
@MaxLength(1000)
footerText?: string;
}
// silence unused import warnings for ValidateNested/Type if unused
void ValidateNested;
void Type;
@@ -0,0 +1,111 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import type {
TahesabInvoiceSettingsDto,
UpdateTahesabInvoiceSettingsRequest,
} from '@mento/shared';
import { PrismaService } from '../../prisma/prisma.service';
const MAX_LOGO_CHARS = 700_000; // ~500KB data URL
function toDto(row: {
id: string;
organizationId: string;
logoDataUrl: string | null;
companyName: string;
companySubtitle: string;
address: string;
phone: string;
headerText: string;
footerText: string;
updatedAt: Date;
}): TahesabInvoiceSettingsDto {
return {
id: row.id,
organizationId: row.organizationId,
logoDataUrl: row.logoDataUrl,
companyName: row.companyName,
companySubtitle: row.companySubtitle,
address: row.address,
phone: row.phone,
headerText: row.headerText,
footerText: row.footerText,
updatedAt: row.updatedAt.toISOString(),
};
}
@Injectable()
export class TahesabInvoiceSettingsService {
constructor(private readonly prisma: PrismaService) {}
async getOrCreate(organizationId: string): Promise<TahesabInvoiceSettingsDto> {
const existing = await this.prisma.tahesabInvoiceSettings.findUnique({
where: { organizationId },
});
if (existing) return toDto(existing);
const org = await this.prisma.organization.findUnique({
where: { id: organizationId },
select: { name: true },
});
const created = await this.prisma.tahesabInvoiceSettings.create({
data: {
organizationId,
companyName: org?.name ?? '',
},
});
return toDto(created);
}
async update(
organizationId: string,
dto: UpdateTahesabInvoiceSettingsRequest,
): Promise<TahesabInvoiceSettingsDto> {
if (dto.logoDataUrl != null && dto.logoDataUrl.length > MAX_LOGO_CHARS) {
throw new BadRequestException(
'حجم لوگو زیاد است؛ تصویر کوچک‌تری انتخاب کنید.',
);
}
if (
dto.logoDataUrl != null &&
dto.logoDataUrl !== '' &&
!/^data:image\/(png|jpe?g|webp|gif);base64,/i.test(dto.logoDataUrl)
) {
throw new BadRequestException('لوگو باید تصویر با فرمت data URL باشد.');
}
const data = {
...(dto.logoDataUrl !== undefined
? { logoDataUrl: dto.logoDataUrl?.trim() || null }
: {}),
...(dto.companyName !== undefined
? { companyName: dto.companyName.trim().slice(0, 200) }
: {}),
...(dto.companySubtitle !== undefined
? { companySubtitle: dto.companySubtitle.trim().slice(0, 200) }
: {}),
...(dto.address !== undefined
? { address: dto.address.trim().slice(0, 500) }
: {}),
...(dto.phone !== undefined
? { phone: dto.phone.trim().slice(0, 80) }
: {}),
...(dto.headerText !== undefined
? { headerText: dto.headerText.trim().slice(0, 1000) }
: {}),
...(dto.footerText !== undefined
? { footerText: dto.footerText.trim().slice(0, 1000) }
: {}),
};
const row = await this.prisma.tahesabInvoiceSettings.upsert({
where: { organizationId },
create: {
organizationId,
companyName: '',
...data,
},
update: data,
});
return toDto(row);
}
}
@@ -22,6 +22,7 @@ import { TahesabProductLinkService } from './product-link.service';
import { TahesabBalanceService } from './balance.service';
import { TahesabDocumentsService } from './documents.service';
import { TahesabAsnadReportService } from './asnad-report.service';
import { TahesabInvoiceSettingsService } from './invoice-settings.service';
import {
CreateInTahesabDto,
CreateTahesabConnectionDto,
@@ -38,6 +39,7 @@ import {
SyncAsnadReportDto,
SyncGroupsPageDto,
UpdateTahesabConnectionDto,
UpdateTahesabInvoiceSettingsDto,
} from './dto';
@Controller('integrations/tahesab')
@@ -51,6 +53,7 @@ export class TahesabController {
private readonly balance: TahesabBalanceService,
private readonly documents: TahesabDocumentsService,
private readonly asnadReport: TahesabAsnadReportService,
private readonly invoiceSettings: TahesabInvoiceSettingsService,
) {}
// —— Connections ——
@@ -279,6 +282,21 @@ export class TahesabController {
return this.products.importBulk(user.organizationId, dto);
}
// —— تنظیمات فاکتور PDF اسناد ته‌حساب ——
@Get('invoice-settings')
getInvoiceSettings(@CurrentUser() user: { organizationId: string }) {
return this.invoiceSettings.getOrCreate(user.organizationId);
}
@Patch('invoice-settings')
@Roles('owner', 'admin')
updateInvoiceSettings(
@CurrentUser() user: { organizationId: string },
@Body() dto: UpdateTahesabInvoiceSettingsDto,
) {
return this.invoiceSettings.update(user.organizationId, dto);
}
// —— گزارش فروش اسناد (اسنپ‌شات DoListAsnad) ——
@Post('reports/asnad/sync')
@Roles('owner', 'admin', 'sales')
@@ -11,6 +11,7 @@ import { TahesabProductLinkService } from './product-link.service';
import { TahesabBalanceService } from './balance.service';
import { TahesabDocumentsService } from './documents.service';
import { TahesabAsnadReportService } from './asnad-report.service';
import { TahesabInvoiceSettingsService } from './invoice-settings.service';
import { TahesabController } from './tahesab.controller';
import { CustomerGroupsModule } from '../../customer-groups/customer-groups.module';
@@ -27,6 +28,7 @@ import { CustomerGroupsModule } from '../../customer-groups/customer-groups.modu
TahesabBalanceService,
TahesabDocumentsService,
TahesabAsnadReportService,
TahesabInvoiceSettingsService,
],
exports: [
TahesabCryptoService,
@@ -36,6 +38,7 @@ import { CustomerGroupsModule } from '../../customer-groups/customer-groups.modu
TahesabBalanceService,
TahesabDocumentsService,
TahesabAsnadReportService,
TahesabInvoiceSettingsService,
],
})
export class TahesabModule {}