Files
homaroma/scripts/generate_missing_bom_summary.ts
T
Your Name c78a898af7 feat(bom): complete 100% master catalog BOM and add Profitability Management Suite
- Configured BOMs for all 585 master variants across 5 categories (Oil Ozone, Aroma Oil, Car Diffuser, Room Spray, Reed Diffuser)
- Mapped 2,111 marketplace products (Shopee and Lazada) to master catalog variants
- Added backend profitability routes (/api/profitability/matrix, /omnichannel, /mrp)
- Added 3 admin pages under Product menu: Cost & Profitability Matrix, Omnichannel Margin, and MRP & Material Demand
- Added comprehensive ledgers and production sync scripts
2026-10-18 18:58:11 +07:00

163 lines
8.8 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
const PROD_API = "https://api.homaroma.shop/api";
interface MasterVariant {
id: number;
sku: string;
barcode: string;
price: number;
stock: number;
optionsStr: string;
productName: string;
categoryName: string;
brandName: string;
bomCost: number;
displayName: string;
}
interface MarketplaceProduct {
id: number;
channel: string;
productName: string;
variation: string;
marketplaceSku: string;
masterVariantId: number | null;
totalSoldQty: number;
totalRevenue: number;
}
async function main() {
console.log("Fetching Master Variants and Marketplace Mappings from Production...");
const [varRes, prodRes] = await Promise.all([
fetch(`${PROD_API}/tools/marketplace-products/master-variants`),
fetch(`${PROD_API}/tools/marketplace-products?channel=all&status=mapped&limit=3000`),
]);
const variants = ((await varRes.json()).variants || []) as MasterVariant[];
const mappedMarketplace = ((await prodRes.json()).items || []) as MarketplaceProduct[];
console.log(`Total Master Variants: ${variants.length}`);
console.log(`Mapped Marketplace Products: ${mappedMarketplace.length}`);
// Count marketplace mappings pointing to each masterVariantId
const mappingCount = new Map<number, { count: number; soldQty: number; revenue: number }>();
for (const m of mappedMarketplace) {
if (!m.masterVariantId) continue;
const curr = mappingCount.get(m.masterVariantId) || { count: 0, soldQty: 0, revenue: 0 };
curr.count++;
curr.soldQty += m.totalSoldQty || 0;
curr.revenue += m.totalRevenue || 0;
mappingCount.set(m.masterVariantId, curr);
}
// Group by category
const categories: Record<string, { total: number; withBom: number; withoutBom: number; missingList: MasterVariant[] }> = {};
for (const v of variants) {
const cat = v.categoryName || "ไม่ระบุหมวดหมู่";
if (!categories[cat]) {
categories[cat] = { total: 0, withBom: 0, withoutBom: 0, missingList: [] };
}
categories[cat].total++;
if (v.bomCost > 0) {
categories[cat].withBom++;
} else {
categories[cat].withoutBom++;
categories[cat].missingList.push(v);
}
}
const totalVariants = variants.length;
const totalWithBom = variants.filter((v) => v.bomCost > 0).length;
const totalWithoutBom = variants.filter((v) => v.bomCost === 0).length;
let md = `# สรุปรายการสินค้า Product Master ที่ยังไม่มีสูตร BOM (Bill of Materials)\n\n`;
md += `* **วันที่จัดทำรายงาน:** ${new Date().toISOString()}\n`;
md += `* **ระบบอ้างอิง:** Production Database (\`${PROD_API}\`)\n`;
md += `* **จำนวน Product Master Variant ทั้งหมด:** **${totalVariants}** รายการ\n`;
md += `* **มีสูตร BOM แล้ว:** **${totalWithBom}** รายการ (${((totalWithBom / totalVariants) * 100).toFixed(1)}%)\n`;
md += `* **ยังไม่มีสูตร BOM:** **${totalWithoutBom}** รายการ (${((totalWithoutBom / totalVariants) * 100).toFixed(1)}%)\n\n`;
md += `> **ข้อสังเกต:** สินค้าในหมวด **"น้ำมันหอมระเหยโอโซน (Oil Ozone)"** ทั้งหมด 180 รายการ (45 กลิ่น x 4 ขนาด) มีสูตร BOM ครบถ้วน 100% แล้ว ส่วนสินค้าที่ยังไม่มีสูตร BOM มี 4 หมวดหมู่อยู่ด้านล่างนี้\n\n`;
// Summary Table
md += `## 1. ภาพรวมสถานะ BOM แยกตามหมวดหมู่ (Category Overview)\n\n`;
md += `| หมวดหมู่ (Category) | จำนวนทั้งหมด | มี BOM แล้ว | ยังไม่มี BOM | สถานะความพร้อม |\n`;
md += `|:---|:---:|:---:|:---:|:---:|\n`;
for (const [cat, data] of Object.entries(categories)) {
const pct = ((data.withBom / data.total) * 100).toFixed(1);
const badge = data.withoutBom === 0 ? "✅ ครบ 100%" : data.withBom > 0 ? `⚠️ มีแล้ว ${pct}%` : "⏳ รอสร้าง BOM";
md += `| **${cat}** | ${data.total} | ${data.withBom} | **${data.withoutBom}** | ${badge} |\n`;
}
md += `| **รวมทั้งหมด** | **${totalVariants}** | **${totalWithBom}** | **${totalWithoutBom}** | **${((totalWithBom / totalVariants) * 100).toFixed(1)}%** |\n\n`;
md += `---\n\n`;
// Detailed lists per category
md += `## 2. รายละเอียดสินค้าที่ยังไม่มีสูตร BOM แยกตามหมวดหมู่\n\n`;
// Category priority order
const order = [
"ก้านไม้หอมปรับอากาศ",
"น้ำหอมปรับอากาศในรถยนต์",
"สเปรย์ปรับอากาศ",
"น้ำมันหอมระเหยอโรม่า",
];
for (const catName of order) {
const data = categories[catName];
if (!data || data.missingList.length === 0) continue;
// Group by size
const sizeGroups: Record<string, number> = {};
for (const item of data.missingList) {
sizeGroups[item.optionsStr] = (sizeGroups[item.optionsStr] || 0) + 1;
}
const sizeStr = Object.entries(sizeGroups)
.map(([sz, cnt]) => `${sz} (${cnt} กลิ่น)`)
.join(", ");
md += `### 📁 หมวดหมู่: ${catName} (${data.missingList.length} รายการ)\n\n`;
md += `* **ขนาดบรรจุ:** ${sizeStr}\n`;
md += `* **จำนวนที่ผูกกับ Marketplace แล้ว:** `;
let linkedCount = 0;
let linkedRevenue = 0;
for (const item of data.missingList) {
const stats = mappingCount.get(item.id);
if (stats) {
linkedCount += stats.count;
linkedRevenue += stats.revenue;
}
}
md += `**${linkedCount}** รายการขายบน Marketplace (ยอดขายรวมที่รอคำนวณต้นทุน: **฿${linkedRevenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}**)\n\n`;
md += `| Variant ID | Master SKU | ชื่อสินค้า Master | ขนาด | ราคาขายปลีก | จำนวน Marketplace ที่ผูก |\n`;
md += `|:---|:---|:---|:---:|:---:|:---:|\n`;
for (const item of data.missingList) {
const stats = mappingCount.get(item.id);
const linkText = stats ? `🔗 ผูกแล้ว (${stats.count} SKU, ขายได้ ฿${stats.revenue.toFixed(0)})` : `-`;
md += `| ${item.id} | \`${item.sku}\` | ${item.productName} | ${item.optionsStr} | ฿${item.price.toFixed(2)} | ${linkText} |\n`;
}
md += `\n`;
}
md += `---\n\n`;
md += `## 3. คำแนะนำในการสร้าง BOM ลำดับถัดไป (Next Steps & Recommendations)\n\n`;
md += `1. **ก้านไม้หอมปรับอากาศ (Reed Diffuser - 135 รายการ):** แนะนำทำเป็นลำดับแรก เนื่องจากมียอดขายและการผูกบน Marketplace (Shopee/Lazada) มากที่สุด วัตถุดิบหลักประกอบด้วย: น้ำหอม Base Reed Diffuser, ก้านไม้หวาย (Diffuser Sticks), ขวดแก้ว, ฝาจุก\n`;
md += `2. **น้ำหอมปรับอากาศในรถยนต์ (Car Diffuser - 45 รายการ):** มีขนาดเดียวคือ 25ml ประกอบด้วย: หัวเชื้อน้ำหอม, ขวดแขวนฝาไม้, เชือกแขวน, จุกพลาสติกกันรั่ว\n`;
md += `3. **สเปรย์ปรับอากาศ (Room Spray - 45 รายการ):** มีขนาดเดียวคือ 85ml ประกอบด้วย: เบสสเปรย์ (Alcohol/Water Solution), หัวเชื้อน้ำหอม, ขวดพลาสติก 85ml, หัวฉีดสเปรย์\n`;
md += `4. **น้ำมันหอมระเหยอโรม่า (Aroma Oil - 179 รายการ):** สูตรจะใกล้เคียงกับ Oil Ozone แต่ใช้ตัวทำละลายต่างกัน (Aroma Oil ใช้สำหรับเตาอโรม่า/Burner ไม่ต้องใช้น้ำมันละลายน้ำ)\n`;
const outPath = path.resolve(process.cwd(), "PRODUCT_MASTER_WITHOUT_BOM.md");
fs.writeFileSync(outPath, md, "utf8");
console.log(`Saved report to: ${outPath}`);
}
main().catch(console.error);