feat: 新增各渠道收入占比
This commit is contained in:
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const RAW_DIR = path.join(ROOT, 'data/ppmt/raw');
|
||||
const RAWDATA_DIR = path.join(ROOT, 'data/ppmt/rawData');
|
||||
const OUTPUT_DIR = path.join(ROOT, 'data/ppmt');
|
||||
|
||||
const FILES = {
|
||||
@@ -565,8 +566,8 @@ writeViewFiles('ppmt_expense_structure', ['period', 'sales_expense', 'admin_expe
|
||||
sales_expense: 2, admin_expense: 2, financial_expense: 2, four_expense_rate: 1,
|
||||
});
|
||||
|
||||
function readRuntimeRows(filename) {
|
||||
const [header, ...lines] = fs.readFileSync(path.join(OUTPUT_DIR, filename), 'utf8').trim().split(/\r?\n/);
|
||||
function readCsvRows(dir, filename) {
|
||||
const [header, ...lines] = fs.readFileSync(path.join(dir, filename), 'utf8').trim().split(/\r?\n/);
|
||||
const headers = header.split(',');
|
||||
return lines.map((line) => Object.fromEntries(line.split(',').map((cell, index) => {
|
||||
const value = Number(cell);
|
||||
@@ -574,6 +575,10 @@ function readRuntimeRows(filename) {
|
||||
})));
|
||||
}
|
||||
|
||||
function readRuntimeRows(filename) {
|
||||
return readCsvRows(OUTPUT_DIR, filename);
|
||||
}
|
||||
|
||||
function writeRuntimeViews(base, headers, views, digits = {}) {
|
||||
writeViewFiles(base, headers, views, digits);
|
||||
}
|
||||
@@ -698,3 +703,45 @@ writeViewFiles('ppmt_inventory', ['period', 'inventory', 'yoy'], inventoryViews,
|
||||
}, 2017);
|
||||
|
||||
console.log('已生成第二批连续图表的全部、年度、半年 CSV。');
|
||||
|
||||
// 渠道收入占比:底表 data/ppmt/rawData/地区渠道收入.csv 整理自
|
||||
// 泡泡玛特-rawdata.xlsx「各地区渠道收入」工作表(中国含港澳台的四地区 × 四渠道明细)。
|
||||
// 全球合计 = 四地区加总;披露口径只有 H1 与全年,H2 单半年 = 全年 − H1;
|
||||
// 占比在金额视图之后计算,按当期四渠道合计归一化,不可用占比直接相减。
|
||||
const CHANNEL_NAME_TO_KEY = {
|
||||
零售店: 'retail',
|
||||
机器人商店: 'robot_store',
|
||||
线上渠道: 'online',
|
||||
批发及其他: 'wholesale',
|
||||
};
|
||||
const channelRevenueByPeriod = new Map();
|
||||
readCsvRows(RAWDATA_DIR, '地区渠道收入.csv').forEach(({ region, channel, period, revenue }) => {
|
||||
const key = CHANNEL_NAME_TO_KEY[channel];
|
||||
if (!key) throw new Error(`地区渠道收入底表存在未知渠道:${channel}`);
|
||||
if (!period || !Number.isFinite(revenue)) throw new Error(`地区渠道收入底表行不完整:${region} ${channel} ${period} ${revenue}`);
|
||||
if (!channelRevenueByPeriod.has(period)) {
|
||||
channelRevenueByPeriod.set(period, { retail: 0, online: 0, robot_store: 0, wholesale: 0 });
|
||||
}
|
||||
channelRevenueByPeriod.get(period)[key] += revenue;
|
||||
});
|
||||
|
||||
const channelAmountViews = buildViews((year, type) => {
|
||||
const bucket = channelRevenueByPeriod.get(type === 'H1' ? `${year}H1` : `${year}FY`);
|
||||
return bucket ?? null;
|
||||
});
|
||||
const channelShareViews = Object.fromEntries(Object.entries(channelAmountViews).map(([view, rows]) => [
|
||||
view,
|
||||
rows.map(({ period, ...amounts }) => {
|
||||
const total = Object.values(amounts).reduce((sum, value) => sum + value, 0);
|
||||
return {
|
||||
period,
|
||||
...Object.fromEntries(Object.entries(amounts).map(([key, value]) => [key, value / total * 100])),
|
||||
total_revenue: total,
|
||||
};
|
||||
}),
|
||||
]));
|
||||
writeRuntimeViews('ppmt_channel_share', ['period', 'retail', 'online', 'robot_store', 'wholesale', 'total_revenue'], channelShareViews, {
|
||||
retail: 1, online: 1, robot_store: 1, wholesale: 1, total_revenue: 2,
|
||||
});
|
||||
|
||||
console.log('已生成渠道收入占比的全部、年度、半年 CSV。');
|
||||
|
||||
Reference in New Issue
Block a user