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[]
|
accountingConnections AccountingConnection[]
|
||||||
accountingSyncJobs AccountingSyncJob[]
|
accountingSyncJobs AccountingSyncJob[]
|
||||||
accountingAsnadLines AccountingAsnadLine[]
|
accountingAsnadLines AccountingAsnadLine[]
|
||||||
|
tahesabInvoiceSettings TahesabInvoiceSettings?
|
||||||
warehouses Warehouse[]
|
warehouses Warehouse[]
|
||||||
stockMovements StockMovement[]
|
stockMovements StockMovement[]
|
||||||
partyOpenings PartyOpeningBalance[]
|
partyOpenings PartyOpeningBalance[]
|
||||||
@@ -1910,6 +1911,23 @@ model AccountingSyncJob {
|
|||||||
@@index([organizationId, createdAt])
|
@@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 {
|
model Warehouse {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|||||||
@@ -449,6 +449,42 @@ export class SyncAsnadReportDto {
|
|||||||
to!: string;
|
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
|
// silence unused import warnings for ValidateNested/Type if unused
|
||||||
void ValidateNested;
|
void ValidateNested;
|
||||||
void Type;
|
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 { TahesabBalanceService } from './balance.service';
|
||||||
import { TahesabDocumentsService } from './documents.service';
|
import { TahesabDocumentsService } from './documents.service';
|
||||||
import { TahesabAsnadReportService } from './asnad-report.service';
|
import { TahesabAsnadReportService } from './asnad-report.service';
|
||||||
|
import { TahesabInvoiceSettingsService } from './invoice-settings.service';
|
||||||
import {
|
import {
|
||||||
CreateInTahesabDto,
|
CreateInTahesabDto,
|
||||||
CreateTahesabConnectionDto,
|
CreateTahesabConnectionDto,
|
||||||
@@ -38,6 +39,7 @@ import {
|
|||||||
SyncAsnadReportDto,
|
SyncAsnadReportDto,
|
||||||
SyncGroupsPageDto,
|
SyncGroupsPageDto,
|
||||||
UpdateTahesabConnectionDto,
|
UpdateTahesabConnectionDto,
|
||||||
|
UpdateTahesabInvoiceSettingsDto,
|
||||||
} from './dto';
|
} from './dto';
|
||||||
|
|
||||||
@Controller('integrations/tahesab')
|
@Controller('integrations/tahesab')
|
||||||
@@ -51,6 +53,7 @@ export class TahesabController {
|
|||||||
private readonly balance: TahesabBalanceService,
|
private readonly balance: TahesabBalanceService,
|
||||||
private readonly documents: TahesabDocumentsService,
|
private readonly documents: TahesabDocumentsService,
|
||||||
private readonly asnadReport: TahesabAsnadReportService,
|
private readonly asnadReport: TahesabAsnadReportService,
|
||||||
|
private readonly invoiceSettings: TahesabInvoiceSettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// —— Connections ——
|
// —— Connections ——
|
||||||
@@ -279,6 +282,21 @@ export class TahesabController {
|
|||||||
return this.products.importBulk(user.organizationId, dto);
|
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) ——
|
// —— گزارش فروش اسناد (اسنپشات DoListAsnad) ——
|
||||||
@Post('reports/asnad/sync')
|
@Post('reports/asnad/sync')
|
||||||
@Roles('owner', 'admin', 'sales')
|
@Roles('owner', 'admin', 'sales')
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TahesabProductLinkService } from './product-link.service';
|
|||||||
import { TahesabBalanceService } from './balance.service';
|
import { TahesabBalanceService } from './balance.service';
|
||||||
import { TahesabDocumentsService } from './documents.service';
|
import { TahesabDocumentsService } from './documents.service';
|
||||||
import { TahesabAsnadReportService } from './asnad-report.service';
|
import { TahesabAsnadReportService } from './asnad-report.service';
|
||||||
|
import { TahesabInvoiceSettingsService } from './invoice-settings.service';
|
||||||
import { TahesabController } from './tahesab.controller';
|
import { TahesabController } from './tahesab.controller';
|
||||||
import { CustomerGroupsModule } from '../../customer-groups/customer-groups.module';
|
import { CustomerGroupsModule } from '../../customer-groups/customer-groups.module';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ import { CustomerGroupsModule } from '../../customer-groups/customer-groups.modu
|
|||||||
TahesabBalanceService,
|
TahesabBalanceService,
|
||||||
TahesabDocumentsService,
|
TahesabDocumentsService,
|
||||||
TahesabAsnadReportService,
|
TahesabAsnadReportService,
|
||||||
|
TahesabInvoiceSettingsService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
TahesabCryptoService,
|
TahesabCryptoService,
|
||||||
@@ -36,6 +38,7 @@ import { CustomerGroupsModule } from '../../customer-groups/customer-groups.modu
|
|||||||
TahesabBalanceService,
|
TahesabBalanceService,
|
||||||
TahesabDocumentsService,
|
TahesabDocumentsService,
|
||||||
TahesabAsnadReportService,
|
TahesabAsnadReportService,
|
||||||
|
TahesabInvoiceSettingsService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TahesabModule {}
|
export class TahesabModule {}
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ import type {
|
|||||||
PersonAccountingLinkDto,
|
PersonAccountingLinkDto,
|
||||||
PersonAssetsDto,
|
PersonAssetsDto,
|
||||||
TahesabConnectionDto,
|
TahesabConnectionDto,
|
||||||
|
TahesabInvoiceSettingsDto,
|
||||||
TahesabSanadType,
|
TahesabSanadType,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
import { TAHSAB_SANAD_LABELS } from '@/lib/tahesab';
|
import { TAHSAB_SANAD_LABELS } from '@/lib/tahesab';
|
||||||
|
import { printTahesabInvoicePdf } from '@/lib/tahesab-invoice-pdf';
|
||||||
|
import TahesabInvoiceSettingsModal from '@/components/person-assets/TahesabInvoiceSettingsModal.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
personId: string;
|
personId: string;
|
||||||
|
/** نام نمایشی مشتری برای فاکتور PDF */
|
||||||
|
personName?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -37,6 +42,8 @@ const assets = ref<PersonAssetsDto | null>(null);
|
|||||||
const activeTab = ref<'مانده حساب' | 'جزئیات حساب' | 'ثبت سند'>('مانده حساب');
|
const activeTab = ref<'مانده حساب' | 'جزئیات حساب' | 'ثبت سند'>('مانده حساب');
|
||||||
const asnadLoading = ref(false);
|
const asnadLoading = ref(false);
|
||||||
const asnadItems = ref<Record<string, unknown>[]>([]);
|
const asnadItems = ref<Record<string, unknown>[]>([]);
|
||||||
|
const invoiceSettingsOpen = ref(false);
|
||||||
|
const invoiceSettings = ref<TahesabInvoiceSettingsDto | null>(null);
|
||||||
/** مدل JalaliDateInput = تاریخ محلی میلادی YYYY-MM-DD */
|
/** مدل JalaliDateInput = تاریخ محلی میلادی YYYY-MM-DD */
|
||||||
const fromDate = ref('');
|
const fromDate = ref('');
|
||||||
const toDate = ref('');
|
const toDate = ref('');
|
||||||
@@ -383,6 +390,38 @@ async function refreshAssets() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadInvoiceSettings() {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<TahesabInvoiceSettingsDto>(
|
||||||
|
'/integrations/tahesab/invoice-settings',
|
||||||
|
);
|
||||||
|
invoiceSettings.value = data;
|
||||||
|
} catch {
|
||||||
|
invoiceSettings.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInvoiceSettingsSaved(dto: TahesabInvoiceSettingsDto) {
|
||||||
|
invoiceSettings.value = dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAsnadPdf(g: {
|
||||||
|
no: string;
|
||||||
|
date: string;
|
||||||
|
rows: Record<string, unknown>[];
|
||||||
|
}) {
|
||||||
|
error.value = '';
|
||||||
|
if (!invoiceSettings.value) {
|
||||||
|
await loadInvoiceSettings();
|
||||||
|
}
|
||||||
|
printTahesabInvoicePdf({
|
||||||
|
group: g,
|
||||||
|
settings: invoiceSettings.value,
|
||||||
|
customerName: props.personName?.trim() || 'مشتری',
|
||||||
|
moshtariCode: currentLink.value?.moshtariCode ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAsnad() {
|
async function loadAsnad() {
|
||||||
if (!connectionId.value || !currentLink.value) return;
|
if (!connectionId.value || !currentLink.value) return;
|
||||||
const from = toShamsiYmd(fromDate.value);
|
const from = toShamsiYmd(fromDate.value);
|
||||||
@@ -394,22 +433,26 @@ async function loadAsnad() {
|
|||||||
asnadLoading.value = true;
|
asnadLoading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<{ items: Record<string, unknown>[] }>(
|
const [{ data }] = await Promise.all([
|
||||||
`/integrations/tahesab/people/${props.personId}/asnad`,
|
api.get<{ items: Record<string, unknown>[] }>(
|
||||||
{
|
`/integrations/tahesab/people/${props.personId}/asnad`,
|
||||||
params: {
|
{
|
||||||
connectionId: connectionId.value,
|
params: {
|
||||||
from,
|
connectionId: connectionId.value,
|
||||||
to,
|
from,
|
||||||
|
to,
|
||||||
|
},
|
||||||
|
timeout: 60_000,
|
||||||
},
|
},
|
||||||
timeout: 60_000,
|
),
|
||||||
},
|
loadInvoiceSettings(),
|
||||||
);
|
]);
|
||||||
asnadItems.value = data.items ?? [];
|
asnadItems.value = data.items ?? [];
|
||||||
ok.value = asnadItems.value.length
|
ok.value = asnadItems.value.length
|
||||||
? `${asnadItems.value.length} سند نمایش داده شد.`
|
? `${asnadItems.value.length} سند نمایش داده شد.`
|
||||||
: 'در این بازه سندی یافت نشد.';
|
: 'در این بازه سندی یافت نشد.';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
asnadItems.value = [];
|
||||||
error.value = axios.isAxiosError(e)
|
error.value = axios.isAxiosError(e)
|
||||||
? String(
|
? String(
|
||||||
(e.response?.data as { message?: string })?.message ??
|
(e.response?.data as { message?: string })?.message ??
|
||||||
@@ -715,6 +758,9 @@ onMounted(async () => {
|
|||||||
<button type="button" class="btn" :disabled="asnadLoading" @click="loadAsnad">
|
<button type="button" class="btn" :disabled="asnadLoading" @click="loadAsnad">
|
||||||
{{ asnadLoading ? '…' : 'نمایش اسناد' }}
|
{{ asnadLoading ? '…' : 'نمایش اسناد' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn ghost" @click="invoiceSettingsOpen = true">
|
||||||
|
تنظیمات فاکتور
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="asnadItems.length" class="view-bar">
|
<div v-if="asnadItems.length" class="view-bar">
|
||||||
<span class="muted">
|
<span class="muted">
|
||||||
@@ -748,6 +794,14 @@ onMounted(async () => {
|
|||||||
<strong>سند شماره {{ showRaw(g.no) }}</strong>
|
<strong>سند شماره {{ showRaw(g.no) }}</strong>
|
||||||
<span v-if="g.rows[0]?.isSanadMovaghat === true" class="inv-badge">موقت</span>
|
<span v-if="g.rows[0]?.isSanadMovaghat === true" class="inv-badge">موقت</span>
|
||||||
<span class="sg-count">{{ g.rows.length.toLocaleString('fa-IR') }} ردیف</span>
|
<span class="sg-count">{{ g.rows.length.toLocaleString('fa-IR') }} ردیف</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn inv-pdf-btn"
|
||||||
|
title="خروجی فاکتور PDF"
|
||||||
|
@click="exportAsnadPdf(g)"
|
||||||
|
>
|
||||||
|
خروجی فاکتور PDF
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<dl class="inv-meta">
|
<dl class="inv-meta">
|
||||||
<div>
|
<div>
|
||||||
@@ -954,6 +1008,11 @@ onMounted(async () => {
|
|||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<TahesabInvoiceSettingsModal
|
||||||
|
v-model:open="invoiceSettingsOpen"
|
||||||
|
@saved="onInvoiceSettingsSaved"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1195,9 +1254,15 @@ onMounted(async () => {
|
|||||||
.inv-title {
|
.inv-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
.inv-pdf-btn {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
}
|
||||||
.inv-badge {
|
.inv-badge {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
padding: 0.1rem 0.45rem;
|
padding: 0.1rem 0.45rem;
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import type { TahesabInvoiceSettingsDto } from '@mento/shared';
|
||||||
|
import { useEscClose } from '@/lib/modal-stack';
|
||||||
|
|
||||||
|
const props = defineProps<{ open: boolean }>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:open': [value: boolean];
|
||||||
|
saved: [value: TahesabInvoiceSettingsDto];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
const ok = ref('');
|
||||||
|
|
||||||
|
const logoDataUrl = ref<string | null>(null);
|
||||||
|
const companyName = ref('');
|
||||||
|
const companySubtitle = ref('');
|
||||||
|
const address = ref('');
|
||||||
|
const phone = ref('');
|
||||||
|
const headerText = ref('');
|
||||||
|
const footerText = ref('');
|
||||||
|
|
||||||
|
useEscClose(
|
||||||
|
() => props.open,
|
||||||
|
() => close(),
|
||||||
|
);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:open', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fill(dto: TahesabInvoiceSettingsDto) {
|
||||||
|
logoDataUrl.value = dto.logoDataUrl;
|
||||||
|
companyName.value = dto.companyName ?? '';
|
||||||
|
companySubtitle.value = dto.companySubtitle ?? '';
|
||||||
|
address.value = dto.address ?? '';
|
||||||
|
phone.value = dto.phone ?? '';
|
||||||
|
headerText.value = dto.headerText ?? '';
|
||||||
|
footerText.value = dto.footerText ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<TahesabInvoiceSettingsDto>(
|
||||||
|
'/integrations/tahesab/invoice-settings',
|
||||||
|
);
|
||||||
|
fill(data);
|
||||||
|
} catch (e) {
|
||||||
|
error.value = axios.isAxiosError(e)
|
||||||
|
? String(
|
||||||
|
(e.response?.data as { message?: string })?.message ??
|
||||||
|
'بارگذاری تنظیمات ناموفق بود.',
|
||||||
|
)
|
||||||
|
: 'بارگذاری تنظیمات ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogoFile(ev: Event) {
|
||||||
|
const input = ev.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
input.value = '';
|
||||||
|
if (!file) return;
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
error.value = 'فقط فایل تصویر مجاز است.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > 400_000) {
|
||||||
|
error.value = 'حجم تصویر حداکثر حدود ۴۰۰ کیلوبایت باشد.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const result = typeof reader.result === 'string' ? reader.result : '';
|
||||||
|
if (!result.startsWith('data:image/')) {
|
||||||
|
error.value = 'خواندن تصویر ناموفق بود.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logoDataUrl.value = result;
|
||||||
|
error.value = '';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogo() {
|
||||||
|
logoDataUrl.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
saving.value = true;
|
||||||
|
error.value = '';
|
||||||
|
ok.value = '';
|
||||||
|
try {
|
||||||
|
const { data } = await api.patch<TahesabInvoiceSettingsDto>(
|
||||||
|
'/integrations/tahesab/invoice-settings',
|
||||||
|
{
|
||||||
|
logoDataUrl: logoDataUrl.value,
|
||||||
|
companyName: companyName.value,
|
||||||
|
companySubtitle: companySubtitle.value,
|
||||||
|
address: address.value,
|
||||||
|
phone: phone.value,
|
||||||
|
headerText: headerText.value,
|
||||||
|
footerText: footerText.value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
fill(data);
|
||||||
|
ok.value = 'تنظیمات فاکتور ذخیره شد.';
|
||||||
|
emit('saved', data);
|
||||||
|
} catch (e) {
|
||||||
|
error.value = axios.isAxiosError(e)
|
||||||
|
? String(
|
||||||
|
(e.response?.data as { message?: string })?.message ??
|
||||||
|
'ذخیره تنظیمات ناموفق بود.',
|
||||||
|
)
|
||||||
|
: 'ذخیره تنظیمات ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
ok.value = '';
|
||||||
|
error.value = '';
|
||||||
|
void load();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.open) void load();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="open" class="backdrop" @click.self="close">
|
||||||
|
<div class="sheet" role="dialog" aria-modal="true" aria-label="تنظیمات فاکتور تهحساب">
|
||||||
|
<header class="sheet-head">
|
||||||
|
<div>
|
||||||
|
<h3>تنظیمات فاکتور تهحساب</h3>
|
||||||
|
<p class="muted">لوگو و متنهای ثابت خروجی PDF اسناد — جدا از فاکتور CRM</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn ghost" @click="close">بستن</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p v-if="error" class="err">{{ error }}</p>
|
||||||
|
<p v-if="ok" class="ok">{{ ok }}</p>
|
||||||
|
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
||||||
|
|
||||||
|
<div v-else class="grid">
|
||||||
|
<label class="field logo-field">
|
||||||
|
<span>لوگو</span>
|
||||||
|
<div class="logo-row">
|
||||||
|
<img v-if="logoDataUrl" :src="logoDataUrl" alt="لوگو" class="logo-preview" />
|
||||||
|
<div class="logo-actions">
|
||||||
|
<input type="file" accept="image/*" @change="onLogoFile" />
|
||||||
|
<button v-if="logoDataUrl" type="button" class="btn ghost" @click="clearLogo">
|
||||||
|
حذف لوگو
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field">
|
||||||
|
<span>نام شرکت / فروشگاه</span>
|
||||||
|
<input v-model="companyName" type="text" maxlength="200" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>زیرعنوان</span>
|
||||||
|
<input v-model="companySubtitle" type="text" maxlength="200" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>آدرس</span>
|
||||||
|
<input v-model="address" type="text" maxlength="500" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>تلفن</span>
|
||||||
|
<input v-model="phone" type="text" maxlength="80" dir="ltr" />
|
||||||
|
</label>
|
||||||
|
<label class="field full">
|
||||||
|
<span>متن بالای فاکتور</span>
|
||||||
|
<textarea v-model="headerText" rows="2" maxlength="1000" />
|
||||||
|
</label>
|
||||||
|
<label class="field full">
|
||||||
|
<span>متن پایین فاکتور</span>
|
||||||
|
<textarea v-model="footerText" rows="2" maxlength="1000" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="sheet-foot">
|
||||||
|
<button type="button" class="btn" :disabled="saving || loading" @click="save">
|
||||||
|
{{ saving ? '…' : 'ذخیره تنظیمات' }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 80;
|
||||||
|
background: rgba(15, 23, 42, 0.45);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.sheet {
|
||||||
|
width: min(640px, 100%);
|
||||||
|
max-height: min(90vh, 820px);
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--surface, #fff);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--line, #e5e7eb);
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.sheet-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.sheet-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--ink-secondary, #6b7280);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.err {
|
||||||
|
color: #b91c1c;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.ok {
|
||||||
|
color: #047857;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.field.full,
|
||||||
|
.logo-field {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.field input,
|
||||||
|
.field textarea {
|
||||||
|
border: 1px solid var(--line, #e5e7eb);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font: inherit;
|
||||||
|
background: var(--surface, #fff);
|
||||||
|
}
|
||||||
|
.logo-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.logo-preview {
|
||||||
|
max-height: 64px;
|
||||||
|
max-width: 140px;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 1px solid var(--line, #e5e7eb);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.logo-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.sheet-foot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import type { TahesabInvoiceSettingsDto } from '@mento/shared';
|
||||||
|
import { toFaDigits } from '@/lib/jalali';
|
||||||
|
|
||||||
|
export type TahesabInvoiceGroup = {
|
||||||
|
no: string;
|
||||||
|
date: string;
|
||||||
|
rows: Record<string, unknown>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dash(v: unknown, fallback = '—'): string {
|
||||||
|
if (v == null) return fallback;
|
||||||
|
const s = String(v).trim();
|
||||||
|
if (!s || s === 'null' || s === 'undefined' || s === 'NaN') return fallback;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtNum(v: unknown): string {
|
||||||
|
const s = dash(v, '');
|
||||||
|
if (!s) return '—';
|
||||||
|
const n = Number(String(s).replace(/,/g, ''));
|
||||||
|
if (!Number.isFinite(n)) return toFaDigits(s);
|
||||||
|
return toFaDigits(
|
||||||
|
new Intl.NumberFormat('fa-IR', { maximumFractionDigits: 3 }).format(n),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function metalLabel(v: unknown): string {
|
||||||
|
const s = dash(v, '');
|
||||||
|
if (s === '0') return 'طلا';
|
||||||
|
if (s === '1') return 'پلاتین';
|
||||||
|
if (s === '2') return 'نقره';
|
||||||
|
return s || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowDesc(r: Record<string, unknown>): string {
|
||||||
|
const parts = [dash(r.NO, ''), dash(r.No_Kar, ''), dash(r.Sharh1, '')].filter(
|
||||||
|
(p) => p && p !== '—',
|
||||||
|
);
|
||||||
|
return parts.join(' · ') || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumMali(rows: Record<string, unknown>[]): number {
|
||||||
|
let t = 0;
|
||||||
|
for (const r of rows) {
|
||||||
|
const n = Number(String(r.Mali ?? '').replace(/,/g, ''));
|
||||||
|
if (Number.isFinite(n)) t += n;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** خروجی فاکتور سند تهحساب — چاپ / ذخیره PDF از دیالوگ مرورگر */
|
||||||
|
export function printTahesabInvoicePdf(opts: {
|
||||||
|
group: TahesabInvoiceGroup;
|
||||||
|
settings: TahesabInvoiceSettingsDto | null;
|
||||||
|
customerName: string;
|
||||||
|
moshtariCode?: number | null;
|
||||||
|
}) {
|
||||||
|
const { group, settings, customerName, moshtariCode } = opts;
|
||||||
|
const first = group.rows[0] ?? {};
|
||||||
|
const time = dash(first.Tarikh_Time ?? first.ZamanSabt, '');
|
||||||
|
const user = dash(first.User, '');
|
||||||
|
const temporary = first.isSanadMovaghat === true;
|
||||||
|
const total = sumMali(group.rows);
|
||||||
|
|
||||||
|
const rowsHtml = group.rows
|
||||||
|
.map((r, i) => {
|
||||||
|
const idx = dash(r.Radif, String(i + 1));
|
||||||
|
return `<tr>
|
||||||
|
<td>${toFaDigits(idx)}</td>
|
||||||
|
<td>${escapeHtml(rowDesc(r))}</td>
|
||||||
|
<td>${escapeHtml(metalLabel(r.Jens_Felez))}</td>
|
||||||
|
<td class="ltr">${fmtNum(r.Count)}</td>
|
||||||
|
<td class="ltr">${fmtNum(r.Vazn)}</td>
|
||||||
|
<td class="ltr">${fmtNum(r.Mazaneh ?? r.Fi)}</td>
|
||||||
|
<td class="ltr">${fmtNum(r.Mali)}</td>
|
||||||
|
<td>${escapeHtml(dash(r.Arz, 'IRR'))}</td>
|
||||||
|
</tr>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const logo = settings?.logoDataUrl
|
||||||
|
? `<img class="logo" src="${escapeHtml(settings.logoDataUrl)}" alt="" />`
|
||||||
|
: '';
|
||||||
|
const company = escapeHtml(settings?.companyName?.trim() || 'فاکتور تهحساب');
|
||||||
|
const subtitle = settings?.companySubtitle?.trim()
|
||||||
|
? `<div class="sub">${escapeHtml(settings.companySubtitle.trim())}</div>`
|
||||||
|
: '';
|
||||||
|
const address = settings?.address?.trim()
|
||||||
|
? `<div class="muted">${escapeHtml(settings.address.trim())}</div>`
|
||||||
|
: '';
|
||||||
|
const phone = settings?.phone?.trim()
|
||||||
|
? `<div class="muted">${escapeHtml(settings.phone.trim())}</div>`
|
||||||
|
: '';
|
||||||
|
const headerText = settings?.headerText?.trim()
|
||||||
|
? `<p class="note">${escapeHtml(settings.headerText.trim())}</p>`
|
||||||
|
: '';
|
||||||
|
const footerText = settings?.footerText?.trim()
|
||||||
|
? `<p class="note foot-note">${escapeHtml(settings.footerText.trim())}</p>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const win = window.open('', '_blank', 'noopener,noreferrer,width=900,height=1200');
|
||||||
|
if (!win) return;
|
||||||
|
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>فاکتور سند ${escapeHtml(group.no)}</title>
|
||||||
|
<style>
|
||||||
|
@page { size: A4; margin: 12mm; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Tahoma, "Segoe UI", sans-serif;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 12px;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 2px solid #111827;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.brand { display: flex; gap: 12px; align-items: center; }
|
||||||
|
.logo { max-height: 64px; max-width: 140px; object-fit: contain; }
|
||||||
|
.org { font-size: 18px; font-weight: 800; margin: 0; }
|
||||||
|
.sub { color: #374151; margin-top: 2px; }
|
||||||
|
.muted { color: #6b7280; margin-top: 2px; }
|
||||||
|
.doc { text-align: left; }
|
||||||
|
.doc-type { font-weight: 800; font-size: 15px; }
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid #d97706;
|
||||||
|
color: #b45309;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 8px 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.lbl { color: #6b7280; font-size: 11px; display: block; margin-bottom: 2px; }
|
||||||
|
.val { font-weight: 600; }
|
||||||
|
.note {
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td {
|
||||||
|
padding: 7px 8px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
th { background: #f3f4f6; font-size: 11px; }
|
||||||
|
.ltr { direction: ltr; unicode-bidi: isolate; text-align: left; }
|
||||||
|
.totals {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.totals-box {
|
||||||
|
min-width: 220px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.t-row { display: flex; justify-content: space-between; gap: 24px; font-weight: 800; }
|
||||||
|
.foot-note { margin-top: 16px; }
|
||||||
|
@media print {
|
||||||
|
body { font-size: 11px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="top">
|
||||||
|
<div class="brand">
|
||||||
|
${logo}
|
||||||
|
<div>
|
||||||
|
<p class="org">${company}</p>
|
||||||
|
${subtitle}${address}${phone}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="doc">
|
||||||
|
<div class="doc-type">فاکتور سند تهحساب</div>
|
||||||
|
<div>شماره: <strong>${toFaDigits(escapeHtml(group.no))}</strong></div>
|
||||||
|
${temporary ? '<span class="badge">سند موقت</span>' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${headerText}
|
||||||
|
<div class="meta">
|
||||||
|
<div><span class="lbl">مشتری</span><div class="val">${escapeHtml(customerName || '—')}</div></div>
|
||||||
|
<div><span class="lbl">کد مشتری</span><div class="val ltr">${
|
||||||
|
moshtariCode != null ? toFaDigits(String(moshtariCode)) : '—'
|
||||||
|
}</div></div>
|
||||||
|
<div><span class="lbl">تاریخ</span><div class="val">${toFaDigits(escapeHtml(group.date))}${
|
||||||
|
time ? ` · ${toFaDigits(escapeHtml(time))}` : ''
|
||||||
|
}</div></div>
|
||||||
|
<div><span class="lbl">کاربر ثبت</span><div class="val">${escapeHtml(user)}</div></div>
|
||||||
|
<div><span class="lbl">تعداد ردیف</span><div class="val">${toFaDigits(String(group.rows.length))}</div></div>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>شرح</th>
|
||||||
|
<th>جنس فلز</th>
|
||||||
|
<th>تعداد</th>
|
||||||
|
<th>وزن</th>
|
||||||
|
<th>مظنه/فی</th>
|
||||||
|
<th>مالی</th>
|
||||||
|
<th>ارز</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rowsHtml}</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="totals">
|
||||||
|
<div class="totals-box">
|
||||||
|
<div class="t-row"><span>جمع مالی</span><span class="ltr">${fmtNum(total)}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${footerText}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
win.document.open();
|
||||||
|
win.document.write(html);
|
||||||
|
win.document.close();
|
||||||
|
win.focus();
|
||||||
|
setTimeout(() => {
|
||||||
|
win.print();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
@@ -1043,7 +1043,11 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
<div v-show="tab === 'assets'" class="panel assets-panel">
|
<div v-show="tab === 'assets'" class="panel assets-panel">
|
||||||
<InternalPartyBalance :person-id="id" />
|
<InternalPartyBalance :person-id="id" />
|
||||||
<MontifyStoresCard :person-id="id" />
|
<MontifyStoresCard :person-id="id" />
|
||||||
<PersonAssetsPanel :person-id="id" @link-changed="onTahesabLinkChanged" />
|
<PersonAssetsPanel
|
||||||
|
:person-id="id"
|
||||||
|
:person-name="fullName"
|
||||||
|
@link-changed="onTahesabLinkChanged"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="canBehavior && tab === 'disc'" class="panel">
|
<div v-if="canBehavior && tab === 'disc'" class="panel">
|
||||||
|
|||||||
@@ -4165,6 +4165,30 @@ export interface TahesabAsnadSummaryDto {
|
|||||||
lastSyncedAt: string | null;
|
lastSyncedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** تنظیمات ظاهر فاکتور PDF اسناد تهحساب (جدا از فاکتور CRM) */
|
||||||
|
export interface TahesabInvoiceSettingsDto {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
logoDataUrl: string | null;
|
||||||
|
companyName: string;
|
||||||
|
companySubtitle: string;
|
||||||
|
address: string;
|
||||||
|
phone: string;
|
||||||
|
headerText: string;
|
||||||
|
footerText: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTahesabInvoiceSettingsRequest {
|
||||||
|
logoDataUrl?: string | null;
|
||||||
|
companyName?: string;
|
||||||
|
companySubtitle?: string;
|
||||||
|
address?: string;
|
||||||
|
phone?: string;
|
||||||
|
headerText?: string;
|
||||||
|
footerText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MandehValueDto {
|
export interface MandehValueDto {
|
||||||
amount: string | number;
|
amount: string | number;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user