|
|
|
|
@@ -30,6 +30,9 @@ type SalesDocumentRecord = {
|
|
|
|
|
status: string;
|
|
|
|
|
issueDate: Date;
|
|
|
|
|
expiresAt?: Date | null;
|
|
|
|
|
discountPercent: number;
|
|
|
|
|
taxPercent: number;
|
|
|
|
|
freightAmount: number;
|
|
|
|
|
notes: string;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
updatedAt: Date;
|
|
|
|
|
@@ -60,6 +63,31 @@ const documentConfig = {
|
|
|
|
|
},
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
function roundMoney(value: number) {
|
|
|
|
|
return Math.round(value * 100) / 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculateTotals(subtotal: number, discountPercent: number, taxPercent: number, freightAmount: number) {
|
|
|
|
|
const normalizedSubtotal = roundMoney(subtotal);
|
|
|
|
|
const normalizedDiscountPercent = Number.isFinite(discountPercent) ? discountPercent : 0;
|
|
|
|
|
const normalizedTaxPercent = Number.isFinite(taxPercent) ? taxPercent : 0;
|
|
|
|
|
const normalizedFreight = roundMoney(Number.isFinite(freightAmount) ? freightAmount : 0);
|
|
|
|
|
const discountAmount = roundMoney(normalizedSubtotal * (normalizedDiscountPercent / 100));
|
|
|
|
|
const taxableSubtotal = roundMoney(normalizedSubtotal - discountAmount);
|
|
|
|
|
const taxAmount = roundMoney(taxableSubtotal * (normalizedTaxPercent / 100));
|
|
|
|
|
const total = roundMoney(taxableSubtotal + taxAmount + normalizedFreight);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
subtotal: normalizedSubtotal,
|
|
|
|
|
discountPercent: normalizedDiscountPercent,
|
|
|
|
|
discountAmount,
|
|
|
|
|
taxPercent: normalizedTaxPercent,
|
|
|
|
|
taxAmount,
|
|
|
|
|
freightAmount: normalizedFreight,
|
|
|
|
|
total,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeLines(lines: SalesLineInput[]) {
|
|
|
|
|
return lines
|
|
|
|
|
.map((line, index) => ({
|
|
|
|
|
@@ -117,6 +145,12 @@ function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto {
|
|
|
|
|
lineTotal: line.quantity * line.unitPrice,
|
|
|
|
|
position: line.position,
|
|
|
|
|
}));
|
|
|
|
|
const totals = calculateTotals(
|
|
|
|
|
lines.reduce((sum, line) => sum + line.lineTotal, 0),
|
|
|
|
|
record.discountPercent,
|
|
|
|
|
record.taxPercent,
|
|
|
|
|
record.freightAmount
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: record.id,
|
|
|
|
|
@@ -125,7 +159,13 @@ function mapDocument(record: SalesDocumentRecord): SalesDocumentDetailDto {
|
|
|
|
|
customerName: record.customer.name,
|
|
|
|
|
customerEmail: record.customer.email,
|
|
|
|
|
status: record.status as SalesDocumentStatus,
|
|
|
|
|
subtotal: lines.reduce((sum, line) => sum + line.lineTotal, 0),
|
|
|
|
|
subtotal: totals.subtotal,
|
|
|
|
|
discountPercent: totals.discountPercent,
|
|
|
|
|
discountAmount: totals.discountAmount,
|
|
|
|
|
taxPercent: totals.taxPercent,
|
|
|
|
|
taxAmount: totals.taxAmount,
|
|
|
|
|
freightAmount: totals.freightAmount,
|
|
|
|
|
total: totals.total,
|
|
|
|
|
issueDate: record.issueDate.toISOString(),
|
|
|
|
|
expiresAt: "expiresAt" in record && record.expiresAt ? record.expiresAt.toISOString() : null,
|
|
|
|
|
notes: record.notes,
|
|
|
|
|
@@ -198,6 +238,7 @@ export async function listSalesCustomerOptions(): Promise<SalesCustomerOptionDto
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
resellerDiscountPercent: true,
|
|
|
|
|
},
|
|
|
|
|
orderBy: [{ name: "asc" }],
|
|
|
|
|
});
|
|
|
|
|
@@ -232,6 +273,12 @@ export async function listSalesDocuments(type: SalesDocumentType, filters: { q?:
|
|
|
|
|
customerName: detail.customerName,
|
|
|
|
|
status: detail.status,
|
|
|
|
|
subtotal: detail.subtotal,
|
|
|
|
|
discountPercent: detail.discountPercent,
|
|
|
|
|
discountAmount: detail.discountAmount,
|
|
|
|
|
taxPercent: detail.taxPercent,
|
|
|
|
|
taxAmount: detail.taxAmount,
|
|
|
|
|
freightAmount: detail.freightAmount,
|
|
|
|
|
total: detail.total,
|
|
|
|
|
issueDate: detail.issueDate,
|
|
|
|
|
updatedAt: detail.updatedAt,
|
|
|
|
|
lineCount: detail.lineCount,
|
|
|
|
|
@@ -274,6 +321,9 @@ export async function createSalesDocument(type: SalesDocumentType, payload: Sale
|
|
|
|
|
status: payload.status,
|
|
|
|
|
issueDate: new Date(payload.issueDate),
|
|
|
|
|
...(type === "QUOTE" ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null } : {}),
|
|
|
|
|
discountPercent: payload.discountPercent,
|
|
|
|
|
taxPercent: payload.taxPercent,
|
|
|
|
|
freightAmount: payload.freightAmount,
|
|
|
|
|
notes: payload.notes,
|
|
|
|
|
lines: {
|
|
|
|
|
create: validatedLines.lines,
|
|
|
|
|
@@ -317,6 +367,9 @@ export async function updateSalesDocument(type: SalesDocumentType, documentId: s
|
|
|
|
|
status: payload.status,
|
|
|
|
|
issueDate: new Date(payload.issueDate),
|
|
|
|
|
...(type === "QUOTE" ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null } : {}),
|
|
|
|
|
discountPercent: payload.discountPercent,
|
|
|
|
|
taxPercent: payload.taxPercent,
|
|
|
|
|
freightAmount: payload.freightAmount,
|
|
|
|
|
notes: payload.notes,
|
|
|
|
|
lines: {
|
|
|
|
|
deleteMany: {},
|
|
|
|
|
@@ -369,6 +422,9 @@ export async function convertQuoteToSalesOrder(quoteId: string) {
|
|
|
|
|
customerId: mappedQuote.customerId,
|
|
|
|
|
status: "DRAFT",
|
|
|
|
|
issueDate: new Date(),
|
|
|
|
|
discountPercent: mappedQuote.discountPercent,
|
|
|
|
|
taxPercent: mappedQuote.taxPercent,
|
|
|
|
|
freightAmount: mappedQuote.freightAmount,
|
|
|
|
|
notes: mappedQuote.notes ? `Converted from ${mappedQuote.documentNumber}\n\n${mappedQuote.notes}` : `Converted from ${mappedQuote.documentNumber}`,
|
|
|
|
|
lines: {
|
|
|
|
|
create: mappedQuote.lines.map((line) => ({
|
|
|
|
|
|