yyb
5 天以前 f541524cb8f311ac0b7c8771621d361d31cd24bb
销售发货单
已添加1个文件
已修改2个文件
348 ■■■■■ 文件已修改
src/api/salesManagement/salesLedger.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js 276 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesLedger.js
@@ -177,3 +177,14 @@
    method: "get",
  });
}
// æ‰“印销售发货单
export function getSalesInvoices(query) {
  const data =
    query && typeof query === "object" ? query : { salesLedgerId: query };
  return request({
    url: "/sales/ledger/salesInvoices",
    method: "post",
    data,
  });
}
src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,276 @@
const PRINT_TITLE = "销售发货单";
const escapeHtml = (value) =>
  String(value ?? "")
    .replaceAll("&", "&")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
const toNumber = (value) => {
  const num = Number(value);
  return Number.isFinite(num) ? num : 0;
};
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 getItemArea = (item) => toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea);
const getOrderNo = (data, row, item) =>
  item?.salesContractNo || item?.orderNo || data?.salesContractNo || row?.salesContractNo || "";
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 normalizeInvoiceData = (raw, selectedRow) => {
  const data = raw ?? {};
  const groups = Array.isArray(data.groups) ? data.groups : [];
  if (!groups.length) return data;
  const items = groups.flatMap((group) =>
    (Array.isArray(group?.items) ? group.items : []).map((item) => ({
      ...item,
      productDescription: group?.productName || item?.productDescription || "",
      salesContractNo: group?.salesContractNo || item?.salesContractNo || "",
      widthHeight: item?.widthHeight || "",
    }))
  );
  return {
    ...data,
    items,
    customerName: data.customerName || selectedRow?.customerName || "",
    contactPerson: data.contactPerson || selectedRow?.contactPerson || "",
    contactPhone: data.contactPhone || selectedRow?.contactPhone || "",
    deliveryAddress:
      data.companyAddress || data.deliveryAddress || data.shippingAddress || selectedRow?.deliveryAddress || "",
    shipmentNo: data.externalOrderNo || data.shipmentNo || "",
    register: data.orderMaker || data.register || selectedRow?.entryPersonName || "",
    registerDate: data.executionDate || data.registerDate || data.entryDate || selectedRow?.entryDate || "",
  };
};
const groupByProduct = (items, data, row) => {
  const list = Array.isArray(items) ? items : [];
  const map = new Map();
  list.forEach((item) => {
    const key = `${item?.productDescription || ""}__${getOrderNo(data, row, item)}`;
    if (!map.has(key)) {
      map.set(key, {
        productName: item?.productDescription || "",
        orderNo: getOrderNo(data, row, item),
        items: [],
      });
    }
    map.get(key).items.push(item);
  });
  return Array.from(map.values());
};
const renderItemRows = (items, startIndex) =>
  items
    .map((item, idx) => {
      const sizeText = item?.widthHeight
        ? escapeHtml(item.widthHeight)
        : item?.width || item?.height
          ? `${escapeHtml(item?.width)} * ${escapeHtml(item?.height)}`
          : "";
      return `
      <tr>
        <td>${startIndex + idx + 1}</td>
        <td class="left">${escapeHtml(item?.floorCode)}</td>
        <td>${sizeText}</td>
        <td>${toNumber(item?.quantity) || ""}</td>
        <td>${getItemArea(item) ? getItemArea(item).toFixed(2) : ""}</td>
        <td class="left">${escapeHtml(item?.remark)}</td>
        <td class="left">${escapeHtml(item?.processRequirement)}</td>
      </tr>
    `;
    })
    .join("");
export const printSalesDeliveryNote = (rawData, selectedRow = {}) => {
  const data = normalizeInvoiceData(rawData, selectedRow);
  const allItems = Array.isArray(data.items) ? data.items : [];
  const pageSize = 18;
  const itemPages = splitItemsByPage(allItems, pageSize);
  const totalPages = itemPages.length;
  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: #222; }
      .page { width: 198mm; margin: 0 auto; padding: 4mm 4mm 6mm; box-sizing: border-box; page-break-after: always; }
      .page:last-child { page-break-after: auto; }
      .head-top {
        display: grid;
        grid-template-columns: 1fr auto 1fr;
        align-items: end;
        margin-bottom: 1px;
      }
      .factory {
        grid-column: 2;
        text-align: center;
        font-size: 20px;
        font-weight: 700;
        line-height: 1.2;
      }
      .page-mark {
        grid-column: 3;
        justify-self: end;
        font-size: 12px;
        margin-right: 8mm;
        margin-bottom: 1px;
      }
      .head-mid {
        display: grid;
        grid-template-columns: 1fr auto 1fr;
        align-items: center;
        margin-bottom: 2px;
      }
      .head-mid-left { font-size: 13px; text-align: left; }
      .head-mid-title { font-size: 20px; font-weight: 700; text-align: center; }
      .head-mid-right { font-size: 13px; text-align: right; padding-right: 8mm; }
      table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; }
      td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; }
      .left { text-align: left; }
      .group-title td { font-weight: 700; }
      .subtotal td, .total-row td { font-weight: 700; }
      .empty td { height: 120px; color: #666; }
      .footer { margin-top: 6px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; font-size: 13px; }
      @media print {
        @page { size: A4 portrait; margin: 8mm; }
        .page { width: 100%; margin: 0; padding: 0; }
      }
    </style>
  </head>
  <body>
  ${itemPages
    .map((pageItems, pageIndex) => {
      const pageGroups = groupByProduct(pageItems, data, selectedRow);
      let serial = pageIndex * pageSize;
      const totalQty = toNumber(data.totalQuantity) || allItems.reduce((s, it) => s + toNumber(it?.quantity), 0);
      const totalArea = toNumber(data.totalArea) || allItems.reduce((s, it) => s + getItemArea(it), 0);
      return `
    <div class="page">
      <div class="head-top">
        <div></div>
        <div class="factory">鹤壁天沐钢化玻璃厂</div>
        <div class="page-mark">第${pageIndex + 1}页,共${totalPages}页</div>
      </div>
      <div class="head-mid">
        <div class="head-mid-left">对方单号: ${escapeHtml(data.deliveryNo || data.shippingNo || selectedRow.expressNumber || "")}</div>
        <div class="head-mid-title">销售发货单</div>
        <div class="head-mid-right">发货单号: ${escapeHtml(data.shipmentNo || data.deliveryNo || "")}</div>
      </div>
      <table>
        <tr>
          <td class="left" colspan="4">客户名称: ${escapeHtml(data.customerName || selectedRow.customerName || "")}</td>
          <td class="left" colspan="3">联系人: ${escapeHtml(data.contactPerson || selectedRow.contactPerson || "")}</td>
        </tr>
        <tr>
          <td class="left" colspan="4">发货地址: ${escapeHtml(data.deliveryAddress || data.shippingAddress || selectedRow.deliveryAddress || "")}</td>
          <td class="left" colspan="3">联系电话: ${escapeHtml(data.contactPhone || selectedRow.contactPhone || "")}</td>
        </tr>
        <tr>
          <th style="width:8%;">序号</th>
          <th style="width:22%;">楼层编号</th>
          <th style="width:20%;">宽(弧长)*高</th>
          <th style="width:10%;">数量</th>
          <th style="width:12%;">面积</th>
          <th style="width:10%;">备注</th>
          <th style="width:18%;">加工要求</th>
        </tr>
        ${
          pageGroups.length
            ? pageGroups
                .map((group) => {
                  const subQty = group.items.reduce((s, it) => s + toNumber(it?.quantity), 0);
                  const subArea = group.items.reduce((s, it) => s + getItemArea(it), 0);
                  const rows = renderItemRows(group.items, serial);
                  serial += group.items.length;
                  return `
          <tr class="group-title">
            <td colspan="5" class="left">产品名称: ${escapeHtml(group.productName)}</td>
            <td colspan="2" class="left">订单编号: ${escapeHtml(group.orderNo)}</td>
          </tr>
          ${rows}
          <tr class="subtotal">
            <td colspan="3">小计:</td>
            <td>${subQty || ""}</td>
            <td>${subArea ? subArea.toFixed(2) : ""}</td>
            <td colspan="2"></td>
          </tr>
                  `;
                })
                .join("")
            : `<tr class="empty"><td colspan="7">暂无明细</td></tr>`
        }
        ${
          pageIndex === totalPages - 1
            ? `
        <tr class="total-row">
          <td colspan="3">合计:</td>
          <td>${totalQty || ""}</td>
          <td>${totalArea ? totalArea.toFixed(2) : ""}</td>
          <td colspan="2"></td>
        </tr>
            `
            : ""
        }
      </table>
      ${
        pageIndex === totalPages - 1
          ? `
      <div class="footer">
        <div>制 å• å‘˜: ${escapeHtml(data.register || selectedRow.entryPersonName || "")}</div>
        <div>制单日期: ${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate || selectedRow.entryDate))}</div>
        <div>客户签字:</div>
        <div>签收日期:</div>
      </div>
          `
          : ""
      }
    </div>
      `;
    })
    .join("")}
  </body>
</html>
`;
  printWindow.document.write(html);
  printWindow.document.close();
  printWindow.onload = () => {
    setTimeout(() => {
      printWindow.focus();
      printWindow.print();
      printWindow.close();
    }, 300);
  };
};
src/views/salesManagement/salesLedger/index.vue
@@ -53,6 +53,7 @@
              <el-dropdown-menu>
                <el-dropdown-item command="finishedProcessCard">生产流程卡(成品)</el-dropdown-item>
                <el-dropdown-item command="salesOrder">销售订单</el-dropdown-item>
                <el-dropdown-item command="salesDeliveryNote">销售发货单</el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
@@ -891,6 +892,7 @@
    saleProcessBind,
    getSaleProcessBindInfo,
    getProcessCard,
    getSalesInvoices,
} from "@/api/salesManagement/salesLedger.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
import useFormData from "@/hooks/useFormData.js";
@@ -898,6 +900,7 @@
import { getCurrentDate } from "@/utils/index.js";
import { printFinishedProcessCard } from "./components/processCardPrint.js";
import { printSalesOrder } from "./components/salesOrderPrint.js";
import { printSalesDeliveryNote } from "./components/salesDeliveryPrint.js";
// import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js";
const userStore = useUserStore();
@@ -2025,14 +2028,53 @@
};
const handlePrintCommand = async (command) => {
    if (command !== "finishedProcessCard" && command !== "salesOrder") return;
    if (selectedRows.value.length !== 1) {
    if (command !== "finishedProcessCard" && command !== "salesOrder" && command !== "salesDeliveryNote") return;
    if (command === "salesDeliveryNote") {
        if (selectedRows.value.length === 0) {
            proxy.$modal.msgWarning("请至少选择一条销售台账数据进行打印");
            return;
        }
        const customerNames = Array.from(
            new Set(selectedRows.value.map((item) => String(item?.customerName ?? "").trim()))
        );
        if (customerNames.length > 1) {
            proxy.$modal.msgWarning("仅支持相同客户名称的销售台账合并发货打印");
            return;
        }
    } else if (selectedRows.value.length !== 1) {
        proxy.$modal.msgWarning("请选择一条销售台账数据进行打印");
        return;
    }
    const selectedRow = selectedRows.value[0];
    const selectedId = selectedRow?.id;
    if (command === "salesDeliveryNote") {
        const selectedIds = selectedRows.value
            .map((item) => item?.id)
            .filter((id) => id !== null && id !== undefined && id !== "");
        if (selectedIds.length !== selectedRows.value.length) {
            proxy.$modal.msgWarning("当前选择数据存在缺少ID的记录,无法打印");
            return;
        }
        const loadingText =
            command === "salesOrder"
                ? "正在获取销售订单数据,请稍候..."
                : command === "salesDeliveryNote"
                    ? "正在获取销售发货单数据,请稍候..."
                    : "正在获取生产流程卡数据,请稍候...";
        proxy.$modal.loading(loadingText);
        try {
            const res = await getSalesInvoices(selectedIds);
            const salesInvoiceData = res?.data ?? {};
            printSalesDeliveryNote(salesInvoiceData, selectedRow);
        } catch (error) {
            console.error("打印销售发货单失败:", error);
            proxy.$modal.msgError("打印失败,请稍后重试");
        } finally {
            proxy.$modal.closeLoading();
        }
        return;
    }
    if (!selectedId) {
        proxy.$modal.msgWarning("当前选择数据缺少ID,无法打印");
        return;
@@ -2041,18 +2083,29 @@
    const loadingText =
        command === "salesOrder"
            ? "正在获取销售订单数据,请稍候..."
            : command === "salesDeliveryNote"
                ? "正在获取销售发货单数据,请稍候..."
            : "正在获取生产流程卡数据,请稍候...";
    proxy.$modal.loading(loadingText);
    try {
        if (command === "salesOrder") {
        const res = await getProcessCard(selectedId);
        const processCardData = res?.data ?? {};
        if (command === "salesOrder") {
            printSalesOrder(processCardData);
        } else {
            const res = await getProcessCard(selectedId);
            const processCardData = res?.data ?? {};
            printFinishedProcessCard(processCardData);
        }
    } catch (error) {
        console.error(command === "salesOrder" ? "打印销售订单失败:" : "打印生产流程卡失败:", error);
        console.error(
            command === "salesOrder"
                ? "打印销售订单失败:"
                : command === "salesDeliveryNote"
                    ? "打印销售发货单失败:"
                    : "打印生产流程卡失败:",
            error
        );
        proxy.$modal.msgError("打印失败,请稍后重试");
    } finally {
        proxy.$modal.closeLoading();