Commit 594861cb authored by allen.wang's avatar allen.wang

feat:init

parent 574904c8
# skills # skills
http://erp.charleskeith.cn/metaData/api/flow/getOutboundDetails?pageNumber=1&pageSize=20&sortName=daily&sortOrder=-1&sku=8887049481029&type=%E5%85%B1%E4%BA%AB%E4%BB%93%E5%BA%93%E5%AD%98%E5%8F%98%E5%8A%A8%E8%AE%B0%E5%BD%95&range_createddate_begin=2026-04-09%2008%3A49%3A47&range_createddate_end=2026-04-10%2008%3A49%3A47
http://erp.charleskeith.cn/metaData/api/flow/getOutboundDetails?pageNumber=1&pageSize=20&sortName=daily&sortOrder=-1&sku=8887049481029&type=%E7%89%A9%E7%90%86%E4%BB%93%E5%BA%93%E5%AD%98%E5%8F%98%E5%8A%A8%E8%AE%B0%E5%BD%95&range_createddate_begin=2026-04-09%2008%3A50%3A20&range_createddate_end=2026-04-10%2008%3A50%3A20
http://erp.charleskeith.cn/metaData/api/flow/getOutboundDetails?pageNumber=1&pageSize=20&sortName=daily&sortOrder=-1&sku=8887049481029&type=%E6%9B%B4%E6%96%B0%E5%BA%97%E9%93%BA%E5%BA%93%E5%AD%98%E8%AE%B0%E5%BD%95&range_createddate_begin=2026-04-09%2008%3A50%3A35&range_createddate_end=2026-04-10%2008%3A50%3A35
http://erp.charleskeith.cn/metaData/api/flow/getOutboundDetails?pageNumber=1&pageSize=20&sortName=daily&sortOrder=-1&sku=8887049481029&type=%E7%B3%BB%E7%BB%9F%E6%8E%A8%E5%8D%95%E8%AE%B0%E5%BD%95&range_createddate_begin=2026-04-09%2008%3A50%3A43&range_createddate_end=2026-04-10%2008%3A50%3A43
http://erp.charleskeith.cn/metaData/api/flow/getOutboundDetailsNum?sku=8887049481029&type=%E4%BB%93%E5%BA%93%E6%96%87%E4%BB%B6%E5%BA%93%E5%AD%98&range_createddate_begin=2026-04-09%2008%3A50%3A55&range_createddate_end=2026-04-10%2008%3A50%3A55&summarizingFields=qty
...@@ -15,6 +15,7 @@ description: 协调 VIP 月报所有页的来源与渲染,通过固定配置 ...@@ -15,6 +15,7 @@ description: 协调 VIP 月报所有页的来源与渲染,通过固定配置
- 依赖 Tableau (`tableau.charleskeith.cn`) 时统一走 `config.yaml` 里的账号(目前是 `ec_user01`),优先尝试下载/导出,若必须截屏则按照脚本裁切。 - 依赖 Tableau (`tableau.charleskeith.cn`) 时统一走 `config.yaml` 里的账号(目前是 `ec_user01`),优先尝试下载/导出,若必须截屏则按照脚本裁切。
- 某些页面会引用 `vip.com`,凭据不驻留仓库;调用 `config.yaml` 中的 `vip.login_endpoint` 让第三方系统自动处理登录。 - 某些页面会引用 `vip.com`,凭据不驻留仓库;调用 `config.yaml` 中的 `vip.login_endpoint` 让第三方系统自动处理登录。
- 需要数据库的页面使用测试库 `ckc_cep_db_test``config.yaml``mysql` 段),会参考 `s11-source-validation.md``s11-sql-hypothesis.sql` 等文件保持口径。 - 需要数据库的页面使用测试库 `ckc_cep_db_test``config.yaml``mysql` 段),会参考 `s11-source-validation.md``s11-sql-hypothesis.sql` 等文件保持口径。
- S11 的活动口径已切换到 `oms_shop_report``memo` 维护活动/非活动,`single='Y'` 维护独家。
## 关键路径 ## 关键路径
......
...@@ -3,7 +3,8 @@ param( ...@@ -3,7 +3,8 @@ param(
[string]$Slides = "S04,S05,S06,S07,S08", [string]$Slides = "S04,S05,S06,S07,S08",
[string]$ReportMonth = "", [string]$ReportMonth = "",
[int]$ReportYear = 0, [int]$ReportYear = 0,
[int]$CompareYear = 0 [int]$CompareYear = 0,
[switch]$DisableTemplateLock
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
...@@ -24,6 +25,9 @@ if ($ReportYear -gt 0) { ...@@ -24,6 +25,9 @@ if ($ReportYear -gt 0) {
if ($CompareYear -gt 0) { if ($CompareYear -gt 0) {
$pythonArgs += @("--compare-year", "$CompareYear") $pythonArgs += @("--compare-year", "$CompareYear")
} }
if ($DisableTemplateLock.IsPresent) {
$pythonArgs += "--disable-template-lock"
}
python @pythonArgs python @pythonArgs
Write-Output $opsPath Write-Output $opsPath
...@@ -13,6 +13,7 @@ description: Use when preparing VIP report slide S11 from MySQL daily report tab ...@@ -13,6 +13,7 @@ description: Use when preparing VIP report slide S11 from MySQL daily report tab
- `oms_daily_report` - `oms_daily_report`
- `oms_daily_report_detail` - `oms_daily_report_detail`
- `oms_shop_report`(活动款/非活动款/ESS/独家标记)
- `ceprepeat` for `Repeat / ESS` supporting evidence - `ceprepeat` for `Repeat / ESS` supporting evidence
## Validation References ## Validation References
...@@ -23,5 +24,7 @@ description: Use when preparing VIP report slide S11 from MySQL daily report tab ...@@ -23,5 +24,7 @@ description: Use when preparing VIP report slide S11 from MySQL daily report tab
## Guardrails ## Guardrails
- `活动款 / 独家款 / 非活动款` 当前仍视为未完全确认 - 活动口径优先使用 `oms_shop_report.memo`
- 不要把弱线索写成正式标签源 - `活动款``活动折扣款` -> 活动款
- `非活动款``ESS` -> 非活动款
- 独家口径使用 `oms_shop_report.single='Y'`
...@@ -114,6 +114,8 @@ if (-not (Test-Path -LiteralPath $TemplatePath)) { ...@@ -114,6 +114,8 @@ if (-not (Test-Path -LiteralPath $TemplatePath)) {
$ops = [pscustomobject]@{ $ops = [pscustomobject]@{
replace_text = @() replace_text = @()
replace_tables = @()
replace_charts = @()
replace_images = @() replace_images = @()
} }
...@@ -125,10 +127,18 @@ if ($OperationsPath) { ...@@ -125,10 +127,18 @@ if ($OperationsPath) {
} }
$replaceText = @() $replaceText = @()
$replaceTables = @()
$replaceCharts = @()
$replaceImages = @() $replaceImages = @()
if ($ops.PSObject.Properties.Name -contains "replace_text") { if ($ops.PSObject.Properties.Name -contains "replace_text") {
$replaceText = @($ops.replace_text) $replaceText = @($ops.replace_text)
} }
if ($ops.PSObject.Properties.Name -contains "replace_tables") {
$replaceTables = @($ops.replace_tables)
}
if ($ops.PSObject.Properties.Name -contains "replace_charts") {
$replaceCharts = @($ops.replace_charts)
}
if ($ops.PSObject.Properties.Name -contains "replace_images") { if ($ops.PSObject.Properties.Name -contains "replace_images") {
$replaceImages = @($ops.replace_images) $replaceImages = @($ops.replace_images)
} }
...@@ -140,7 +150,7 @@ if ($outDir -and -not (Test-Path -LiteralPath $outDir)) { ...@@ -140,7 +150,7 @@ if ($outDir -and -not (Test-Path -LiteralPath $outDir)) {
Copy-Item -LiteralPath $TemplatePath -Destination $OutputPath -Force Copy-Item -LiteralPath $TemplatePath -Destination $OutputPath -Force
if ($replaceText.Count -eq 0 -and $replaceImages.Count -eq 0) { if ($replaceText.Count -eq 0 -and $replaceTables.Count -eq 0 -and $replaceCharts.Count -eq 0 -and $replaceImages.Count -eq 0) {
# 无替换操作时直接复制模板,避免 PowerPoint 重新保存造成包结构差异。 # 无替换操作时直接复制模板,避免 PowerPoint 重新保存造成包结构差异。
Write-Output $OutputPath Write-Output $OutputPath
return return
...@@ -171,6 +181,52 @@ try { ...@@ -171,6 +181,52 @@ try {
Replace-PictureShape -Slide $slide -Shape $shape -ImagePath ([string]$item.image_path) Replace-PictureShape -Slide $slide -Shape $shape -ImagePath ([string]$item.image_path)
} }
foreach ($item in $replaceTables) {
$slide = $pres.Slides.Item([int]$item.slide)
$shape = Find-Shape -Slide $slide -ShapeId $item.shape_id -ShapeName $item.shape_name -ExactText $item.exact_text -ContainsText $item.contains_text
if ($null -eq $shape) {
throw "Table target not found on slide $($item.slide)"
}
if ($shape.HasTable -ne -1) {
throw "Shape '$($shape.Name)' is not a table."
}
foreach ($cell in @($item.cells)) {
$row = [int]$cell.row
$col = [int]$cell.col
$newText = [string]$cell.new_text
$shape.Table.Cell($row, $col).Shape.TextFrame.TextRange.Text = $newText
}
}
foreach ($item in $replaceCharts) {
$slide = $pres.Slides.Item([int]$item.slide)
$shape = Find-Shape -Slide $slide -ShapeId $item.shape_id -ShapeName $item.shape_name -ExactText $item.exact_text -ContainsText $item.contains_text
if ($null -eq $shape) {
throw "Chart target not found on slide $($item.slide)"
}
if ($shape.HasChart -ne -1) {
throw "Shape '$($shape.Name)' is not a chart."
}
$chart = $shape.Chart
foreach ($series in @($item.series)) {
$index = [int]$series.index
$chartSeries = $chart.SeriesCollection($index)
if ($series.PSObject.Properties.Name -contains "name") {
try { $chartSeries.Name = [string]$series.name } catch {}
}
if ($series.PSObject.Properties.Name -contains "x_values") {
try { $chartSeries.XValues = @($series.x_values) } catch {}
}
if ($series.PSObject.Properties.Name -contains "values") {
try { $chartSeries.Values = @($series.values) } catch {}
}
}
try { $chart.Refresh() | Out-Null } catch {}
}
$pres.Save() $pres.Save()
Write-Output $OutputPath Write-Output $OutputPath
} finally { } finally {
......
...@@ -77,20 +77,54 @@ def resolve_report_period(args: argparse.Namespace, config: dict[str, Any]) -> t ...@@ -77,20 +77,54 @@ def resolve_report_period(args: argparse.Namespace, config: dict[str, Any]) -> t
return report_month, report_year, compare_year return report_month, report_year, compare_year
def build_filters(report_month: str, report_year: int) -> list[dict[str, Any]]: def resolve_month_index(report_month: str) -> int:
"""将月份标签转换成 1-12;无法识别时默认返回 1。"""
month_index_map = {
"一月": 1,
"二月": 2,
"三月": 3,
"四月": 4,
"五月": 5,
"六月": 6,
"七月": 7,
"八月": 8,
"九月": 9,
"十月": 10,
"十一月": 11,
"十二月": 12,
}
normalized = normalize_month_label(report_month)
if normalized in month_index_map:
return month_index_map[normalized]
match = re.fullmatch(r"0?([1-9]|1[0-2])", str(report_month).strip())
if match:
return int(match.group(1))
return 1
def build_filters(report_month: str, report_year: int, compare_year: int | None = None) -> list[dict[str, Any]]:
"""构造尽可能多的 caption 过滤字段,失败会在 JS 侧被吞掉,避免抛错。""" """构造尽可能多的 caption 过滤字段,失败会在 JS 侧被吞掉,避免抛错。"""
# billdate 年筛选按“报告年 + 上一年”动态计算,避免硬编码固定年份。
resolved_compare_year = report_year - 1 if compare_year is None else compare_year
year_values = [str(resolved_compare_year), str(report_year)]
# month 按累计区间筛选:例如三月 => 1,2,3。
month_index = resolve_month_index(report_month)
month_values_numeric_str = [str(idx) for idx in range(1, month_index + 1)]
return [ return [
{"field": "Storename (组)", "values": ["CKC-VIP"]}, {"field": "Storename (组)", "values": ["ckc-vip"]},
{"field": "storename (组)", "values": ["CKC-VIP"]}, {"field": "storename (组)", "values": ["ckc-vip"]},
{"field": "Storename", "values": ["CKC-VIP"]}, {"field": "Storename", "values": ["ckc-vip"]},
{"field": "storename", "values": ["CKC-VIP"]}, {"field": "storename", "values": ["ckc-vip"]},
{"field": "Month", "values": [report_month]}, {"field": "Month", "values": month_values_numeric_str},
{"field": "月(billdate)", "values": [report_month]}, {"field": "month", "values": month_values_numeric_str},
{"field": "billdate 月", "values": [report_month]}, {"field": "月(billdate)", "values": month_values_numeric_str},
{"field": "Year", "values": [str(report_year)]}, {"field": "billdate 月", "values": month_values_numeric_str},
{"field": "year", "values": [str(report_year)]}, {"field": "月", "values": month_values_numeric_str},
{"field": "年(billdate)", "values": [str(report_year)]}, {"field": "Year", "values": year_values},
{"field": "billdate 年", "values": [str(report_year)]}, {"field": "year", "values": year_values},
{"field": "年(billdate)", "values": year_values},
{"field": "billdate 年", "values": year_values},
{"field": "Brand", "values": ["CK"]}, {"field": "Brand", "values": ["CK"]},
{"field": "brand", "values": ["CK"]}, {"field": "brand", "values": ["CK"]},
] ]
...@@ -98,7 +132,7 @@ def build_filters(report_month: str, report_year: int) -> list[dict[str, Any]]: ...@@ -98,7 +132,7 @@ def build_filters(report_month: str, report_year: int) -> list[dict[str, Any]]:
def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[str, Any]: def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[str, Any]:
"""返回 S04-S08 需要的 capture/asset 描述,包含裁切信息与说明。""" """返回 S04-S08 需要的 capture/asset 描述,包含裁切信息与说明。"""
filters = build_filters(report_month, report_year) filters = build_filters(report_month, report_year, compare_year)
capture_template = [ capture_template = [
{ {
"capture_id": "overall", "capture_id": "overall",
...@@ -151,7 +185,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[ ...@@ -151,7 +185,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"inner_frame_fragment": info["inner_frame_fragment"], "inner_frame_fragment": info["inner_frame_fragment"],
"activate_sheet": info["activate_sheet"], "activate_sheet": info["activate_sheet"],
"filters": filters, "filters": filters,
"params": {}, "params": {
"year2": compare_year,
"Year2": compare_year,
},
"raw_screenshot_name": info["raw_screenshot_name"], "raw_screenshot_name": info["raw_screenshot_name"],
"note": info["note"], "note": info["note"],
} }
...@@ -165,10 +202,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[ ...@@ -165,10 +202,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_id": 2, "shape_id": 2,
"asset_name": "s04_category_overall_top", "asset_name": "s04_category_overall_top",
"capture_id": "overall", "capture_id": "overall",
"crop": {"left": 0, "top": 120, "width": 1400, "height": 460}, "crop": {"left": 0, "top": 110, "width": 1400, "height": 240},
"resize_to": {"width": 948, "height": 208}, "resize_to": {"width": 948, "height": 208},
"source_view": "Overall", "source_view": "Overall",
"note": "S04 上半部分,保留 Overall 主要图表。", "note": "S04 第一行:Sales LFL 与 YTM。",
}, },
{ {
"slide_code": "S04", "slide_code": "S04",
...@@ -177,10 +214,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[ ...@@ -177,10 +214,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_id": 4, "shape_id": 4,
"asset_name": "s04_category_overall_mid", "asset_name": "s04_category_overall_mid",
"capture_id": "overall", "capture_id": "overall",
"crop": {"left": 0, "top": 580, "width": 1400, "height": 420}, "crop": {"left": 0, "top": 610, "width": 1400, "height": 250},
"resize_to": {"width": 948, "height": 207}, "resize_to": {"width": 948, "height": 207},
"source_view": "Overall", "source_view": "Overall",
"note": "S04 中段图表,用于突出 category 列表。", "note": "S04 第二行:Category Contributions。",
}, },
{ {
"slide_code": "S04", "slide_code": "S04",
...@@ -189,10 +226,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[ ...@@ -189,10 +226,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_id": 5, "shape_id": 5,
"asset_name": "s04_category_overall_bottom", "asset_name": "s04_category_overall_bottom",
"capture_id": "overall", "capture_id": "overall",
"crop": {"left": 0, "top": 1010, "width": 1400, "height": 510}, "crop": {"left": 0, "top": 1590, "width": 1400, "height": 400},
"resize_to": {"width": 948, "height": 310}, "resize_to": {"width": 948, "height": 310},
"source_view": "Overall", "source_view": "Overall",
"note": "S04 下段,用于强调趋势或列表。", "note": "S04 第三/四行:ASP LFL/GP Difference + PD Difference/Regular Qty% Difference。",
}, },
{ {
"slide_code": "S05", "slide_code": "S05",
...@@ -201,10 +238,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[ ...@@ -201,10 +238,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_id": 2, "shape_id": 2,
"asset_name": "s05_bags_top", "asset_name": "s05_bags_top",
"capture_id": "bags", "capture_id": "bags",
"crop": {"left": 0, "top": 130, "width": 1400, "height": 360}, "crop": {"left": 0, "top": 175, "width": 1400, "height": 250},
"resize_to": {"width": 1093, "height": 237}, "resize_to": {"width": 1093, "height": 237},
"source_view": "Bags", "source_view": "Bags",
"note": "S05 上部图表对齐模板宽度。", "note": "S05 第一行:Sales LFL 与 YTM。",
}, },
{ {
"slide_code": "S05", "slide_code": "S05",
...@@ -213,10 +250,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[ ...@@ -213,10 +250,10 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_id": 3, "shape_id": 3,
"asset_name": "s05_bags_bottom", "asset_name": "s05_bags_bottom",
"capture_id": "bags", "capture_id": "bags",
"crop": {"left": 0, "top": 520, "width": 1400, "height": 500}, "crop": {"left": 0, "top": 1400, "width": 1400, "height": 420},
"resize_to": {"width": 1093, "height": 376}, "resize_to": {"width": 1093, "height": 376},
"source_view": "Bags", "source_view": "Bags",
"note": "S05 下段清晰呈现 bags 分类细节。", "note": "S05 第二/三行:Bags Qty%/ASP 与 Bags GP/PD。",
}, },
{ {
"slide_code": "S06", "slide_code": "S06",
...@@ -372,7 +409,16 @@ def build_configure_view_js(spec: dict[str, Any]) -> str: ...@@ -372,7 +409,16 @@ def build_configure_view_js(spec: dict[str, Any]) -> str:
if (activeSheet && typeof activeSheet.getWorksheets === 'function') {{ if (activeSheet && typeof activeSheet.getWorksheets === 'function') {{
worksheets = activeSheet.getWorksheets(); worksheets = activeSheet.getWorksheets();
}} }}
const targets = worksheets.length ? worksheets : [activeSheet]; // 先对 activeSheet(Dashboard)应用筛选,再回退到内部 worksheet。
const targets = [];
if (activeSheet) {{
targets.push(activeSheet);
}}
for (const worksheet of worksheets) {{
if (!targets.includes(worksheet)) {{
targets.push(worksheet);
}}
}}
const updateType = window.tableau.FilterUpdateType.REPLACE; const updateType = window.tableau.FilterUpdateType.REPLACE;
const filterApply = []; const filterApply = [];
...@@ -566,6 +612,11 @@ def parse_args() -> argparse.Namespace: ...@@ -566,6 +612,11 @@ def parse_args() -> argparse.Namespace:
default=0, default=0,
help="需要传给 Tableau 年份参数 year2", help="需要传给 Tableau 年份参数 year2",
) )
parser.add_argument(
"--disable-template-lock",
action="store_true",
help="禁用基线模板锁定,强制走 Tableau 实时抓取。",
)
return parser.parse_args() return parser.parse_args()
...@@ -666,7 +717,11 @@ def main() -> None: ...@@ -666,7 +717,11 @@ def main() -> None:
captures_by_id = {item["capture_id"]: item for item in specs["captures"]} captures_by_id = {item["capture_id"]: item for item in specs["captures"]}
required_capture_ids = collect_required_capture_ids(filtered_assets) required_capture_ids = collect_required_capture_ids(filtered_assets)
use_template_lock = should_use_template_lock(report_month, report_year, compare_year) use_template_lock = (
False
if args.disable_template_lock
else should_use_template_lock(report_month, report_year, compare_year)
)
raw_screenshots: dict[str, Path] = {} raw_screenshots: dict[str, Path] = {}
if not use_template_lock: if not use_template_lock:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment