|
- MOB(手机):
+
+ MOB(手机):
+
@@ -177,7 +193,7 @@ export function GujiaQuotationTemplateSheet({
|
- MOB(手机):
+ MOB(手机):
|
|
|
- 序号
+ 序号
|
- 产品货号
+ 品牌货号
|
- 规格
+ 产品货号
|
- 图片
+ 规格
|
- 采购价(含税)
+ 图片
|
- 售价
+ 采购价(含税)
|
- 材质
+ 售价
|
- 规格(CM)
+ 材质
|
- 起订量
+ 规格(CM)
+ |
+
+ 起订量
|
- 采购周期{"\n"}(天)
+
+ 采购周期{"\n"}(天)
+
|
- 包装件数
+ 包装件数
|
- 包装方式
+ 包装方式
|
备注
@@ -245,6 +266,18 @@ export function GujiaQuotationTemplateSheet({
{row.sequenceNumber}
|
) : null}
+
+
+ onLineItemChange(
+ row.lineId,
+ "brandItemCode",
+ event.target.value,
+ )
+ }
+ />
+ |
|
@@ -381,14 +414,14 @@ export function GujiaQuotationTemplateSheet({
|
|
- |
+ |
|
基地名称
|
仓库详细地址
@@ -399,7 +432,7 @@ export function GujiaQuotationTemplateSheet({
电商办公仓
|
湖州市安吉县阳光二区北庄路曼龙工业链园区3号厂房3楼
diff --git a/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningLineItems.ts b/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningLineItems.ts
index 89f3e264..7b056cb2 100644
--- a/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningLineItems.ts
+++ b/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningLineItems.ts
@@ -94,6 +94,7 @@ export function buildBrandFilingPlanningLineItems(params: {
return normalizeBrandFilingPlanningLineItem({
...createEmptyBrandFilingPlanningLineItem(),
+ brandItemCode: pickNonEmptyText(matchedItem?.brandItemCode),
itemCode: pickNonEmptyText(matchedItem?.itemCode, row.itemCode),
sizeRequirement: pickNonEmptyText(
matchedItem?.sizeRequirement,
@@ -158,6 +159,7 @@ export function buildBrandFilingPlanningDisplayRows(params: {
const normalizedItem = normalizeBrandFilingPlanningLineItem({
...createEmptyBrandFilingPlanningLineItem(),
...matchedItem,
+ brandItemCode: pickNonEmptyText(matchedItem?.brandItemCode),
itemCode: pickNonEmptyText(matchedItem?.itemCode, row.itemCode),
sizeRequirement: pickNonEmptyText(
matchedItem?.sizeRequirement,
diff --git a/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningTableSchema.ts b/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningTableSchema.ts
index 5dcc210c..382f875f 100644
--- a/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningTableSchema.ts
+++ b/furniture-3d-system/src/app/intake/brand-filing/brandFilingPlanningTableSchema.ts
@@ -125,11 +125,11 @@ export const BRAND_FILING_PLANNING_ROW_LAYOUT = {
plan: {
dataStartRow: 11,
mergedRanges: [
- ["B", "C"],
- ["D", "E"],
- ["F", "G"],
- ["H", "I"],
- ["J", "L"],
+ ["C", "D"],
+ ["E", "F"],
+ ["G", "H"],
+ ["I", "J"],
+ ["K", "L"],
] as const,
},
promotion: {
@@ -138,7 +138,6 @@ export const BRAND_FILING_PLANNING_ROW_LAYOUT = {
headerBottomRow: 14,
dataStartRow: 15,
mergedRanges: [
- ["I", "J"],
["K", "L"],
] as const,
},
diff --git a/furniture-3d-system/src/app/intake/brand-filing/brandFilingProductData.ts b/furniture-3d-system/src/app/intake/brand-filing/brandFilingProductData.ts
index bb9c4ff2..78c7ccf4 100644
--- a/furniture-3d-system/src/app/intake/brand-filing/brandFilingProductData.ts
+++ b/furniture-3d-system/src/app/intake/brand-filing/brandFilingProductData.ts
@@ -20,7 +20,10 @@ import {
getBrandFilingMatchedSkuItemsByItemCode,
resolveBrandFilingPlanningItemCode,
} from "@/app/intake/brand-filing/brandFilingPlanningSkuRows";
-import { resolveBrandFilingSequenceNumber } from "@/app/intake/brand-filing/brandFilingSequence";
+import {
+ getBrandFilingBasicInfoApprovedAt,
+ resolveBrandFilingSequenceNumber,
+} from "@/app/intake/brand-filing/brandFilingSequence";
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
import { getNonIntegratedListPeople } from "@/app/intake/product-development/nonIntegratedProductListLogic";
@@ -271,17 +274,25 @@ export function upsertBrandFilingSequenceNumber(params: {
if (!normalizedSequenceNumber) {
return product;
}
- if (product.brandFilingData?.[brand]?.sequenceNumber) {
+
+ const currentBrandData = product.brandFilingData?.[brand];
+ if (currentBrandData?.sequenceNumber && currentBrandData.enteredAt) {
return product;
}
+ const enteredAt =
+ currentBrandData?.enteredAt ||
+ getBrandFilingBasicInfoApprovedAt(product) ||
+ new Date().toISOString();
return {
...product,
brandFilingData: {
...(product.brandFilingData ?? {}),
[brand]: {
- ...(product.brandFilingData?.[brand] ?? {}),
- sequenceNumber: normalizedSequenceNumber,
+ ...(currentBrandData ?? {}),
+ sequenceNumber:
+ currentBrandData?.sequenceNumber ?? normalizedSequenceNumber,
+ enteredAt,
},
},
};
diff --git a/furniture-3d-system/src/app/intake/brand-filing/brandFilingReportStatus.ts b/furniture-3d-system/src/app/intake/brand-filing/brandFilingReportStatus.ts
new file mode 100644
index 00000000..c70f3982
--- /dev/null
+++ b/furniture-3d-system/src/app/intake/brand-filing/brandFilingReportStatus.ts
@@ -0,0 +1,21 @@
+import type { BrandFilingBrand } from "@/app/intake/brand-filing/brandFilingConfig";
+import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
+
+export function markBrandFilingReported(params: {
+ product: NonIntegratedProductRecord;
+ brand: BrandFilingBrand;
+ reportedAt?: string;
+}) {
+ const reportedAt = params.reportedAt ?? new Date().toISOString();
+
+ return {
+ ...params.product,
+ brandFilingData: {
+ ...(params.product.brandFilingData ?? {}),
+ [params.brand]: {
+ ...(params.product.brandFilingData?.[params.brand] ?? {}),
+ reportedAt,
+ },
+ },
+ } satisfies NonIntegratedProductRecord;
+}
diff --git a/furniture-3d-system/src/app/intake/brand-filing/brandFilingRequiredValidation.ts b/furniture-3d-system/src/app/intake/brand-filing/brandFilingRequiredValidation.ts
new file mode 100644
index 00000000..056d2b41
--- /dev/null
+++ b/furniture-3d-system/src/app/intake/brand-filing/brandFilingRequiredValidation.ts
@@ -0,0 +1,179 @@
+import type {
+ BrandFilingPlanningFieldKey,
+ BrandFilingPlanningForm,
+ BrandFilingPlanningLineItem,
+ BrandFilingPlanningLineItemFieldKey,
+ GujiaProductInfoConfirmationLineItem,
+ GujiaQuotationHeaderFieldKey,
+ GujiaQuotationHeaderForm,
+ GujiaQuotationLineItem,
+} from "@/app/intake/brand-filing/brandFilingTypes";
+
+function hasText(value: string | null | undefined) {
+ return Boolean(value?.trim());
+}
+
+function pushMissingField(
+ missingFields: string[],
+ value: string | null | undefined,
+ label: string,
+) {
+ if (!hasText(value)) {
+ missingFields.push(label);
+ }
+}
+
+const PLANNING_REQUIRED_FORM_FIELDS = [
+ ["planningProductLine", "企划产品线"],
+ ["productLine", "产品线"],
+ ["annualPlanStatus", "是否年度规划内"],
+ ["developmentMode", "开发方式"],
+ ["productSeries", "产品系列"],
+ ["developmentCategory", "开发类别"],
+ ["supplierName", "买采供应商名称"],
+ ["warehousingStatus", "是否入仓"],
+ ["launchDate", "需求上市时间"],
+ ["activityStyleStatus", "是否活动款"],
+ ["imageUrl", "图片"],
+ ["productDemandAnalysis", "产品需求分析"],
+ ["legacyIterationNote", "特殊说明(是否老款迭代)"],
+ ["productManager", "产品经理"],
+ ["planningOwner", "企划负责人"],
+ ["purchaseOwner", "采购负责人"],
+] as const satisfies readonly [BrandFilingPlanningFieldKey, string][];
+
+const PLANNING_REQUIRED_LINE_FIELDS = [
+ ["itemCode", "货号"],
+ ["retailPriceDemand", "零售价需求"],
+ ["purchasePriceDemand", "采购价需求"],
+ ["sizeRequirement", "尺寸要求"],
+ ["materialAndColor", "材质/颜色"],
+ ["seriesName", "系列"],
+ ["channel", "渠道"],
+ ["month1Sales", "第1个月"],
+ ["month2Sales", "第2个月"],
+ ["month3Sales", "第3个月"],
+ ["month4To6Sales", "第4-6月"],
+ ["month7To9Sales", "第7-9月"],
+ ["month10To12Sales", "第10-12月"],
+] as const satisfies readonly [BrandFilingPlanningLineItemFieldKey, string][];
+
+const QUOTATION_REQUIRED_HEADER_FIELDS = [
+ ["contactName", "编号"],
+ ["demandCompany", "TO(需求方)"],
+ ["demandMobile", "需求方手机"],
+ ["supplyCompany", "FROM(供应方)"],
+ ["supplyMobile", "供应方手机"],
+] as const satisfies readonly [GujiaQuotationHeaderFieldKey, string][];
+
+const QUOTATION_REQUIRED_LINE_FIELDS = [
+ ["sequenceNumber", "序号"],
+ ["itemCode", "产品货号"],
+ ["specification", "规格"],
+ ["imageUrl", "图片"],
+ ["purchasePrice", "采购价(含税)"],
+ ["salesPrice", "售价"],
+ ["material", "材质"],
+ ["sizeCm", "规格(CM)"],
+ ["minimumOrderQuantity", "起订量"],
+ ["leadTimeDays", "采购周期(天)"],
+ ["packageCount", "包装件数"],
+ ["packageMethod", "包装方式"],
+] as const satisfies readonly [keyof GujiaQuotationLineItem, string][];
+
+const PRODUCT_INFO_REQUIRED_LINE_FIELDS = [
+ ["sequenceNumber", "序号"],
+ ["productImageUrl", "产品图片"],
+ ["productLine", "产品线"],
+ ["itemCode", "货号"],
+ ["productName", "品名"],
+ ["sizeCm", "规格"],
+ ["certificateMaterial", "合格证的主材/基材"],
+ ["accessoryMaterial", "辅材"],
+ ["packageCount", "包装件数"],
+ ["packageSizeCm", "外包装尺寸"],
+ ["packageVolumeM3", "外包装体积"],
+ ["grossWeightKg", "包装毛重"],
+ ["netWeightKg", "产品净重"],
+ ["assemblyMethod", "拆装方式"],
+ ["manufacturer", "合格证上的制造商"],
+ ["colorBoardCode", "色板编号"],
+] as const satisfies readonly [
+ keyof GujiaProductInfoConfirmationLineItem,
+ string,
+][];
+
+export function validateBrandFilingPlanningRequiredFields(
+ form: BrandFilingPlanningForm,
+ lineItems: BrandFilingPlanningLineItem[],
+) {
+ const missingFields: string[] = [];
+
+ PLANNING_REQUIRED_FORM_FIELDS.forEach(([key, label]) => {
+ pushMissingField(missingFields, form[key], label);
+ });
+
+ if (lineItems.length === 0) {
+ missingFields.push("企划方案明细");
+ }
+
+ lineItems.forEach((lineItem, index) => {
+ PLANNING_REQUIRED_LINE_FIELDS.forEach(([key, label]) => {
+ pushMissingField(missingFields, lineItem[key], `第 ${index + 1} 行 ${label}`);
+ });
+ });
+
+ return missingFields;
+}
+
+export function validateGujiaQuotationRequiredFields(
+ headerForm: GujiaQuotationHeaderForm,
+ lineItems: GujiaQuotationLineItem[],
+) {
+ const missingFields: string[] = [];
+
+ QUOTATION_REQUIRED_HEADER_FIELDS.forEach(([key, label]) => {
+ pushMissingField(missingFields, headerForm[key], label);
+ });
+
+ if (lineItems.length === 0) {
+ missingFields.push("报价明细");
+ }
+
+ lineItems.forEach((lineItem, index) => {
+ QUOTATION_REQUIRED_LINE_FIELDS.forEach(([key, label]) => {
+ pushMissingField(missingFields, lineItem[key], `第 ${index + 1} 行 ${label}`);
+ });
+ });
+
+ return missingFields;
+}
+
+export function validateGujiaProductInfoConfirmationRequiredFields(
+ lineItems: GujiaProductInfoConfirmationLineItem[],
+) {
+ const missingFields: string[] = [];
+
+ if (lineItems.length === 0) {
+ missingFields.push("信息确认函明细");
+ }
+
+ lineItems.forEach((lineItem, index) => {
+ PRODUCT_INFO_REQUIRED_LINE_FIELDS.forEach(([key, label]) => {
+ pushMissingField(missingFields, lineItem[key], `第 ${index + 1} 行 ${label}`);
+ });
+ });
+
+ return missingFields;
+}
+
+export function buildRequiredFieldsMessage(
+ title: string,
+ missingFields: string[],
+) {
+ const previewFields = missingFields.slice(0, 8).join("、");
+ const suffix =
+ missingFields.length > 8 ? ` 等 ${missingFields.length} 个字段` : "";
+
+ return `${title}还有必填内容未填写:${previewFields}${suffix}。填完后才能确认。`;
+}
diff --git a/furniture-3d-system/src/app/intake/brand-filing/brandFilingSequence.ts b/furniture-3d-system/src/app/intake/brand-filing/brandFilingSequence.ts
index e08b001b..7fa04127 100644
--- a/furniture-3d-system/src/app/intake/brand-filing/brandFilingSequence.ts
+++ b/furniture-3d-system/src/app/intake/brand-filing/brandFilingSequence.ts
@@ -109,6 +109,16 @@ export function getBrandFilingBasicInfoApprovedAt(
);
}
+export function getBrandFilingEnteredAt(
+ product: NonIntegratedProductRecord,
+ brand: BrandFilingBrand,
+) {
+ return (
+ safeTrim(product.brandFilingData?.[brand]?.enteredAt) ||
+ getBrandFilingBasicInfoApprovedAt(product)
+ );
+}
+
function compareProductsByBrandFilingSequenceAnchor(
left: { product: NonIntegratedProductRecord; sourceIndex: number },
right: { product: NonIntegratedProductRecord; sourceIndex: number },
diff --git a/furniture-3d-system/src/app/intake/brand-filing/brandFilingTypes.ts b/furniture-3d-system/src/app/intake/brand-filing/brandFilingTypes.ts
index 55529e55..415532dc 100644
--- a/furniture-3d-system/src/app/intake/brand-filing/brandFilingTypes.ts
+++ b/furniture-3d-system/src/app/intake/brand-filing/brandFilingTypes.ts
@@ -31,6 +31,7 @@ export type BrandFilingPlanningFieldKey =
| "imageUrl";
export type BrandFilingPlanningLineItemFieldKey =
+ | "brandItemCode"
| "itemCode"
| "retailPriceDemand"
| "purchasePriceDemand"
@@ -80,6 +81,7 @@ export type GujiaQuotationLineItem = {
productId: string;
skuId: string;
sequenceNumber: string;
+ brandItemCode: string;
itemCode: string;
specification: string;
imageUrl: string;
@@ -134,6 +136,8 @@ export type GujiaProductInfoConfirmationRecord = {
export type BrandFilingBrandData = {
sequenceNumber?: number;
+ enteredAt?: string;
+ reportedAt?: string;
planningTable?: BrandFilingPlanningRecord;
gujiaQuotationTable?: GujiaQuotationRecord;
gujiaProductInfoConfirmation?: GujiaProductInfoConfirmationRecord;
@@ -178,6 +182,7 @@ export const BRAND_FILING_PLANNING_FIELD_KEYS = [
] as const satisfies readonly BrandFilingPlanningFieldKey[];
export const BRAND_FILING_PLANNING_LINE_ITEM_FIELD_KEYS = [
+ "brandItemCode",
"itemCode",
"retailPriceDemand",
"purchasePriceDemand",
@@ -316,6 +321,7 @@ export function normalizeGujiaQuotationLineItem(
productId: "",
skuId: "",
sequenceNumber: "",
+ brandItemCode: "",
itemCode: "",
specification: "",
imageUrl: "",
@@ -336,6 +342,7 @@ export function normalizeGujiaQuotationLineItem(
productId: readText(value.productId),
skuId: readText(value.skuId),
sequenceNumber: readText(value.sequenceNumber),
+ brandItemCode: readText(value.brandItemCode),
itemCode: readText(value.itemCode),
specification: readText(value.specification),
imageUrl: readText(value.imageUrl),
@@ -440,6 +447,8 @@ function normalizeBrandFilingBrandData(
if (!isRecord(value)) return undefined;
const sequenceNumber = readOptionalSequenceNumber(value.sequenceNumber);
+ const enteredAt = readText(value.enteredAt);
+ const reportedAt = readText(value.reportedAt);
const planningTable = normalizeBrandFilingPlanningRecord(value.planningTable);
const gujiaQuotationTable = normalizeGujiaQuotationRecord(
value.gujiaQuotationTable,
@@ -450,6 +459,8 @@ function normalizeBrandFilingBrandData(
);
if (
!sequenceNumber &&
+ !enteredAt &&
+ !reportedAt &&
!planningTable &&
!gujiaQuotationTable &&
!gujiaProductInfoConfirmation
@@ -459,6 +470,8 @@ function normalizeBrandFilingBrandData(
return {
...(sequenceNumber ? { sequenceNumber } : {}),
+ ...(enteredAt ? { enteredAt } : {}),
+ ...(reportedAt ? { reportedAt } : {}),
...(planningTable ? { planningTable } : {}),
...(gujiaQuotationTable ? { gujiaQuotationTable } : {}),
...(gujiaProductInfoConfirmation ? { gujiaProductInfoConfirmation } : {}),
diff --git a/furniture-3d-system/src/app/intake/brand-filing/gujiaQuotationData.ts b/furniture-3d-system/src/app/intake/brand-filing/gujiaQuotationData.ts
index 176b764b..1c6ddc63 100644
--- a/furniture-3d-system/src/app/intake/brand-filing/gujiaQuotationData.ts
+++ b/furniture-3d-system/src/app/intake/brand-filing/gujiaQuotationData.ts
@@ -292,6 +292,7 @@ function buildDefaultLineItem(params: {
sequenceNumber && sequenceNumber > 0
? String(sequenceNumber)
: String(productIndex + 1),
+ brandItemCode: "",
itemCode,
specification: buildSpecification(product, measurementRow, skuItem),
imageUrl: previewImage,
diff --git a/furniture-3d-system/src/server/services/brand-filing-gujia-quotation-table.service.ts b/furniture-3d-system/src/server/services/brand-filing-gujia-quotation-table.service.ts
index fca827d9..46dca31b 100644
--- a/furniture-3d-system/src/server/services/brand-filing-gujia-quotation-table.service.ts
+++ b/furniture-3d-system/src/server/services/brand-filing-gujia-quotation-table.service.ts
@@ -16,8 +16,8 @@ const SHEET_NAME = "报价单指导模板";
const DATA_START_ROW = 6;
const DATA_END_ROW = 279;
const FOOTER_START_ROW = 280;
-const DATA_COLUMN_COUNT = 13;
-const IMAGE_COLUMN = 4;
+const DATA_COLUMN_COUNT = 14;
+const IMAGE_COLUMN = 5;
const FOOTER_NOTE_ROW_COUNT = 5;
const TEMPLATE_NOTE_TEXT = [
"1、上述报价含增值税13%;",
@@ -160,6 +160,105 @@ function clearRowValues(
}
}
+function cloneCellStylePart(value: T): T {
+ if (value == null) {
+ return value;
+ }
+ return JSON.parse(JSON.stringify(value)) as T;
+}
+
+function copyCellStyle(sourceCell: ExcelJS.Cell, targetCell: ExcelJS.Cell) {
+ targetCell.numFmt = sourceCell.numFmt;
+ targetCell.font = cloneCellStylePart(sourceCell.font);
+ targetCell.alignment = cloneCellStylePart(sourceCell.alignment);
+ targetCell.fill = cloneCellStylePart(sourceCell.fill);
+ targetCell.border = cloneCellStylePart(sourceCell.border);
+ targetCell.protection = cloneCellStylePart(sourceCell.protection);
+}
+
+function unmergeIfExists(worksheet: ExcelJS.Worksheet, ref: string) {
+ try {
+ worksheet.unMergeCells(ref);
+ } catch {
+ // Some refs may already be unmerged after workbook normalization.
+ }
+}
+
+function mergeCellsWithStyle(worksheet: ExcelJS.Worksheet, ref: string) {
+ const sourceCell = worksheet.getCell(ref.split(":")[0]!);
+ const styleSnapshot = {
+ numFmt: sourceCell.numFmt,
+ font: cloneCellStylePart(sourceCell.font),
+ alignment: cloneCellStylePart(sourceCell.alignment),
+ fill: cloneCellStylePart(sourceCell.fill),
+ border: cloneCellStylePart(sourceCell.border),
+ protection: cloneCellStylePart(sourceCell.protection),
+ };
+
+ worksheet.mergeCells(ref);
+ const mergedCell = worksheet.getCell(ref.split(":")[0]!);
+ mergedCell.numFmt = styleSnapshot.numFmt;
+ mergedCell.font = cloneCellStylePart(styleSnapshot.font);
+ mergedCell.alignment = cloneCellStylePart(styleSnapshot.alignment);
+ mergedCell.fill = cloneCellStylePart(styleSnapshot.fill);
+ mergedCell.border = cloneCellStylePart(styleSnapshot.border);
+ mergedCell.protection = cloneCellStylePart(styleSnapshot.protection);
+}
+
+function prepareQuotationTemplateColumns(worksheet: ExcelJS.Worksheet) {
+ worksheet.getColumn(14).width = worksheet.getColumn(13).width ?? 9.65;
+
+ for (let rowNumber = 1; rowNumber <= FOOTER_START_ROW + 10; rowNumber += 1) {
+ copyCellStyle(
+ worksheet.getRow(rowNumber).getCell(13),
+ worksheet.getRow(rowNumber).getCell(14),
+ );
+ }
+
+ [
+ "A1:M1",
+ "A2:M2",
+ "A3:F3",
+ "H3:M3",
+ "A4:F4",
+ "H4:M4",
+ ].forEach((ref) => unmergeIfExists(worksheet, ref));
+
+ mergeCellsWithStyle(worksheet, "A1:N1");
+ mergeCellsWithStyle(worksheet, "A2:N2");
+ mergeCellsWithStyle(worksheet, "A3:G3");
+ mergeCellsWithStyle(worksheet, "I3:N3");
+ mergeCellsWithStyle(worksheet, "A4:G4");
+ mergeCellsWithStyle(worksheet, "I4:N4");
+
+ const headerLabels = [
+ "序号",
+ "品牌货号",
+ "产品货号",
+ "规格",
+ "图片",
+ "采购价(含税)",
+ "售价",
+ "材质",
+ "规格(CM)",
+ "起订量",
+ "采购周期\n(天)",
+ "包装件数",
+ "包装方式",
+ "备注",
+ ];
+ headerLabels.forEach((label, index) => {
+ const cell = worksheet.getRow(DATA_START_ROW - 1).getCell(index + 1);
+ cell.value = label;
+ cell.alignment = {
+ ...cell.alignment,
+ horizontal: "center",
+ vertical: "middle",
+ wrapText: true,
+ };
+ });
+}
+
function restoreFooterLayout(worksheet: ExcelJS.Worksheet, rowCount: number) {
const noteStartRow = DATA_START_ROW + rowCount;
const noteEndRow = noteStartRow + FOOTER_NOTE_ROW_COUNT - 1;
@@ -173,7 +272,7 @@ function restoreFooterLayout(worksheet: ExcelJS.Worksheet, rowCount: number) {
clearRowValues(worksheet, rowNumber);
}
- worksheet.mergeCells(`A${noteStartRow}:M${noteEndRow}`);
+ worksheet.mergeCells(`A${noteStartRow}:N${noteEndRow}`);
const noteCell = worksheet.getCell(`A${noteStartRow}`);
noteCell.value = TEMPLATE_NOTE_TEXT;
noteCell.font = {
@@ -187,9 +286,9 @@ function restoreFooterLayout(worksheet: ExcelJS.Worksheet, rowCount: number) {
wrapText: true,
};
- worksheet.mergeCells(`A${spacerRow}:M${spacerRow}`);
- worksheet.mergeCells(`B${warehouseHeaderRow}:M${warehouseHeaderRow}`);
- worksheet.mergeCells(`B${warehouseAddressRow}:M${warehouseAddressRow}`);
+ worksheet.mergeCells(`A${spacerRow}:N${spacerRow}`);
+ worksheet.mergeCells(`B${warehouseHeaderRow}:N${warehouseHeaderRow}`);
+ worksheet.mergeCells(`B${warehouseAddressRow}:N${warehouseAddressRow}`);
worksheet.getCell(`A${warehouseHeaderRow}`).value = "基地名称";
worksheet.getCell(`B${warehouseHeaderRow}`).value = "仓库详细地址";
@@ -329,16 +428,17 @@ async function buildWorkbookBytes(products: NonIntegratedProductRecord[]) {
throw new AppError(ErrorCode.SYSTEM_INTERNAL, "顾家产品报价表模板读取失败");
}
+ prepareQuotationTemplateColumns(worksheet);
prepareWorksheetRows(worksheet, displayRows.length);
restoreFooterLayout(worksheet, displayRows.length);
worksheet.getCell("A2").value = `编号:${initialState.headerForm.contactName.trim()}`;
worksheet.getCell("A3").value = `TO(需求方): ${initialState.headerForm.demandCompany.trim()}`;
- worksheet.getCell("G3").value = "FROM(供应方):";
- worksheet.getCell("H3").value = initialState.headerForm.supplyCompany.trim();
+ worksheet.getCell("H3").value = "FROM(供应方):";
+ worksheet.getCell("I3").value = initialState.headerForm.supplyCompany.trim();
worksheet.getCell("A4").value = `MOB(手机):${initialState.headerForm.demandMobile.trim()}`;
- worksheet.getCell("G4").value = "MOB(手机):";
- worksheet.getCell("H4").value = initialState.headerForm.supplyMobile.trim();
+ worksheet.getCell("H4").value = "MOB(手机):";
+ worksheet.getCell("I4").value = initialState.headerForm.supplyMobile.trim();
const imageTasks = displayRows.map(async (row, index) => {
const rowNumber = DATA_START_ROW + index;
@@ -349,18 +449,19 @@ async function buildWorkbookBytes(products: NonIntegratedProductRecord[]) {
}
}
- worksheet.getCell(`B${rowNumber}`).value = row.itemCode.trim();
- worksheet.getCell(`C${rowNumber}`).value = row.specification.trim();
- worksheet.getCell(`D${rowNumber}`).value = "";
- worksheet.getCell(`E${rowNumber}`).value = row.purchasePrice.trim();
- worksheet.getCell(`F${rowNumber}`).value = row.salesPrice.trim();
- worksheet.getCell(`G${rowNumber}`).value = row.material.trim();
- worksheet.getCell(`H${rowNumber}`).value = row.sizeCm.trim();
- worksheet.getCell(`I${rowNumber}`).value = row.minimumOrderQuantity.trim();
- worksheet.getCell(`J${rowNumber}`).value = row.leadTimeDays.trim();
- worksheet.getCell(`K${rowNumber}`).value = row.packageCount.trim();
- worksheet.getCell(`L${rowNumber}`).value = row.packageMethod.trim();
- worksheet.getCell(`M${rowNumber}`).value = row.remark.trim();
+ worksheet.getCell(`B${rowNumber}`).value = row.brandItemCode.trim();
+ worksheet.getCell(`C${rowNumber}`).value = row.itemCode.trim();
+ worksheet.getCell(`D${rowNumber}`).value = row.specification.trim();
+ worksheet.getCell(`E${rowNumber}`).value = "";
+ worksheet.getCell(`F${rowNumber}`).value = row.purchasePrice.trim();
+ worksheet.getCell(`G${rowNumber}`).value = row.salesPrice.trim();
+ worksheet.getCell(`H${rowNumber}`).value = row.material.trim();
+ worksheet.getCell(`I${rowNumber}`).value = row.sizeCm.trim();
+ worksheet.getCell(`J${rowNumber}`).value = row.minimumOrderQuantity.trim();
+ worksheet.getCell(`K${rowNumber}`).value = row.leadTimeDays.trim();
+ worksheet.getCell(`L${rowNumber}`).value = row.packageCount.trim();
+ worksheet.getCell(`M${rowNumber}`).value = row.packageMethod.trim();
+ worksheet.getCell(`N${rowNumber}`).value = row.remark.trim();
if (!row.imageUrl.trim()) return;
diff --git a/furniture-3d-system/src/server/services/brand-filing-planning-table.service.ts b/furniture-3d-system/src/server/services/brand-filing-planning-table.service.ts
index a67794c5..499ef56f 100644
--- a/furniture-3d-system/src/server/services/brand-filing-planning-table.service.ts
+++ b/furniture-3d-system/src/server/services/brand-filing-planning-table.service.ts
@@ -328,16 +328,34 @@ function insertStyledRows(
}
const DYNAMIC_TEMPLATE_MERGE_REFS = [
+ "B10:C10",
+ "C10:D10",
+ "D10:E10",
+ "E10:F10",
+ "F10:G10",
+ "G10:H10",
+ "H10:I10",
+ "I10:J10",
+ "J10:L10",
+ "K10:L10",
"B11:C11",
+ "C11:D11",
"D11:E11",
+ "E11:F11",
"F11:G11",
+ "G11:H11",
"H11:I11",
+ "I11:J11",
"J11:L11",
+ "K11:L11",
"A12:L12",
"A13:A14",
"B13:B14",
+ "C13:C14",
"C13:D13",
+ "D13:E13",
"E13:L13",
+ "F13:L13",
"I14:J14",
"K14:L14",
"A15:B15",
@@ -491,19 +509,95 @@ function setCellCenterAlignment(
};
}
-function applyCenterAcrossRange(
+function clearRowValues(
worksheet: ExcelJS.Worksheet,
- ref: string,
+ rowNumber: number,
+ startColumnNumber = 1,
+ endColumnNumber = 12,
) {
- const addresses = expandRangeAddresses(ref);
- if (addresses.length === 0) return;
+ for (
+ let columnNumber = startColumnNumber;
+ columnNumber <= endColumnNumber;
+ columnNumber += 1
+ ) {
+ worksheet.getCell(`${columnNumberToName(columnNumber)}${rowNumber}`).value =
+ "";
+ }
+}
- const firstCell = worksheet.getCell(addresses[0]!);
- firstCell.alignment = {
- ...firstCell.alignment,
- horizontal: "centerContinuous",
- vertical: "middle",
- };
+function setTextCellValue(
+ worksheet: ExcelJS.Worksheet,
+ address: string,
+ value: string,
+) {
+ worksheet.getCell(address).value = value.trim();
+ setCellCenterAlignment(worksheet, address);
+}
+
+function writePlanningHeaderRows(params: {
+ worksheet: ExcelJS.Worksheet;
+ planHeaderRow: number;
+ promotionHeaderTopRow: number;
+ promotionHeaderBottomRow: number;
+}) {
+ const {
+ worksheet,
+ planHeaderRow,
+ promotionHeaderTopRow,
+ promotionHeaderBottomRow,
+ } = params;
+
+ clearRowValues(worksheet, planHeaderRow);
+ applyMergeRefs(worksheet, [
+ `C${planHeaderRow}:D${planHeaderRow}`,
+ `E${planHeaderRow}:F${planHeaderRow}`,
+ `G${planHeaderRow}:H${planHeaderRow}`,
+ `I${planHeaderRow}:J${planHeaderRow}`,
+ `K${planHeaderRow}:L${planHeaderRow}`,
+ ]);
+ [
+ ["A", "品牌货号"],
+ ["B", "货号"],
+ ["C", "零售价需求"],
+ ["E", "采购价需求"],
+ ["G", "尺寸要求"],
+ ["I", "材质/颜色"],
+ ["K", "特殊说明(是否老款迭代)"],
+ ].forEach(([column, label]) => {
+ setTextCellValue(worksheet, `${column}${planHeaderRow}`, label);
+ });
+
+ clearRowValues(worksheet, promotionHeaderTopRow);
+ clearRowValues(worksheet, promotionHeaderBottomRow);
+ applyMergeRefs(worksheet, [
+ `A${promotionHeaderTopRow}:A${promotionHeaderBottomRow}`,
+ `B${promotionHeaderTopRow}:B${promotionHeaderBottomRow}`,
+ `C${promotionHeaderTopRow}:C${promotionHeaderBottomRow}`,
+ `D${promotionHeaderTopRow}:E${promotionHeaderTopRow}`,
+ `F${promotionHeaderTopRow}:L${promotionHeaderTopRow}`,
+ `K${promotionHeaderBottomRow}:L${promotionHeaderBottomRow}`,
+ ]);
+ [
+ ["A", "系列"],
+ ["B", "品牌货号"],
+ ["C", "货号"],
+ ["D", "上市计划"],
+ ["F", "上市季度销量"],
+ ].forEach(([column, label]) => {
+ setTextCellValue(worksheet, `${column}${promotionHeaderTopRow}`, label);
+ });
+ [
+ ["D", "全年销售总量"],
+ ["E", "渠道"],
+ ["F", "第1个月"],
+ ["G", "第2个月"],
+ ["H", "第3个月"],
+ ["I", "第4-6月"],
+ ["J", "第7-9月"],
+ ["K", "第10-12月"],
+ ].forEach(([column, label]) => {
+ setTextCellValue(worksheet, `${column}${promotionHeaderBottomRow}`, label);
+ });
}
function writePlanningSectionRows(params: {
@@ -566,22 +660,13 @@ function writePlanningSectionRows(params: {
applyMergeRefs(worksheet, [
`A${promotionSectionTitleRow}:L${promotionSectionTitleRow}`,
- `A${promotionHeaderTopRow}:A${promotionHeaderBottomRow}`,
- `B${promotionHeaderTopRow}:B${promotionHeaderBottomRow}`,
- `C${promotionHeaderTopRow}:D${promotionHeaderTopRow}`,
]);
- applyCenterAcrossRange(
+ writePlanningHeaderRows({
worksheet,
- `E${promotionHeaderTopRow}:L${promotionHeaderTopRow}`,
- );
- applyCenterAcrossRange(
- worksheet,
- `I${promotionHeaderBottomRow}:J${promotionHeaderBottomRow}`,
- );
- applyCenterAcrossRange(
- worksheet,
- `K${promotionHeaderBottomRow}:L${promotionHeaderBottomRow}`,
- );
+ planHeaderRow: planDataStartRow - 1,
+ promotionHeaderTopRow,
+ promotionHeaderBottomRow,
+ });
for (let offset = 0; offset < rowCount; offset += 1) {
const rowNumber = promotionDataStartRow + offset;
@@ -604,43 +689,48 @@ function writePlanningSectionRows(params: {
displayRows.forEach((displayRow, index) => {
const planRowNumber = planDataStartRow + index;
- setCellValue(
+ setTextCellValue(
worksheet,
`A${planRowNumber}`,
+ displayRow.brandItemCode,
+ );
+ setCellValue(
+ worksheet,
+ `B${planRowNumber}`,
"itemCode",
displayRow.itemCode,
);
setCellValue(
worksheet,
- `B${planRowNumber}`,
+ `C${planRowNumber}`,
"retailPriceDemand",
displayRow.retailPriceDemand,
);
setCellValue(
worksheet,
- `D${planRowNumber}`,
+ `E${planRowNumber}`,
"purchasePriceDemand",
displayRow.purchasePriceDemand,
);
setCellValue(
worksheet,
- `F${planRowNumber}`,
+ `G${planRowNumber}`,
"sizeRequirement",
displayRow.sizeRequirement,
);
setCellValue(
worksheet,
- `H${planRowNumber}`,
+ `I${planRowNumber}`,
"materialAndColor",
displayRow.materialAndColor,
);
setCellValue(
worksheet,
- `J${planRowNumber}`,
+ `K${planRowNumber}`,
"legacyIterationNote",
record.form.legacyIterationNote,
);
- ["A", "B", "D", "F", "H", "J"].forEach((column) => {
+ ["A", "B", "C", "E", "G", "I", "K"].forEach((column) => {
setCellCenterAlignment(worksheet, `${column}${planRowNumber}`);
});
@@ -651,51 +741,56 @@ function writePlanningSectionRows(params: {
"seriesName",
displayRow.seriesName,
);
- setCellValue(
+ setTextCellValue(
worksheet,
`B${promotionRowNumber}`,
+ displayRow.brandItemCode,
+ );
+ setCellValue(
+ worksheet,
+ `C${promotionRowNumber}`,
"itemCode",
displayRow.itemCode,
);
setCellValue(
worksheet,
- `C${promotionRowNumber}`,
+ `D${promotionRowNumber}`,
"annualSalesTarget",
displayRow.annualSalesTarget,
);
setCellValue(
worksheet,
- `D${promotionRowNumber}`,
+ `E${promotionRowNumber}`,
"channel",
displayRow.channel,
);
setCellValue(
worksheet,
- `E${promotionRowNumber}`,
+ `F${promotionRowNumber}`,
"month1Sales",
displayRow.month1Sales,
);
setCellValue(
worksheet,
- `F${promotionRowNumber}`,
+ `G${promotionRowNumber}`,
"month2Sales",
displayRow.month2Sales,
);
setCellValue(
worksheet,
- `G${promotionRowNumber}`,
+ `H${promotionRowNumber}`,
"month3Sales",
displayRow.month3Sales,
);
setCellValue(
worksheet,
- `H${promotionRowNumber}`,
+ `I${promotionRowNumber}`,
"month4To6Sales",
displayRow.month4To6Sales,
);
setCellValue(
worksheet,
- `I${promotionRowNumber}`,
+ `J${promotionRowNumber}`,
"month7To9Sales",
displayRow.month7To9Sales,
);
@@ -705,9 +800,11 @@ function writePlanningSectionRows(params: {
"month10To12Sales",
displayRow.month10To12Sales,
);
- ["A", "B", "C", "D", "E", "F", "G", "H", "I", "K"].forEach((column) => {
- setCellCenterAlignment(worksheet, `${column}${promotionRowNumber}`);
- });
+ ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"].forEach(
+ (column) => {
+ setCellCenterAlignment(worksheet, `${column}${promotionRowNumber}`);
+ },
+ );
});
setCellValue(
|