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:
@@ -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;
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user