const PRINT_TITLE = "销售订单";
|
|
const formatDisplayDate = (value) => {
|
if (!value) return "";
|
const date = new Date(value);
|
if (Number.isNaN(date.getTime())) return String(value);
|
const year = date.getFullYear();
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
return `${year}/${month}/${day}`;
|
};
|
|
const getCurrentDateTime = () => {
|
const date = new Date();
|
const year = date.getFullYear();
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
return `${year}/${month}/${day}`;
|
};
|
|
const escapeHtml = (value) =>
|
String(value ?? "")
|
.replaceAll("&", "&")
|
.replaceAll("<", "<")
|
.replaceAll(">", ">")
|
.replaceAll('"', """)
|
.replaceAll("'", "'");
|
|
const toNumber = (value) => {
|
const num = Number(value);
|
return Number.isFinite(num) ? num : 0;
|
};
|
|
const formatMoney = (value) => {
|
const num = toNumber(value);
|
return num.toFixed(2);
|
};
|
|
const formatArea = (value) => {
|
const num = toNumber(value);
|
return num ? num.toFixed(2) : "";
|
};
|
|
const formatQty = (value) => {
|
const num = toNumber(value);
|
return num ? String(num) : "";
|
};
|
|
const getItemAmount = (item) => {
|
const fromPrice = toNumber(item?.taxInclusiveUnitPrice) * toNumber(item?.quantity);
|
const amount = toNumber(item?.taxInclusiveTotalPrice || item?.amount || fromPrice);
|
return amount;
|
};
|
|
const getItemArea = (item) =>
|
toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea);
|
|
const getDeliveryAddress = (data) =>
|
data?.companyAddress || data?.deliveryAddress || data?.shippingAddress || data?.address || data?.shipAddress || "";
|
|
const normalizeRequirementText = (value) => {
|
const text = String(value ?? "").trim();
|
if (!text) return "";
|
return text
|
.replace(/^加工要求和备注[::]\s*/g, "")
|
.replace(/^加工要求[::]\s*/g, "")
|
.replace(/^备注[::]\s*/g, "");
|
};
|
|
const extractOtherFees = (data, items) => {
|
const source = [];
|
if (Array.isArray(data?.otherFees)) source.push(...data.otherFees);
|
if (Array.isArray(data?.otherAmounts)) source.push(...data.otherAmounts);
|
if (Array.isArray(data?.otherAmountList)) source.push(...data.otherAmountList);
|
if (Array.isArray(data?.otherAmountProjects)) source.push(...data.otherAmountProjects);
|
if (Array.isArray(data?.salesProductProcessList)) source.push(...data.salesProductProcessList);
|
(Array.isArray(items) ? items : []).forEach((item) => {
|
if (Array.isArray(item?.salesProductProcessList)) source.push(...item.salesProductProcessList);
|
if (Array.isArray(item?.otherAmounts)) source.push(...item.otherAmounts);
|
});
|
|
const map = new Map();
|
source.forEach((fee) => {
|
const name = String(fee?.feeName || fee?.processName || fee?.name || fee?.itemName || "").trim();
|
if (!name) return;
|
const quantity = toNumber(fee?.quantity || fee?.num);
|
const unitPrice = toNumber(fee?.unitPrice || fee?.price);
|
const amount =
|
toNumber(fee?.amount || fee?.totalPrice) || (quantity && unitPrice ? quantity * unitPrice : 0);
|
const key = name;
|
if (!map.has(key)) {
|
map.set(key, { name, quantity: 0, unitPrice: 0, amount: 0 });
|
}
|
const row = map.get(key);
|
row.quantity += quantity;
|
row.unitPrice = unitPrice || row.unitPrice;
|
row.amount += amount;
|
});
|
return Array.from(map.values());
|
};
|
|
const renderOtherFeeRows = (rows) => {
|
const list = Array.isArray(rows) ? rows : [];
|
if (list.length === 0) {
|
return `<tr><td></td><td></td><td></td><td></td></tr>`;
|
}
|
return list
|
.map((row) => {
|
const name = escapeHtml(row.name);
|
const unitPrice = row.unitPrice ? formatMoney(row.unitPrice) : "";
|
const quantity = row.quantity ? String(row.quantity) : "";
|
const amount = row.amount ? formatMoney(row.amount) : "";
|
return `<tr><td class="other-fee-shift-left">${name}</td><td class="other-fee-shift-left">${unitPrice}</td><td class="other-fee-shift-left">${quantity}</td><td>${amount}</td></tr>`;
|
})
|
.join("");
|
};
|
|
const splitItemsByPage = (items, pageSize) => {
|
const list = Array.isArray(items) ? items : [];
|
if (list.length === 0) return [[]];
|
const pages = [];
|
for (let i = 0; i < list.length; i += pageSize) {
|
pages.push(list.slice(i, i + pageSize));
|
}
|
return pages;
|
};
|
|
const renderRows = (items, startIndex) => {
|
const list = Array.isArray(items) ? items : [];
|
if (list.length === 0) {
|
return `
|
<tr>
|
<td colspan="8" class="empty">暂无明细</td>
|
</tr>
|
`;
|
}
|
return list
|
.map((item, idx) => {
|
const width = escapeHtml(item?.width);
|
const height = escapeHtml(item?.height);
|
const sizeText = width || height ? `${width}*${height}` : "";
|
const unitPrice = formatMoney(item?.taxInclusiveUnitPrice || item?.unitPrice);
|
const amount = formatMoney(getItemAmount(item));
|
return `
|
<tr>
|
<td>${startIndex + idx + 1}</td>
|
<td>${escapeHtml(item?.floorCode)}</td>
|
<td>${sizeText}</td>
|
<td>${formatQty(item?.quantity)}</td>
|
<td>${formatArea(item?.area || item?.settleTotalArea || item?.actualTotalArea)}</td>
|
<td>${unitPrice}</td>
|
<td>${amount}</td>
|
<td>${escapeHtml(item?.processRequirement)}</td>
|
</tr>
|
`;
|
})
|
.join("");
|
};
|
|
export const printSalesOrder = (orderData) => {
|
const data = orderData ?? {};
|
const items = Array.isArray(data.items) ? data.items : [];
|
const pageSize = 15;
|
const pages = splitItemsByPage(items, pageSize);
|
const totalPages = pages.length;
|
|
const subtotalQuantity = toNumber(data.subtotalQuantity);
|
const subtotalArea = toNumber(data.subtotalArea);
|
const subtotalAmount = toNumber(data.subtotalAmount);
|
const totalQuantity = toNumber(data.totalQuantity) || items.reduce((sum, item) => sum + toNumber(item?.quantity), 0);
|
const totalArea = toNumber(data.totalArea) || items.reduce((sum, item) => sum + getItemArea(item), 0);
|
const totalAmount = toNumber(data.totalAmount) || items.reduce((sum, item) => sum + getItemAmount(item), 0);
|
const otherFees = extractOtherFees(data, items);
|
|
const printWindow = window.open("", "_blank", "width=1200,height=900");
|
if (!printWindow) {
|
throw new Error("浏览器拦截了弹窗,请允许弹窗后重试");
|
}
|
|
const html = `
|
<!DOCTYPE html>
|
<html>
|
<head>
|
<meta charset="UTF-8" />
|
<title>${PRINT_TITLE}</title>
|
<style>
|
body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #111; }
|
.page {
|
width: 198mm;
|
min-height: 186mm;
|
margin: 0 auto;
|
padding: 5mm 4mm 16mm;
|
box-sizing: border-box;
|
page-break-after: always;
|
position: relative;
|
}
|
.page:last-child { page-break-after: auto; }
|
.title-main { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 1px; line-height: 1.15; margin-top: 1mm; }
|
.title-sub { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 6px; line-height: 1.15; margin: 1mm 0 4mm; }
|
table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; }
|
.detail-table {
|
border-right: 1px solid #222 !important;
|
}
|
.detail-table-wrap {
|
position: relative;
|
margin-top: -1px;
|
}
|
.detail-table-wrap::after {
|
content: none;
|
}
|
.sheet-wrap {
|
position: relative;
|
overflow: visible;
|
}
|
.sheet-wrap::after {
|
content: none;
|
}
|
td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; }
|
tr > td:first-child, tr > th:first-child { border-left: 1px solid #222 !important; }
|
tr > td:last-child, tr > th:last-child { border-right: 1px solid #222 !important; }
|
.left { text-align: left; }
|
.bold { font-weight: 700; }
|
.cell-title { font-weight: 700; white-space: nowrap; }
|
.product-row td { font-size: 13px; font-weight: 700; }
|
.empty { height: 140px; color: #777; }
|
.large-row td { height: 46px; vertical-align: top; }
|
.other-fee-content-row td { height: auto !important; }
|
.total-row td { font-weight: 700; font-size: 13px; line-height: 1.2; }
|
.footer { margin-top: 5px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; font-size: 13px; line-height: 1.7; }
|
.footer b { display: inline-block; min-width: 74px; }
|
.customer-sign-row {
|
margin-top: 6px;
|
display: grid;
|
grid-template-columns: 1fr 1fr 1fr;
|
}
|
.customer-sign-cell { text-align: left; }
|
.customer-sign { font-size: 13px; font-weight: 700; line-height: 1.2; }
|
.right { text-align: right; }
|
.footer-page-right { display: block; text-align: right; font-size: 12px; }
|
.footer-pair {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
}
|
.other-fee-cell { white-space: normal; word-break: break-all; line-height: 1.35; vertical-align: top; }
|
.other-fee-header { font-weight: 700; }
|
.other-fee-header-grid {
|
display: grid;
|
grid-template-columns: repeat(4, 1fr);
|
align-items: center;
|
text-align: center;
|
}
|
.other-fee-header-row td { border-bottom: none !important; }
|
.other-fee-content-row td { border-top: none !important; }
|
.other-fee-inner {
|
width: 100%;
|
border-collapse: collapse;
|
table-layout: fixed;
|
border: none !important;
|
}
|
.other-fee-inner td {
|
border: none !important;
|
padding: 0 1px;
|
text-align: center;
|
vertical-align: middle;
|
white-space: normal;
|
word-break: break-all;
|
line-height: 1.1;
|
}
|
.other-fee-inner tr > td:first-child,
|
.other-fee-inner tr > td:last-child {
|
border-left: none !important;
|
border-right: none !important;
|
}
|
.other-fee-shift-left {
|
position: relative;
|
left: -30px;
|
}
|
@media print {
|
@page { size: A4 landscape; margin: 6mm; }
|
.page { width: 100%; margin: 0; padding: 0 0 14mm; min-height: 0; }
|
}
|
</style>
|
</head>
|
<body>
|
${pages
|
.map((pageItems, pageIndex) => {
|
const isLastPage = pageIndex === totalPages - 1;
|
const startIndex = pageIndex * pageSize;
|
return `
|
<div class="page">
|
<div class="title-main">${escapeHtml(data.companyName || "鹤壁天沐钢化玻璃厂")}</div>
|
<div class="title-sub">销售订单</div>
|
|
<div class="sheet-wrap">
|
<table>
|
<colgroup>
|
<col style="width: 14%;" />
|
<col style="width: 20%;" />
|
<col style="width: 20%;" />
|
<col style="width: 14%;" />
|
<col style="width: 32%;" />
|
</colgroup>
|
<tr>
|
<td class="cell-title">客户名称:</td>
|
<td class="left">${escapeHtml(data.customerName)}</td>
|
<td class="cell-title">项目名称:</td>
|
<td class="left">${escapeHtml(data.projectName)}</td>
|
<td class="left"><span class="cell-title">业 务 员:</span> ${escapeHtml(data.salesman)}</td>
|
</tr>
|
<tr>
|
<td class="cell-title">制单日期:</td>
|
<td class="left">${escapeHtml(formatDisplayDate(data.executionDate || data.orderMakerDate || data.registerDate || data.entryDate))}</td>
|
<td class="cell-title">交货日期:</td>
|
<td class="left">${escapeHtml(formatDisplayDate(data.deliveryDate))}</td>
|
<td class="left"></td>
|
</tr>
|
<tr>
|
<td class="cell-title">送货地址:</td>
|
<td colspan="4" class="left">${escapeHtml(getDeliveryAddress(data))}</td>
|
</tr>
|
</table>
|
|
<div class="detail-table-wrap">
|
<table class="detail-table">
|
<colgroup>
|
<col style="width: 9%;" />
|
<col style="width: 12%;" />
|
<col style="width: 12%;" />
|
<col style="width: 7%;" />
|
<col style="width: 10%;" />
|
<col style="width: 8%;" />
|
<col style="width: 10%;" />
|
<col style="width: 32%;" />
|
</colgroup>
|
<tr>
|
<th>序号</th>
|
<th>楼层编号</th>
|
<th>宽(弧长)*高</th>
|
<th>数量</th>
|
<th>结算面积</th>
|
<th>单价</th>
|
<th>金额</th>
|
<th>加工要求</th>
|
</tr>
|
<tr class="product-row">
|
<td colspan="6" class="left">产品名称: ${escapeHtml(data.productName || items[0]?.productDescription)}</td>
|
<td colspan="2" class="left">订单编号: ${escapeHtml(data.salesContractNo)}</td>
|
</tr>
|
${renderRows(pageItems, startIndex)}
|
${
|
isLastPage
|
? `
|
<tr class="total-row">
|
<td colspan="3" class="left">小计:</td>
|
<td>${subtotalQuantity || totalQuantity || ""}</td>
|
<td>${subtotalArea ? subtotalArea.toFixed(2) : totalArea ? totalArea.toFixed(2) : ""}</td>
|
<td></td>
|
<td>${formatMoney(subtotalAmount || totalAmount)}</td>
|
<td></td>
|
</tr>
|
<tr class="total-row">
|
<td colspan="3" class="left">合计:</td>
|
<td>${totalQuantity || ""}</td>
|
<td>${totalArea ? totalArea.toFixed(2) : ""}</td>
|
<td></td>
|
<td>${formatMoney(totalAmount)}</td>
|
<td></td>
|
</tr>
|
<tr class="other-fee-header-row">
|
<td colspan="5" class="left other-fee-header">
|
<div class="other-fee-header-grid">
|
<span>其他费用</span>
|
<span>单价</span>
|
<span>数量</span>
|
<span>金额</span>
|
</div>
|
</td>
|
<td colspan="3" class="left other-fee-header">加工要求和备注:</td>
|
</tr>
|
<tr class="large-row other-fee-content-row">
|
<td colspan="5" class="left other-fee-cell">
|
<table class="other-fee-inner">
|
<colgroup>
|
<col style="width: 34%;" />
|
<col style="width: 22%;" />
|
<col style="width: 20%;" />
|
<col style="width: 24%;" />
|
</colgroup>
|
${renderOtherFeeRows(otherFees)}
|
</table>
|
</td>
|
<td colspan="3" class="left other-fee-cell">${escapeHtml(normalizeRequirementText(data.remakes || data.remarks || data.orderProcessRequirement))}</td>
|
</tr>
|
<tr class="total-row">
|
<td colspan="8" class="left">总金额: ${escapeHtml(data.totalAmountDisplay || `${formatMoney(totalAmount)}元`)}</td>
|
</tr>
|
`
|
: `
|
<tr><td colspan="8" class="right">下页续...</td></tr>
|
`
|
}
|
</table>
|
</div>
|
</div>
|
|
${
|
isLastPage
|
? `
|
<div class="customer-sign-row">
|
<span></span>
|
<span></span>
|
<span class="customer-sign customer-sign-cell">客户签名:</span>
|
</div>
|
<div class="footer">
|
<div><b>制单员:</b>${escapeHtml(data.orderMaker || data.register)}</div>
|
<div><b>审核员:</b>${escapeHtml(data.auditor)}</div>
|
<div><b>打印人:</b>${escapeHtml(data.printPeople || data.register)}</div>
|
<div><b>制单日期:</b>${escapeHtml(formatDisplayDate(data.orderMakerDate || data.executionDate || data.registerDate || data.entryDate))}</div>
|
<div><b>审核日期:</b>${escapeHtml(formatDisplayDate(data.auditDate))}</div>
|
<div class="footer-pair"><span><b>打印时间:</b>${escapeHtml(data.printTime || getCurrentDateTime())}</span><span class="footer-page-right">第${pageIndex + 1}页,共${totalPages}页</span></div>
|
</div>
|
`
|
: ""
|
}
|
</div>`;
|
})
|
.join("")}
|
</body>
|
</html>
|
`;
|
|
printWindow.document.write(html);
|
printWindow.document.close();
|
printWindow.onload = () => {
|
setTimeout(() => {
|
printWindow.focus();
|
printWindow.print();
|
printWindow.close();
|
}, 300);
|
};
|
};
|