task-h.5.3
This commit is contained in:
@@ -384,72 +384,79 @@ const DOCUMENT_TEMPLATE_MAPPINGS: TemplateMappingSeed[] = [
|
||||
dataType: 'scalar',
|
||||
sortOrder: 9
|
||||
},
|
||||
{
|
||||
placeholderKey: 'quotation_price_data',
|
||||
sourcePath: 'pdfme.quotation_price_data',
|
||||
dataType: 'table',
|
||||
defaultValue: JSON.stringify([['Price (Exclude VAT)', '-', ' ', 'THB']]),
|
||||
sortOrder: 10
|
||||
},
|
||||
{
|
||||
placeholderKey: 'quotation_price',
|
||||
sourcePath: 'pdfme.quotation_price',
|
||||
dataType: 'scalar',
|
||||
formatMask: null,
|
||||
sortOrder: 10
|
||||
sortOrder: 11
|
||||
},
|
||||
{
|
||||
placeholderKey: 'currency',
|
||||
sourcePath: 'quotation.currency',
|
||||
dataType: 'scalar',
|
||||
sortOrder: 11
|
||||
sortOrder: 12
|
||||
},
|
||||
{
|
||||
placeholderKey: 'exclusion_data',
|
||||
sourcePath: 'pdfme.exclusion_data',
|
||||
dataType: 'table',
|
||||
sortOrder: 12
|
||||
sortOrder: 13
|
||||
},
|
||||
{
|
||||
placeholderKey: 'app1',
|
||||
sourcePath: 'signatures.preparedBy.name',
|
||||
dataType: 'scalar',
|
||||
defaultValue: '-',
|
||||
sortOrder: 13
|
||||
sortOrder: 14
|
||||
},
|
||||
{
|
||||
placeholderKey: 'app1_position',
|
||||
sourcePath: 'signatures.preparedBy.position',
|
||||
dataType: 'scalar',
|
||||
defaultValue: '-',
|
||||
sortOrder: 14
|
||||
sortOrder: 15
|
||||
},
|
||||
{
|
||||
placeholderKey: 'app2',
|
||||
sourcePath: 'signatures.approvedBy.name',
|
||||
dataType: 'scalar',
|
||||
defaultValue: '-',
|
||||
sortOrder: 15
|
||||
sortOrder: 16
|
||||
},
|
||||
{
|
||||
placeholderKey: 'app2_position',
|
||||
sourcePath: 'signatures.approvedBy.position',
|
||||
dataType: 'scalar',
|
||||
defaultValue: '-',
|
||||
sortOrder: 16
|
||||
sortOrder: 17
|
||||
},
|
||||
{
|
||||
placeholderKey: 'app3',
|
||||
sourcePath: 'signatures.authorizedBy.name',
|
||||
dataType: 'scalar',
|
||||
defaultValue: '-',
|
||||
sortOrder: 17
|
||||
sortOrder: 18
|
||||
},
|
||||
{
|
||||
placeholderKey: 'app3_position',
|
||||
sourcePath: 'signatures.authorizedBy.position',
|
||||
dataType: 'scalar',
|
||||
defaultValue: '-',
|
||||
sortOrder: 18
|
||||
sortOrder: 19
|
||||
},
|
||||
{
|
||||
placeholderKey: 'items_table',
|
||||
sourcePath: 'items',
|
||||
dataType: 'table',
|
||||
sortOrder: 19,
|
||||
sortOrder: 20,
|
||||
columns: [
|
||||
{ columnName: 'Item', sourceField: 'itemNumber', columnLetter: 'A', sortOrder: 1 },
|
||||
{ columnName: 'Description', sourceField: 'description', columnLetter: 'B', sortOrder: 2 },
|
||||
@@ -922,6 +929,17 @@ async function upsertDocumentTemplatesForOrganization(
|
||||
)
|
||||
and deleted_at is null
|
||||
`;
|
||||
|
||||
await sql`
|
||||
update crm_document_template_mappings
|
||||
set
|
||||
deleted_at = now(),
|
||||
updated_at = now()
|
||||
where organization_id = ${organization.id}
|
||||
and template_version_id = ${resolvedVersionId}
|
||||
and placeholder_key in (${'exclusion_data_'})
|
||||
and deleted_at is null
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const EMPTY_TABLE_FALLBACK = [['-']];
|
||||
const PRICE_TABLE_LABEL = 'Price (Exclude VAT)';
|
||||
|
||||
function normalizeNumber(
|
||||
amount: number | string | null | undefined
|
||||
@@ -70,6 +71,45 @@ export function formatPdfCurrency(
|
||||
return `${symbol}${formatted}`.trim();
|
||||
}
|
||||
|
||||
export function formatPdfCurrencyAmount(
|
||||
amount: number | string | null | undefined,
|
||||
currencyCode = 'THB'
|
||||
): string {
|
||||
const numericAmount = normalizeNumber(amount);
|
||||
|
||||
if (numericAmount === null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const normalizedCode = currencyCode.toUpperCase();
|
||||
const symbols: Record<string, string> = {
|
||||
THB: '฿',
|
||||
USD: '$',
|
||||
EUR: 'EUR '
|
||||
};
|
||||
const symbol = symbols[normalizedCode] ?? `${normalizedCode} `;
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(numericAmount);
|
||||
|
||||
return `${symbol}${formatted}`.trim();
|
||||
}
|
||||
|
||||
export function buildPdfPriceTableData(
|
||||
amount: number | string | null | undefined,
|
||||
currencyCode?: string | null
|
||||
): string[][] {
|
||||
const normalizedCurrency = currencyCode?.trim() || 'THB';
|
||||
|
||||
return [[
|
||||
PRICE_TABLE_LABEL,
|
||||
formatPdfCurrencyAmount(amount, normalizedCurrency),
|
||||
' ',
|
||||
normalizedCurrency
|
||||
]];
|
||||
}
|
||||
|
||||
export function formatTopicItems(topic?: {
|
||||
items?: Array<{ content?: string | null; sortOrder?: number | null }> | string[];
|
||||
}): string[][] {
|
||||
@@ -109,6 +149,16 @@ export function normalizePdfmeTable(
|
||||
if (value.every((row) => Array.isArray(row))) {
|
||||
const normalizedRows = value.map((row) =>
|
||||
(row as unknown[]).map((cell) => {
|
||||
if (typeof cell === 'string') {
|
||||
const trimmedValue = cell.trim();
|
||||
|
||||
if (trimmedValue) {
|
||||
return trimmedValue;
|
||||
}
|
||||
|
||||
return cell.length > 0 ? cell : '-';
|
||||
}
|
||||
|
||||
const stringValue = String(cell ?? '').trim();
|
||||
return stringValue || '-';
|
||||
})
|
||||
|
||||
@@ -1,52 +1,73 @@
|
||||
import { and, eq, inArray, isNull } from 'drizzle-orm';
|
||||
import { crmCustomerContacts, crmCustomers, crmQuotations, organizations } from '@/db/schema';
|
||||
import { db } from '@/lib/db';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { and, eq, inArray, isNull } from "drizzle-orm";
|
||||
import {
|
||||
crmCustomerContacts,
|
||||
crmCustomers,
|
||||
crmQuotations,
|
||||
organizations,
|
||||
} from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
import { AuthError } from "@/lib/auth/session";
|
||||
import {
|
||||
listApprovalRequests,
|
||||
getApprovalRequest
|
||||
} from '@/features/foundation/approval/server/service';
|
||||
getApprovalRequest,
|
||||
} from "@/features/foundation/approval/server/service";
|
||||
import {
|
||||
mapDocumentDataToTemplateInput,
|
||||
resolveTemplateForDocument,
|
||||
resolveTemplateMappings
|
||||
} from '@/features/foundation/document-template/server/service';
|
||||
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
|
||||
resolveTemplateMappings,
|
||||
} from "@/features/foundation/document-template/server/service";
|
||||
import { getActiveOptionsByCategory } from "@/features/foundation/master-options/service";
|
||||
import {
|
||||
getQuotationDetail,
|
||||
listQuotationCustomers,
|
||||
listQuotationItems,
|
||||
listQuotationTopics
|
||||
} from '@/features/crm/quotations/server/service';
|
||||
listQuotationTopics,
|
||||
} from "@/features/crm/quotations/server/service";
|
||||
import type {
|
||||
ApprovedQuotationSnapshot,
|
||||
QuotationDocumentApprovalStep,
|
||||
QuotationDocumentData
|
||||
} from '../types';
|
||||
import type { PdfTopic } from './pdf-topic.type';
|
||||
import { formatPdfCurrency, formatPdfDate, formatTopicItems } from './pdfme-transforms';
|
||||
import { buildPdfTopicTemplate } from './pdf-topic-engine';
|
||||
import { resolveQuotationSignatures } from './signature-resolver';
|
||||
import { resolveQuotationTopicTypeMapping } from './topic-mapping';
|
||||
import type { Template } from '@pdfme/common';
|
||||
QuotationDocumentData,
|
||||
} from "../types";
|
||||
import type { PdfTopic } from "./pdf-topic.type";
|
||||
import {
|
||||
buildPdfPriceTableData,
|
||||
formatPdfCurrency,
|
||||
formatPdfDate,
|
||||
formatTopicItems,
|
||||
} from "./pdfme-transforms";
|
||||
import { buildPdfTopicTemplate } from "./pdf-topic-engine";
|
||||
import { resolveQuotationSignatures } from "./signature-resolver";
|
||||
import { resolveQuotationTopicTypeMapping } from "./topic-mapping";
|
||||
import type { Template } from "@pdfme/common";
|
||||
|
||||
const DOCUMENT_OPTION_CATEGORIES = {
|
||||
branch: 'crm_branch',
|
||||
status: 'crm_quotation_status',
|
||||
quotationType: 'crm_quotation_type',
|
||||
currency: 'crm_currency',
|
||||
discountType: 'crm_discount_type',
|
||||
unit: 'crm_unit',
|
||||
customerRole: 'crm_project_party_role',
|
||||
productType: 'crm_product_type',
|
||||
topicType: 'crm_quotation_topic_type'
|
||||
branch: "crm_branch",
|
||||
status: "crm_quotation_status",
|
||||
quotationType: "crm_quotation_type",
|
||||
currency: "crm_currency",
|
||||
discountType: "crm_discount_type",
|
||||
unit: "crm_unit",
|
||||
customerRole: "crm_project_party_role",
|
||||
productType: "crm_product_type",
|
||||
topicType: "crm_quotation_topic_type",
|
||||
} as const;
|
||||
|
||||
const PDFME_DEFAULT_LABELS = {
|
||||
tel: "Tel",
|
||||
email: "Email",
|
||||
att: "Att",
|
||||
project: "Project",
|
||||
location: "Location",
|
||||
} as const;
|
||||
|
||||
function toRevisionLabel(revision: number) {
|
||||
return `R${String(revision).padStart(2, '0')}`;
|
||||
return `R${String(revision).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
async function assertQuotationDocumentAccess(quotationId: string, organizationId: string) {
|
||||
async function assertQuotationDocumentAccess(
|
||||
quotationId: string,
|
||||
organizationId: string,
|
||||
) {
|
||||
const [quotation] = await db
|
||||
.select()
|
||||
.from(crmQuotations)
|
||||
@@ -54,13 +75,13 @@ async function assertQuotationDocumentAccess(quotationId: string, organizationId
|
||||
and(
|
||||
eq(crmQuotations.id, quotationId),
|
||||
eq(crmQuotations.organizationId, organizationId),
|
||||
isNull(crmQuotations.deletedAt)
|
||||
)
|
||||
isNull(crmQuotations.deletedAt),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!quotation) {
|
||||
throw new AuthError('Quotation not found', 404);
|
||||
throw new AuthError("Quotation not found", 404);
|
||||
}
|
||||
|
||||
return quotation;
|
||||
@@ -68,14 +89,16 @@ async function assertQuotationDocumentAccess(quotationId: string, organizationId
|
||||
|
||||
function findOptionLabel(
|
||||
options: Array<{ id: string; code: string; label: string }>,
|
||||
id: string | null | undefined
|
||||
id: string | null | undefined,
|
||||
) {
|
||||
return id ? (options.find((option) => option.id === id)?.label ?? null) : null;
|
||||
return id
|
||||
? (options.find((option) => option.id === id)?.label ?? null)
|
||||
: null;
|
||||
}
|
||||
|
||||
function findOptionCode(
|
||||
options: Array<{ id: string; code: string; label: string }>,
|
||||
id: string | null | undefined
|
||||
id: string | null | undefined,
|
||||
) {
|
||||
return id ? (options.find((option) => option.id === id)?.code ?? null) : null;
|
||||
}
|
||||
@@ -88,9 +111,11 @@ function extractSinglePlaceholder(value: string) {
|
||||
}
|
||||
|
||||
function getFieldName(field: unknown) {
|
||||
return field && typeof field === 'object' && typeof (field as { name?: unknown }).name === 'string'
|
||||
? ((field as { name: string }).name ?? '')
|
||||
: '';
|
||||
return field &&
|
||||
typeof field === "object" &&
|
||||
typeof (field as { name?: unknown }).name === "string"
|
||||
? ((field as { name: string }).name ?? "")
|
||||
: "";
|
||||
}
|
||||
|
||||
function normalizeTopicKey(value: string | null | undefined) {
|
||||
@@ -98,11 +123,14 @@ function normalizeTopicKey(value: string | null | undefined) {
|
||||
value
|
||||
?.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(/[\s_-]+/g, '') ?? ''
|
||||
.replaceAll(/[\s_-]+/g, "") ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
function matchesTopicAlias(actualCode: string | null | undefined, expectedCode: string) {
|
||||
function matchesTopicAlias(
|
||||
actualCode: string | null | undefined,
|
||||
expectedCode: string,
|
||||
) {
|
||||
const actual = normalizeTopicKey(actualCode);
|
||||
const expected = normalizeTopicKey(expectedCode);
|
||||
|
||||
@@ -115,30 +143,33 @@ function matchesTopicAlias(actualCode: string | null | undefined, expectedCode:
|
||||
}
|
||||
|
||||
const aliasGroups = [
|
||||
['scope', 'scopeofwork', 'dockscopeofwork', 'solarcellscopeofwork'],
|
||||
["scope", "scopeofwork", "dockscopeofwork", "solarcellscopeofwork"],
|
||||
[
|
||||
'exclusion',
|
||||
'exclusionfromscopeofsupply',
|
||||
'exclusionofsupplyinstallation',
|
||||
'solarcellexclusionofsupplyinstallation'
|
||||
"exclusion",
|
||||
"exclusionfromscopeofsupply",
|
||||
"exclusionofsupplyinstallation",
|
||||
"solarcellexclusionofsupplyinstallation",
|
||||
],
|
||||
[
|
||||
'payment',
|
||||
'paymentconditions',
|
||||
'termsofpaymentuponprogressofeachitem',
|
||||
'solarcellpaymentconditions'
|
||||
"payment",
|
||||
"paymentconditions",
|
||||
"termsofpaymentuponprogressofeachitem",
|
||||
"solarcellpaymentconditions",
|
||||
],
|
||||
['delivery', 'deliverydate', 'solarcelldelivery'],
|
||||
['warranty', 'solarcellwarranty']
|
||||
["delivery", "deliverydate", "solarcelldelivery"],
|
||||
["warranty", "solarcellwarranty"],
|
||||
];
|
||||
|
||||
return aliasGroups.some((group) => group.includes(actual) && group.includes(expected));
|
||||
return aliasGroups.some(
|
||||
(group) => group.includes(actual) && group.includes(expected),
|
||||
);
|
||||
}
|
||||
|
||||
function sortBySortOrder<T extends { sortOrder?: number | null }>(items: T[]) {
|
||||
return [...items].sort((left, right) => {
|
||||
const delta =
|
||||
(left.sortOrder ?? Number.MAX_SAFE_INTEGER) - (right.sortOrder ?? Number.MAX_SAFE_INTEGER);
|
||||
(left.sortOrder ?? Number.MAX_SAFE_INTEGER) -
|
||||
(right.sortOrder ?? Number.MAX_SAFE_INTEGER);
|
||||
|
||||
if (delta !== 0) {
|
||||
return delta;
|
||||
@@ -148,9 +179,12 @@ function sortBySortOrder<T extends { sortOrder?: number | null }>(items: T[]) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizePdfmeTemplateInput(schemaJson: unknown, templateInput: Record<string, unknown>) {
|
||||
function normalizePdfmeTemplateInput(
|
||||
schemaJson: unknown,
|
||||
templateInput: Record<string, unknown>,
|
||||
) {
|
||||
const pages =
|
||||
schemaJson && typeof schemaJson === 'object' && 'schemas' in schemaJson
|
||||
schemaJson && typeof schemaJson === "object" && "schemas" in schemaJson
|
||||
? (schemaJson as { schemas?: unknown }).schemas
|
||||
: null;
|
||||
|
||||
@@ -166,28 +200,37 @@ function normalizePdfmeTemplateInput(schemaJson: unknown, templateInput: Record<
|
||||
}
|
||||
|
||||
for (const field of page) {
|
||||
if (!field || typeof field !== 'object') {
|
||||
if (!field || typeof field !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableField = field as { type?: unknown; content?: unknown };
|
||||
const fieldName = getFieldName(field);
|
||||
const placeholderKey =
|
||||
typeof tableField.content === 'string'
|
||||
typeof tableField.content === "string"
|
||||
? extractSinglePlaceholder(tableField.content)
|
||||
: null;
|
||||
|
||||
if (fieldName && placeholderKey && fieldName !== placeholderKey) {
|
||||
if (normalized[fieldName] !== undefined && normalized[placeholderKey] === undefined) {
|
||||
if (
|
||||
normalized[fieldName] !== undefined &&
|
||||
normalized[placeholderKey] === undefined
|
||||
) {
|
||||
normalized[placeholderKey] = normalized[fieldName];
|
||||
}
|
||||
|
||||
if (normalized[placeholderKey] !== undefined && normalized[fieldName] === undefined) {
|
||||
if (
|
||||
normalized[placeholderKey] !== undefined &&
|
||||
normalized[fieldName] === undefined
|
||||
) {
|
||||
normalized[fieldName] = normalized[placeholderKey];
|
||||
}
|
||||
}
|
||||
|
||||
if (tableField.type !== 'table' || typeof tableField.content !== 'string') {
|
||||
if (
|
||||
tableField.type !== "table" ||
|
||||
typeof tableField.content !== "string"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -197,12 +240,12 @@ function normalizePdfmeTemplateInput(schemaJson: unknown, templateInput: Record<
|
||||
|
||||
const value = normalized[placeholderKey];
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
if (typeof value !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lines = value
|
||||
.split('\n')
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -219,9 +262,12 @@ function normalizePdfmeTemplateInput(schemaJson: unknown, templateInput: Record<
|
||||
|
||||
export async function buildQuotationDocumentData(
|
||||
quotationId: string,
|
||||
organizationId: string
|
||||
organizationId: string,
|
||||
): Promise<QuotationDocumentData> {
|
||||
const quotation = await assertQuotationDocumentAccess(quotationId, organizationId);
|
||||
const quotation = await assertQuotationDocumentAccess(
|
||||
quotationId,
|
||||
organizationId,
|
||||
);
|
||||
const [
|
||||
company,
|
||||
quotationDetail,
|
||||
@@ -237,7 +283,7 @@ export async function buildQuotationDocumentData(
|
||||
customerRoleOptions,
|
||||
productTypeOptions,
|
||||
topicTypeOptions,
|
||||
approvalRequests
|
||||
approvalRequests,
|
||||
] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
@@ -248,24 +294,42 @@ export async function buildQuotationDocumentData(
|
||||
listQuotationItems(quotationId, organizationId),
|
||||
listQuotationCustomers(quotationId, organizationId),
|
||||
listQuotationTopics(quotationId, organizationId),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.branch, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.status, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.quotationType, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.currency, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.discountType, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.unit, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.customerRole, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.productType, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.topicType, { organizationId }),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.branch, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.status, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.quotationType, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.currency, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.discountType, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.unit, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.customerRole, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.productType, {
|
||||
organizationId,
|
||||
}),
|
||||
getActiveOptionsByCategory(DOCUMENT_OPTION_CATEGORIES.topicType, {
|
||||
organizationId,
|
||||
}),
|
||||
listApprovalRequests(organizationId, {
|
||||
entityType: 'quotation',
|
||||
entityType: "quotation",
|
||||
entityId: quotationId,
|
||||
limit: 20
|
||||
})
|
||||
limit: 20,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!company) {
|
||||
throw new AuthError('Organization not found', 404);
|
||||
throw new AuthError("Organization not found", 404);
|
||||
}
|
||||
|
||||
const [customer, contact, relatedCustomers, signatures] = await Promise.all([
|
||||
@@ -276,8 +340,8 @@ export async function buildQuotationDocumentData(
|
||||
and(
|
||||
eq(crmCustomers.id, quotation.customerId),
|
||||
eq(crmCustomers.organizationId, organizationId),
|
||||
isNull(crmCustomers.deletedAt)
|
||||
)
|
||||
isNull(crmCustomers.deletedAt),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0] ?? null),
|
||||
quotation.contactId
|
||||
@@ -288,47 +352,57 @@ export async function buildQuotationDocumentData(
|
||||
and(
|
||||
eq(crmCustomerContacts.id, quotation.contactId),
|
||||
eq(crmCustomerContacts.organizationId, organizationId),
|
||||
isNull(crmCustomerContacts.deletedAt)
|
||||
)
|
||||
isNull(crmCustomerContacts.deletedAt),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: Promise.resolve(null),
|
||||
quotationCustomers.length
|
||||
? db
|
||||
.select({ id: crmCustomers.id, code: crmCustomers.code, name: crmCustomers.name })
|
||||
.select({
|
||||
id: crmCustomers.id,
|
||||
code: crmCustomers.code,
|
||||
name: crmCustomers.name,
|
||||
})
|
||||
.from(crmCustomers)
|
||||
.where(
|
||||
and(
|
||||
eq(crmCustomers.organizationId, organizationId),
|
||||
inArray(
|
||||
crmCustomers.id,
|
||||
quotationCustomers.map((item) => item.customerId)
|
||||
quotationCustomers.map((item) => item.customerId),
|
||||
),
|
||||
isNull(crmCustomers.deletedAt)
|
||||
)
|
||||
isNull(crmCustomers.deletedAt),
|
||||
),
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
resolveQuotationSignatures(quotationId, organizationId)
|
||||
resolveQuotationSignatures(quotationId, organizationId),
|
||||
]);
|
||||
|
||||
if (!customer) {
|
||||
throw new AuthError('Customer not found', 404);
|
||||
throw new AuthError("Customer not found", 404);
|
||||
}
|
||||
|
||||
const approvalRequest =
|
||||
approvalRequests.items.find((item) => item.status === 'approved') ??
|
||||
approvalRequests.items.find((item) => item.status === "approved") ??
|
||||
approvalRequests.items[0] ??
|
||||
null;
|
||||
const approvalDetail = approvalRequest
|
||||
? await getApprovalRequest(approvalRequest.id, organizationId)
|
||||
: null;
|
||||
const branch = branchOptions.find((option) => option.id === quotation.branchId) ?? null;
|
||||
const relatedCustomerMap = new Map(relatedCustomers.map((item) => [item.id, item]));
|
||||
const branch =
|
||||
branchOptions.find((option) => option.id === quotation.branchId) ?? null;
|
||||
const relatedCustomerMap = new Map(
|
||||
relatedCustomers.map((item) => [item.id, item]),
|
||||
);
|
||||
const statusCode = findOptionCode(statusOptions, quotation.status);
|
||||
const statusLabel = findOptionLabel(statusOptions, quotation.status);
|
||||
const topicCodeById = new Map(topicTypeOptions.map((option) => [option.id, option.code]));
|
||||
const topicCodeById = new Map(
|
||||
topicTypeOptions.map((option) => [option.id, option.code]),
|
||||
);
|
||||
const primaryProductType =
|
||||
items.find((item) => findOptionCode(productTypeOptions, item.productType))?.productType ?? null;
|
||||
items.find((item) => findOptionCode(productTypeOptions, item.productType))
|
||||
?.productType ?? null;
|
||||
const productTypeCode =
|
||||
findOptionCode(productTypeOptions, primaryProductType) ??
|
||||
findOptionCode(quotationTypeOptions, quotationDetail.quotationType) ??
|
||||
@@ -340,7 +414,7 @@ export async function buildQuotationDocumentData(
|
||||
findOptionLabel(topicTypeOptions, item.topicType) ??
|
||||
item.title?.trim() ??
|
||||
topicCodeById.get(item.topicType) ??
|
||||
'Topic';
|
||||
"Topic";
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
@@ -351,8 +425,8 @@ export async function buildQuotationDocumentData(
|
||||
items: sortBySortOrder(item.items).map((child) => ({
|
||||
id: child.id,
|
||||
content: child.content,
|
||||
sortOrder: child.sortOrder
|
||||
}))
|
||||
sortOrder: child.sortOrder,
|
||||
})),
|
||||
};
|
||||
});
|
||||
const pdfTopics: PdfTopic[] = resolvedTopics.map((item) => ({
|
||||
@@ -361,24 +435,24 @@ export async function buildQuotationDocumentData(
|
||||
sortOrder: item.sortOrder,
|
||||
items: item.items.map((child) => ({
|
||||
id: child.id,
|
||||
content: child.content?.trim() || '-',
|
||||
sortOrder: child.sortOrder
|
||||
}))
|
||||
content: child.content?.trim() || "-",
|
||||
sortOrder: child.sortOrder,
|
||||
})),
|
||||
}));
|
||||
const scopeTopics = resolvedTopics.filter((item) =>
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.scope)
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.scope),
|
||||
);
|
||||
const exclusionTopics = resolvedTopics.filter((item) =>
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.exclusion)
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.exclusion),
|
||||
);
|
||||
const paymentTopics = resolvedTopics.filter((item) =>
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.payment)
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.payment),
|
||||
);
|
||||
const warrantyTopics = resolvedTopics.filter((item) =>
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.warranty)
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.warranty),
|
||||
);
|
||||
const deliveryTopics = resolvedTopics.filter((item) =>
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.delivery)
|
||||
matchesTopicAlias(item.topicCode, topicTypeMapping.delivery),
|
||||
);
|
||||
const otherTopics = resolvedTopics.filter((item) => {
|
||||
return ![
|
||||
@@ -386,7 +460,7 @@ export async function buildQuotationDocumentData(
|
||||
topicTypeMapping.exclusion,
|
||||
topicTypeMapping.payment,
|
||||
topicTypeMapping.warranty,
|
||||
topicTypeMapping.delivery
|
||||
topicTypeMapping.delivery,
|
||||
].some((expectedCode) => matchesTopicAlias(item.topicCode, expectedCode));
|
||||
});
|
||||
const exclusionTopic = exclusionTopics[0];
|
||||
@@ -395,21 +469,23 @@ export async function buildQuotationDocumentData(
|
||||
const deliveryTopic = deliveryTopics[0];
|
||||
const approvalApprovers: QuotationDocumentApprovalStep[] =
|
||||
approvalDetail?.timeline
|
||||
.filter((item) => ['approve', 'reject', 'return'].includes(item.action))
|
||||
.filter((item) => ["approve", "reject", "return"].includes(item.action))
|
||||
.map((item) => {
|
||||
const step = approvalDetail.steps.find(
|
||||
(candidate) => candidate.stepNumber === item.stepNumber
|
||||
(candidate) => candidate.stepNumber === item.stepNumber,
|
||||
);
|
||||
return {
|
||||
stepNumber: item.stepNumber,
|
||||
roleCode: step?.roleCode ?? '',
|
||||
roleCode: step?.roleCode ?? "",
|
||||
roleName: step?.roleName ?? `Step ${item.stepNumber}`,
|
||||
action: item.action,
|
||||
actorName: item.actorName,
|
||||
actedAt: item.actedAt,
|
||||
remark: item.remark
|
||||
remark: item.remark,
|
||||
};
|
||||
}) ?? [];
|
||||
const currencyCode =
|
||||
findOptionCode(currencyOptions, quotationDetail.currency) ?? "THB";
|
||||
|
||||
return {
|
||||
company: {
|
||||
@@ -417,14 +493,14 @@ export async function buildQuotationDocumentData(
|
||||
name: company.name,
|
||||
slug: company.slug,
|
||||
imageUrl: company.imageUrl,
|
||||
plan: company.plan
|
||||
plan: company.plan,
|
||||
},
|
||||
branch: branch
|
||||
? {
|
||||
id: branch.id,
|
||||
code: branch.code,
|
||||
label: branch.label,
|
||||
value: branch.value
|
||||
value: branch.value,
|
||||
}
|
||||
: null,
|
||||
customer: {
|
||||
@@ -434,7 +510,7 @@ export async function buildQuotationDocumentData(
|
||||
address: customer.address,
|
||||
phone: customer.phone,
|
||||
email: customer.email,
|
||||
taxId: customer.taxId
|
||||
taxId: customer.taxId,
|
||||
},
|
||||
contact: contact
|
||||
? {
|
||||
@@ -442,7 +518,7 @@ export async function buildQuotationDocumentData(
|
||||
name: contact.name,
|
||||
email: contact.email,
|
||||
mobile: contact.mobile,
|
||||
position: contact.position
|
||||
position: contact.position,
|
||||
}
|
||||
: null,
|
||||
quotation: {
|
||||
@@ -458,20 +534,32 @@ export async function buildQuotationDocumentData(
|
||||
revisionLabel: toRevisionLabel(quotationDetail.revision),
|
||||
statusCode,
|
||||
statusLabel,
|
||||
quotationTypeCode: findOptionCode(quotationTypeOptions, quotationDetail.quotationType),
|
||||
quotationTypeLabel: findOptionLabel(quotationTypeOptions, quotationDetail.quotationType),
|
||||
currency: findOptionCode(currencyOptions, quotationDetail.currency),
|
||||
currencyCode: findOptionCode(currencyOptions, quotationDetail.currency),
|
||||
quotationTypeCode: findOptionCode(
|
||||
quotationTypeOptions,
|
||||
quotationDetail.quotationType,
|
||||
),
|
||||
quotationTypeLabel: findOptionLabel(
|
||||
quotationTypeOptions,
|
||||
quotationDetail.quotationType,
|
||||
),
|
||||
currency: currencyCode,
|
||||
currencyCode,
|
||||
currencyLabel: findOptionLabel(currencyOptions, quotationDetail.currency),
|
||||
exchangeRate: quotationDetail.exchangeRate,
|
||||
subtotal: quotationDetail.subtotal,
|
||||
discount: quotationDetail.discount,
|
||||
discountTypeCode: findOptionCode(discountTypeOptions, quotationDetail.discountType),
|
||||
discountTypeLabel: findOptionLabel(discountTypeOptions, quotationDetail.discountType),
|
||||
discountTypeCode: findOptionCode(
|
||||
discountTypeOptions,
|
||||
quotationDetail.discountType,
|
||||
),
|
||||
discountTypeLabel: findOptionLabel(
|
||||
discountTypeOptions,
|
||||
quotationDetail.discountType,
|
||||
),
|
||||
taxRate: quotationDetail.taxRate,
|
||||
taxAmount: quotationDetail.taxAmount,
|
||||
totalAmount: quotationDetail.totalAmount,
|
||||
notes: quotationDetail.notes
|
||||
notes: quotationDetail.notes,
|
||||
},
|
||||
items: items.map((item) => ({
|
||||
id: item.id,
|
||||
@@ -487,101 +575,128 @@ export async function buildQuotationDocumentData(
|
||||
discountTypeCode: findOptionCode(discountTypeOptions, item.discountType),
|
||||
taxRate: item.taxRate,
|
||||
totalPrice: item.totalPrice,
|
||||
notes: item.notes
|
||||
notes: item.notes,
|
||||
})),
|
||||
quotationCustomers: quotationCustomers.map((item) => ({
|
||||
id: item.id,
|
||||
customerId: item.customerId,
|
||||
customerName: relatedCustomerMap.get(item.customerId)?.name ?? item.customerName,
|
||||
customerCode: relatedCustomerMap.get(item.customerId)?.code ?? item.customerCode,
|
||||
customerName:
|
||||
relatedCustomerMap.get(item.customerId)?.name ?? item.customerName,
|
||||
customerCode:
|
||||
relatedCustomerMap.get(item.customerId)?.code ?? item.customerCode,
|
||||
roleCode: item.roleCode,
|
||||
roleLabel: item.roleLabel ?? findOptionLabel(customerRoleOptions, item.role),
|
||||
isPrimary: item.isPrimary
|
||||
roleLabel:
|
||||
item.roleLabel ?? findOptionLabel(customerRoleOptions, item.role),
|
||||
isPrimary: item.isPrimary,
|
||||
})),
|
||||
topics: {
|
||||
scope: scopeTopics.map((item) => ({
|
||||
title: item.title,
|
||||
items: item.items.map((child) => child.content ?? '-')
|
||||
items: item.items.map((child) => child.content ?? "-"),
|
||||
})),
|
||||
exclusion: exclusionTopics.map((item) => ({
|
||||
title: item.title,
|
||||
items: item.items.map((child) => child.content ?? '-')
|
||||
items: item.items.map((child) => child.content ?? "-"),
|
||||
})),
|
||||
payment: paymentTopics.map((item) => ({
|
||||
title: item.title,
|
||||
items: item.items.map((child) => child.content ?? '-')
|
||||
items: item.items.map((child) => child.content ?? "-"),
|
||||
})),
|
||||
warranty: warrantyTopics.map((item) => ({
|
||||
title: item.title,
|
||||
items: item.items.map((child) => child.content ?? '-')
|
||||
items: item.items.map((child) => child.content ?? "-"),
|
||||
})),
|
||||
delivery: deliveryTopics.map((item) => ({
|
||||
title: item.title,
|
||||
items: item.items.map((child) => child.content ?? '-')
|
||||
items: item.items.map((child) => child.content ?? "-"),
|
||||
})),
|
||||
other: otherTopics.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
sortOrder: item.sortOrder,
|
||||
items: item.items.map((child) => child.content ?? '-')
|
||||
items: item.items.map((child) => child.content ?? "-"),
|
||||
})),
|
||||
all: pdfTopics
|
||||
all: pdfTopics,
|
||||
},
|
||||
pdfme: {
|
||||
quotation_date: formatPdfDate(quotationDetail.quotationDate, 'en'),
|
||||
labels: PDFME_DEFAULT_LABELS,
|
||||
quotation_date: formatPdfDate(quotationDetail.quotationDate, "en"),
|
||||
quotation_price: formatPdfCurrency(
|
||||
quotationDetail.totalAmount,
|
||||
findOptionCode(currencyOptions, quotationDetail.currency) ?? 'THB'
|
||||
quotationDetail.subtotal,
|
||||
currencyCode,
|
||||
),
|
||||
quotation_price_data: buildPdfPriceTableData(
|
||||
quotationDetail.subtotal,
|
||||
currencyCode,
|
||||
),
|
||||
exclusion_data: formatTopicItems(exclusionTopic),
|
||||
payment_data: formatTopicItems(paymentTopic),
|
||||
warranty_data: formatTopicItems(warrantyTopic),
|
||||
delivery_data: formatTopicItems(deliveryTopic),
|
||||
topics_data: formatTopicItems(scopeTopics[0] ?? exclusionTopic ?? paymentTopic),
|
||||
topic_inputs: {}
|
||||
topics_data: formatTopicItems(
|
||||
scopeTopics[0] ?? exclusionTopic ?? paymentTopic,
|
||||
),
|
||||
topic_inputs: {},
|
||||
},
|
||||
approval: {
|
||||
requestId: approvalDetail?.request.id ?? null,
|
||||
workflowName: approvalDetail?.workflow.name ?? null,
|
||||
status: approvalDetail?.request.status ?? null,
|
||||
approvedAt:
|
||||
approvalDetail?.request.status === 'approved' ? approvalDetail.request.completedAt : null,
|
||||
approvalDetail?.request.status === "approved"
|
||||
? approvalDetail.request.completedAt
|
||||
: null,
|
||||
currentStep: approvalDetail?.request.currentStep ?? null,
|
||||
currentStepRoleName: approvalDetail?.currentStep?.roleName ?? null,
|
||||
approvers: approvalApprovers
|
||||
approvers: approvalApprovers,
|
||||
},
|
||||
signatures,
|
||||
watermarkStatus: statusCode && statusCode !== 'approved' ? (statusLabel ?? statusCode) : null
|
||||
watermarkStatus:
|
||||
statusCode && statusCode !== "approved"
|
||||
? (statusLabel ?? statusCode)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getQuotationDocumentPreviewData(quotationId: string, organizationId: string) {
|
||||
const documentData = await buildQuotationDocumentData(quotationId, organizationId);
|
||||
export async function getQuotationDocumentPreviewData(
|
||||
quotationId: string,
|
||||
organizationId: string,
|
||||
) {
|
||||
const documentData = await buildQuotationDocumentData(
|
||||
quotationId,
|
||||
organizationId,
|
||||
);
|
||||
const template = await resolveTemplateForDocument(organizationId, {
|
||||
documentType: 'quotation',
|
||||
productType: 'default',
|
||||
fileType: 'pdfme'
|
||||
documentType: "quotation",
|
||||
productType: "default",
|
||||
fileType: "pdfme",
|
||||
});
|
||||
const mappings = await resolveTemplateMappings(template.version.id, organizationId);
|
||||
const mappings = await resolveTemplateMappings(
|
||||
template.version.id,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
if (!mappings.length) {
|
||||
throw new AuthError(
|
||||
'Document template mappings are not configured for this template version',
|
||||
409
|
||||
"Document template mappings are not configured for this template version",
|
||||
409,
|
||||
);
|
||||
}
|
||||
|
||||
const templateInput = normalizePdfmeTemplateInput(
|
||||
template.version.schemaJson,
|
||||
mapDocumentDataToTemplateInput(documentData as unknown as Record<string, unknown>, mappings)
|
||||
mapDocumentDataToTemplateInput(
|
||||
documentData as unknown as Record<string, unknown>,
|
||||
mappings,
|
||||
),
|
||||
);
|
||||
const { template: renderTemplate, topicInputs } = buildPdfTopicTemplate(
|
||||
template.version.schemaJson as Template,
|
||||
documentData.topics.all
|
||||
documentData.topics.all,
|
||||
);
|
||||
const mergedTemplateInput = {
|
||||
...templateInput,
|
||||
...topicInputs
|
||||
...topicInputs,
|
||||
};
|
||||
|
||||
documentData.pdfme.topic_inputs = topicInputs;
|
||||
@@ -592,20 +707,23 @@ export async function getQuotationDocumentPreviewData(quotationId: string, organ
|
||||
...template,
|
||||
version: {
|
||||
...template.version,
|
||||
schemaJson: renderTemplate
|
||||
}
|
||||
schemaJson: renderTemplate,
|
||||
},
|
||||
},
|
||||
mappings,
|
||||
templateInput: mergedTemplateInput
|
||||
templateInput: mergedTemplateInput,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareApprovedQuotationSnapshot(
|
||||
quotationId: string,
|
||||
organizationId: string,
|
||||
userId: string
|
||||
userId: string,
|
||||
): Promise<ApprovedQuotationSnapshot> {
|
||||
const preview = await getQuotationDocumentPreviewData(quotationId, organizationId);
|
||||
const preview = await getQuotationDocumentPreviewData(
|
||||
quotationId,
|
||||
organizationId,
|
||||
);
|
||||
const generatedAt = new Date().toISOString();
|
||||
|
||||
return {
|
||||
@@ -615,6 +733,6 @@ export async function prepareApprovedQuotationSnapshot(
|
||||
templateVersionId: preview.template.version.id,
|
||||
templateInput: preview.templateInput,
|
||||
generatedAt,
|
||||
generatedBy: userId
|
||||
generatedBy: userId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,8 +115,16 @@ export interface QuotationDocumentData {
|
||||
all: PdfTopic[];
|
||||
};
|
||||
pdfme: {
|
||||
labels: {
|
||||
tel: string;
|
||||
email: string;
|
||||
att: string;
|
||||
project: string;
|
||||
location: string;
|
||||
};
|
||||
quotation_date: string;
|
||||
quotation_price: string;
|
||||
quotation_price_data: string[][];
|
||||
exclusion_data: string[][];
|
||||
payment_data?: string[][];
|
||||
warranty_data?: string[][];
|
||||
|
||||
@@ -704,6 +704,44 @@ function applyFormatMask(value: unknown, formatMask?: string | null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseTableDefaultValue(defaultValue?: string | null) {
|
||||
if (!defaultValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(defaultValue) as unknown;
|
||||
|
||||
return Array.isArray(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isBlankString(value: unknown) {
|
||||
return typeof value === 'string' && value.trim().length === 0;
|
||||
}
|
||||
|
||||
function resolveScalarMappingValue(
|
||||
rawValue: unknown,
|
||||
formattedValue: unknown,
|
||||
defaultValue?: string | null
|
||||
) {
|
||||
if (formattedValue !== undefined && formattedValue !== null && !isBlankString(formattedValue)) {
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
if (rawValue !== undefined && rawValue !== null && !isBlankString(rawValue)) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
if (defaultValue !== undefined && defaultValue !== null && defaultValue.trim().length > 0) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return '-';
|
||||
}
|
||||
|
||||
export function mapDocumentDataToTemplateInput(
|
||||
documentData: Record<string, unknown>,
|
||||
mappings: DocumentTemplateMappingWithColumns[]
|
||||
@@ -715,9 +753,10 @@ export function mapDocumentDataToTemplateInput(
|
||||
|
||||
if (mapping.dataType === 'table') {
|
||||
const rows = Array.isArray(rawValue) ? rawValue : [];
|
||||
const defaultRows = parseTableDefaultValue(mapping.defaultValue);
|
||||
|
||||
if (rows.length === 0) {
|
||||
result[mapping.placeholderKey] = normalizePdfmeTable([]);
|
||||
result[mapping.placeholderKey] = normalizePdfmeTable(defaultRows ?? []);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -760,17 +799,23 @@ export function mapDocumentDataToTemplateInput(
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
result[mapping.placeholderKey] = normalized || mapping.defaultValue || '';
|
||||
result[mapping.placeholderKey] = normalized || mapping.defaultValue || '-';
|
||||
continue;
|
||||
}
|
||||
|
||||
result[mapping.placeholderKey] =
|
||||
typeof rawValue === 'string' ? rawValue : mapping.defaultValue || '';
|
||||
typeof rawValue === 'string' && rawValue.trim().length > 0
|
||||
? rawValue
|
||||
: mapping.defaultValue || '-';
|
||||
continue;
|
||||
}
|
||||
|
||||
result[mapping.placeholderKey] =
|
||||
applyFormatMask(rawValue, mapping.formatMask) ?? mapping.defaultValue ?? '-';
|
||||
const formattedValue = applyFormatMask(rawValue, mapping.formatMask);
|
||||
result[mapping.placeholderKey] = resolveScalarMappingValue(
|
||||
rawValue,
|
||||
formattedValue,
|
||||
mapping.defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -341,152 +341,6 @@
|
||||
"required": false,
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"name": "exclusion_label",
|
||||
"type": "table",
|
||||
"position": {
|
||||
"x": 10,
|
||||
"y": 195
|
||||
},
|
||||
"width": 190,
|
||||
"height": 5.6448,
|
||||
"content": "[[\"Exclusion from scope of Supply:\"]]",
|
||||
"showHead": false,
|
||||
"repeatHead": false,
|
||||
"head": [
|
||||
"Name"
|
||||
],
|
||||
"headWidthPercentages": [
|
||||
100
|
||||
],
|
||||
"tableStyles": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#000000"
|
||||
},
|
||||
"headStyles": {
|
||||
"fontName": "cordia",
|
||||
"fontSize": 13,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#ffffff",
|
||||
"borderColor": "",
|
||||
"backgroundColor": "#2980ba",
|
||||
"borderWidth": {
|
||||
"top": 0,
|
||||
"right": 0,
|
||||
"bottom": 0,
|
||||
"left": 0
|
||||
},
|
||||
"padding": {
|
||||
"top": 5,
|
||||
"right": 5,
|
||||
"bottom": 5,
|
||||
"left": 5
|
||||
}
|
||||
},
|
||||
"bodyStyles": {
|
||||
"fontName": "cordiaBold",
|
||||
"fontSize": 16,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#000000",
|
||||
"borderColor": "#ffffff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"alternateBackgroundColor": "#ffffff",
|
||||
"borderWidth": {
|
||||
"top": 0.1,
|
||||
"right": 0.1,
|
||||
"bottom": 0.1,
|
||||
"left": 0.1
|
||||
},
|
||||
"padding": {
|
||||
"top": 0,
|
||||
"right": 0,
|
||||
"bottom": 0,
|
||||
"left": 0
|
||||
}
|
||||
},
|
||||
"columnStyles": {},
|
||||
"required": false,
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"name": "exclusion_data",
|
||||
"type": "table",
|
||||
"position": {
|
||||
"x": 15,
|
||||
"y": 205
|
||||
},
|
||||
"width": 185,
|
||||
"height": 7.6448,
|
||||
"content": "[[\"{exclusion_data}\"]]",
|
||||
"showHead": false,
|
||||
"repeatHead": false,
|
||||
"head": [
|
||||
"Name"
|
||||
],
|
||||
"headWidthPercentages": [
|
||||
100
|
||||
],
|
||||
"tableStyles": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#000000"
|
||||
},
|
||||
"headStyles": {
|
||||
"fontName": "cordia",
|
||||
"fontSize": 13,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#ffffff",
|
||||
"borderColor": "",
|
||||
"backgroundColor": "#2980ba",
|
||||
"borderWidth": {
|
||||
"top": 0,
|
||||
"right": 0,
|
||||
"bottom": 0,
|
||||
"left": 0
|
||||
},
|
||||
"padding": {
|
||||
"top": 5,
|
||||
"right": 5,
|
||||
"bottom": 5,
|
||||
"left": 5
|
||||
}
|
||||
},
|
||||
"bodyStyles": {
|
||||
"fontName": "cordia",
|
||||
"fontSize": 16,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#000000",
|
||||
"borderColor": "#ffffff",
|
||||
"backgroundColor": "",
|
||||
"alternateBackgroundColor": "#ffffff",
|
||||
"borderWidth": {
|
||||
"top": 0.1,
|
||||
"right": 0.1,
|
||||
"bottom": 0.1,
|
||||
"left": 0.1
|
||||
},
|
||||
"padding": {
|
||||
"top": 1,
|
||||
"right": 0,
|
||||
"bottom": 1,
|
||||
"left": 0
|
||||
}
|
||||
},
|
||||
"columnStyles": {},
|
||||
"required": false,
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"name": "quotation_price_data",
|
||||
"type": "table",
|
||||
@@ -499,17 +353,9 @@
|
||||
"content": "[[\"Price (Exclude VAT)\",\"{quotation_price}\",\" \",\"{currency}\"]]",
|
||||
"showHead": false,
|
||||
"repeatHead": false,
|
||||
"head": [
|
||||
"Name",
|
||||
"City",
|
||||
"Head 3",
|
||||
"Head 4"
|
||||
],
|
||||
"head": ["Name", "City", "Head 3", "Head 4"],
|
||||
"headWidthPercentages": [
|
||||
28.125,
|
||||
58.34767550533545,
|
||||
2.4513428524798244,
|
||||
11.075981642184729
|
||||
28.125, 58.34767550533545, 2.4513428524798244, 11.075981642184729
|
||||
],
|
||||
"tableStyles": {
|
||||
"borderWidth": 0,
|
||||
@@ -951,7 +797,7 @@
|
||||
{
|
||||
"name": "colon_site",
|
||||
"type": "text",
|
||||
"content": ":",
|
||||
"content": "",
|
||||
"position": {
|
||||
"x": 25,
|
||||
"y": 140.03
|
||||
@@ -1124,12 +970,8 @@
|
||||
"content": "[[\"Topic:\"]]",
|
||||
"showHead": false,
|
||||
"repeatHead": false,
|
||||
"head": [
|
||||
"Terms of payment upon progress of each item:"
|
||||
],
|
||||
"headWidthPercentages": [
|
||||
100
|
||||
],
|
||||
"head": ["Terms of payment upon progress of each item:"],
|
||||
"headWidthPercentages": [100],
|
||||
"tableStyles": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#000000"
|
||||
@@ -1197,12 +1039,8 @@
|
||||
"content": "[[\"{item_topic}\"]]",
|
||||
"showHead": false,
|
||||
"repeatHead": false,
|
||||
"head": [
|
||||
"Head 2"
|
||||
],
|
||||
"headWidthPercentages": [
|
||||
100
|
||||
],
|
||||
"head": ["Head 2"],
|
||||
"headWidthPercentages": [100],
|
||||
"tableStyles": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#000000"
|
||||
@@ -1479,12 +1317,7 @@
|
||||
"basePdf": {
|
||||
"width": 210,
|
||||
"height": 297,
|
||||
"padding": [
|
||||
31,
|
||||
10,
|
||||
25,
|
||||
10
|
||||
],
|
||||
"padding": [31, 10, 25, 10],
|
||||
"staticSchema": [
|
||||
{
|
||||
"name": "field1",
|
||||
|
||||
@@ -314,144 +314,6 @@
|
||||
"required": false,
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"name": "exclusion_label",
|
||||
"type": "table",
|
||||
"position": {
|
||||
"x": 10,
|
||||
"y": 195
|
||||
},
|
||||
"width": 190,
|
||||
"height": 5.6448,
|
||||
"content": "[[\"Exclusion from scope of Supply:\"]]",
|
||||
"showHead": false,
|
||||
"repeatHead": false,
|
||||
"head": ["Name"],
|
||||
"headWidthPercentages": [100],
|
||||
"tableStyles": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#000000"
|
||||
},
|
||||
"headStyles": {
|
||||
"fontName": "cordia",
|
||||
"fontSize": 13,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#ffffff",
|
||||
"borderColor": "",
|
||||
"backgroundColor": "#2980ba",
|
||||
"borderWidth": {
|
||||
"top": 0,
|
||||
"right": 0,
|
||||
"bottom": 0,
|
||||
"left": 0
|
||||
},
|
||||
"padding": {
|
||||
"top": 5,
|
||||
"right": 5,
|
||||
"bottom": 5,
|
||||
"left": 5
|
||||
}
|
||||
},
|
||||
"bodyStyles": {
|
||||
"fontName": "cordiaBold",
|
||||
"fontSize": 16,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#000000",
|
||||
"borderColor": "#ffffff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"alternateBackgroundColor": "#ffffff",
|
||||
"borderWidth": {
|
||||
"top": 0.1,
|
||||
"right": 0.1,
|
||||
"bottom": 0.1,
|
||||
"left": 0.1
|
||||
},
|
||||
"padding": {
|
||||
"top": 0,
|
||||
"right": 0,
|
||||
"bottom": 0,
|
||||
"left": 0
|
||||
}
|
||||
},
|
||||
"columnStyles": {},
|
||||
"required": false,
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"name": "exclusion_data",
|
||||
"type": "table",
|
||||
"position": {
|
||||
"x": 15,
|
||||
"y": 205
|
||||
},
|
||||
"width": 185,
|
||||
"height": 7.6448,
|
||||
"content": "[[\"{exclusion_data}\"]]",
|
||||
"showHead": false,
|
||||
"repeatHead": false,
|
||||
"head": ["Name"],
|
||||
"headWidthPercentages": [100],
|
||||
"tableStyles": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#000000"
|
||||
},
|
||||
"headStyles": {
|
||||
"fontName": "cordia",
|
||||
"fontSize": 13,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#ffffff",
|
||||
"borderColor": "",
|
||||
"backgroundColor": "#2980ba",
|
||||
"borderWidth": {
|
||||
"top": 0,
|
||||
"right": 0,
|
||||
"bottom": 0,
|
||||
"left": 0
|
||||
},
|
||||
"padding": {
|
||||
"top": 5,
|
||||
"right": 5,
|
||||
"bottom": 5,
|
||||
"left": 5
|
||||
}
|
||||
},
|
||||
"bodyStyles": {
|
||||
"fontName": "cordia",
|
||||
"fontSize": 16,
|
||||
"characterSpacing": 0,
|
||||
"alignment": "left",
|
||||
"verticalAlignment": "middle",
|
||||
"lineHeight": 1,
|
||||
"fontColor": "#000000",
|
||||
"borderColor": "#ffffff",
|
||||
"backgroundColor": "",
|
||||
"alternateBackgroundColor": "#ffffff",
|
||||
"borderWidth": {
|
||||
"top": 0.1,
|
||||
"right": 0.1,
|
||||
"bottom": 0.1,
|
||||
"left": 0.1
|
||||
},
|
||||
"padding": {
|
||||
"top": 1,
|
||||
"right": 0,
|
||||
"bottom": 1,
|
||||
"left": 0
|
||||
}
|
||||
},
|
||||
"columnStyles": {},
|
||||
"required": false,
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"name": "quotation_price_data",
|
||||
"type": "table",
|
||||
@@ -908,7 +770,7 @@
|
||||
{
|
||||
"name": "colon_site",
|
||||
"type": "text",
|
||||
"content": ": ",
|
||||
"content": ":",
|
||||
"position": {
|
||||
"x": 25,
|
||||
"y": 140
|
||||
|
||||
Reference in New Issue
Block a user