Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
S
skills
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
allen.wang
skills
Commits
d90a2b4d
Commit
d90a2b4d
authored
Apr 13, 2026
by
allen.wang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat:init
parent
89ec371c
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
460 additions
and
226 deletions
+460
-226
vip-report-top-products-sync.ps1
vip-report/bin/vip-report-top-products-sync.ps1
+7
-1
sync_inventory_monthly_assets.py
vip-report/scripts/sync_inventory_monthly_assets.py
+219
-45
sync_top_products_assets.py
vip-report/scripts/sync_top_products_assets.py
+234
-180
No files found.
vip-report/bin/vip-report-top-products-sync.ps1
View file @
d90a2b4d
param
(
param
(
[
string
]
$ConfigPath
=
"C:\Users\niuniu\.codex\vip-report\config.yaml"
,
[
string
]
$ConfigPath
=
"C:\Users\niuniu\.codex\vip-report\config.yaml"
,
[
string
]
$Slides
=
"S10"
,
[
string
]
$Slides
=
"S
09,S
10"
,
[
string
]
$ReportMonth
=
""
,
[
string
]
$ReportMonth
=
""
,
[
int
]
$ReportYear
=
0,
[
int
]
$ReportYear
=
0,
[
int
]
$CompareYear
=
0,
[
int
]
$CompareYear
=
0,
...
@@ -40,10 +40,16 @@ if ($CompareYear -gt 0) {
...
@@ -40,10 +40,16 @@ if ($CompareYear -gt 0) {
}
}
python @pythonArgs
python @pythonArgs
if
(
$LASTEXITCODE
-ne 0
)
{
throw
"sync_top_products_assets.py failed with exit code
$LASTEXITCODE
"
}
if
(
$Render
)
{
if
(
$Render
)
{
powershell -ExecutionPolicy Bypass -File
"
$root
\bin\vip-report-render.ps1"
-TemplatePath
"
$TemplatePath
"
-OutputPath
"
$OutputPath
"
-OperationsPath
"
$opsPath
"
| Out-Null
powershell -ExecutionPolicy Bypass -File
"
$root
\bin\vip-report-render.ps1"
-TemplatePath
"
$TemplatePath
"
-OutputPath
"
$OutputPath
"
-OperationsPath
"
$opsPath
"
| Out-Null
python
"
$root
\scripts\compare_pptx.py"
"
$TemplatePath
"
"
$OutputPath
"
--output
"
$CompareOutputPath
"
python
"
$root
\scripts\compare_pptx.py"
"
$TemplatePath
"
"
$OutputPath
"
--output
"
$CompareOutputPath
"
if
(
$LASTEXITCODE
-ne 0
)
{
throw
"compare_pptx.py failed with exit code
$LASTEXITCODE
"
}
Write-Output
$OutputPath
Write-Output
$OutputPath
Write-Output
$CompareOutputPath
Write-Output
$CompareOutputPath
}
else
{
}
else
{
...
...
vip-report/scripts/sync_inventory_monthly_assets.py
View file @
d90a2b4d
...
@@ -122,6 +122,7 @@ def build_filters(
...
@@ -122,6 +122,7 @@ def build_filters(
include_compare_year
:
bool
=
True
,
include_compare_year
:
bool
=
True
,
cumulative_month
:
bool
=
True
,
cumulative_month
:
bool
=
True
,
storename_value_candidates
:
list
[
list
[
str
]]
|
None
=
None
,
storename_value_candidates
:
list
[
list
[
str
]]
|
None
=
None
,
storename_field_candidates
:
list
[
str
]
|
None
=
None
,
)
->
list
[
dict
[
str
,
Any
]]:
)
->
list
[
dict
[
str
,
Any
]]:
"""构造 inventory monthly 的筛选候选,命中 worksheet 后停止继续尝试。"""
"""构造 inventory monthly 的筛选候选,命中 worksheet 后停止继续尝试。"""
# billdate 年筛选按“报告年 + 上一年”动态计算,避免硬编码固定年份。
# billdate 年筛选按“报告年 + 上一年”动态计算,避免硬编码固定年份。
...
@@ -136,21 +137,18 @@ def build_filters(
...
@@ -136,21 +137,18 @@ def build_filters(
cumulative
=
cumulative_month
,
cumulative
=
cumulative_month
,
)
)
storename_candidates
=
storename_value_candidates
or
[[
"CKC-VIP"
],
[
"ckc-vip"
]]
storename_candidates
=
storename_value_candidates
or
[[
"CKC-VIP"
],
[
"ckc-vip"
]]
storename_fields
=
storename_field_candidates
or
[
return
[
{
"label"
:
"storename"
,
"field_candidates"
:
[
"storename (Shoes) "
,
"Storename (Shoes) "
,
"storename (Shoes)"
,
"Storename (Shoes)"
,
"storename"
,
"storename"
,
"Storename"
,
"Storename"
,
"storecode"
,
"storecode"
,
"storename (组)"
,
"storename (组)"
,
"Storename (组)"
,
"Storename (组)"
,
],
]
return
[
{
"label"
:
"storename"
,
"field_candidates"
:
storename_fields
,
"value_candidates"
:
storename_candidates
,
"value_candidates"
:
storename_candidates
,
"require_worksheet_match"
:
False
,
"require_worksheet_match"
:
False
,
},
},
...
@@ -162,7 +160,7 @@ def build_filters(
...
@@ -162,7 +160,7 @@ def build_filters(
},
},
{
{
"label"
:
"brand"
,
"label"
:
"brand"
,
"field_candidates"
:
[
"Brand"
,
"brand"
],
"field_candidates"
:
[
"Brand
(Shoes)"
,
"Brand
"
,
"brand"
],
"value_candidates"
:
[[
"CK"
]],
"value_candidates"
:
[[
"CK"
]],
"require_worksheet_match"
:
False
,
"require_worksheet_match"
:
False
,
},
},
...
@@ -180,14 +178,21 @@ def build_subcategory_filters(
...
@@ -180,14 +178,21 @@ def build_subcategory_filters(
report_year
:
int
,
report_year
:
int
,
compare_year
:
int
|
None
=
None
,
compare_year
:
int
|
None
=
None
,
)
->
list
[
dict
[
str
,
Any
]]:
)
->
list
[
dict
[
str
,
Any
]]:
"""S08
需要按报告月精确
筛选,并限定 category/storename。"""
"""S08
按统一年月
筛选,并限定 category/storename。"""
filters
=
build_filters
(
filters
=
build_filters
(
report_month
,
report_month
,
report_year
,
report_year
,
compare_year
,
compare_year
,
include_compare_year
=
False
,
include_compare_year
=
True
,
cumulative_month
=
False
,
cumulative_month
=
True
,
storename_value_candidates
=
[[
"ckc-vip"
],
[
"CKC-VIP"
]],
storename_value_candidates
=
[[
"CKC-VIP"
],
[
"ckc-vip"
]],
storename_field_candidates
=
[
"storename"
,
"Storename"
,
"storecode"
,
"storename (组)"
,
"Storename (组)"
,
],
)
)
for
item
in
filters
:
for
item
in
filters
:
if
item
.
get
(
"label"
)
==
"brand"
:
if
item
.
get
(
"label"
)
==
"brand"
:
...
@@ -216,16 +221,78 @@ def build_subcategory_filters(
...
@@ -216,16 +221,78 @@ def build_subcategory_filters(
return
filters
return
filters
def
build_bags_filters
(
report_month
:
str
,
report_year
:
int
,
compare_year
:
int
|
None
=
None
,
)
->
list
[
dict
[
str
,
Any
]]:
"""S05 需要严格命中 worksheet 级的年月/门店筛选。"""
filters
=
build_filters
(
report_month
,
report_year
,
compare_year
,
include_compare_year
=
True
,
cumulative_month
=
True
,
storename_value_candidates
=
[[
"CKC-VIP"
],
[
"ckc-vip"
]],
storename_field_candidates
=
[
"storename"
,
"Storename"
,
"storecode"
,
"storename (组)"
,
"Storename (组)"
,
],
)
for
item
in
filters
:
if
item
.
get
(
"label"
)
==
"storename"
:
item
[
"require_worksheet_match"
]
=
True
if
item
.
get
(
"label"
)
in
{
"storename"
,
"year"
,
"month"
}:
item
[
"apply_all_fields"
]
=
True
return
filters
def
build_shoes_filters
(
report_month
:
str
,
report_year
:
int
,
compare_year
:
int
|
None
=
None
,
)
->
list
[
dict
[
str
,
Any
]]:
"""S06 需要命中 Shoes 专属门店字段,其余筛选允许 dashboard 级联动。"""
filters
=
build_filters
(
report_month
,
report_year
,
compare_year
,
include_compare_year
=
True
,
cumulative_month
=
True
,
storename_value_candidates
=
[[
"CKC-VIP"
],
[
"ckc-vip"
]],
storename_field_candidates
=
[
"storename (Shoes) "
,
"Storename (Shoes) "
,
"storename (Shoes)"
,
"Storename (Shoes)"
,
"storename"
,
"Storename"
,
"storecode"
,
"storename (组)"
,
"Storename (组)"
,
],
)
for
item
in
filters
:
if
item
.
get
(
"label"
)
==
"storename"
:
item
[
"require_worksheet_match"
]
=
True
return
filters
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
,
compare_year
)
filters
=
build_filters
(
report_month
,
report_year
,
compare_year
)
s05_filters
=
build_bags_filters
(
report_month
,
report_year
,
compare_year
)
s06_filters
=
build_shoes_filters
(
report_month
,
report_year
,
compare_year
)
s07_filters
=
build_filters
(
s07_filters
=
build_filters
(
report_month
,
report_month
,
report_year
,
report_year
,
compare_year
,
compare_year
,
include_compare_year
=
Fals
e
,
include_compare_year
=
Tru
e
,
cumulative_month
=
Fals
e
,
cumulative_month
=
Tru
e
,
storename_value_candidates
=
[[
"
ckc-vip"
],
[
"CKC-VIP
"
]],
storename_value_candidates
=
[[
"
CKC-VIP"
],
[
"ckc-vip
"
]],
)
)
s08_filters
=
build_subcategory_filters
(
report_month
,
report_year
,
compare_year
)
s08_filters
=
build_subcategory_filters
(
report_month
,
report_year
,
compare_year
)
capture_template
=
[
capture_template
=
[
...
@@ -238,20 +305,56 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
...
@@ -238,20 +305,56 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"raw_screenshot_name"
:
"inventory-s04-overall.png"
,
"raw_screenshot_name"
:
"inventory-s04-overall.png"
,
},
},
{
{
"capture_id"
:
"bags"
,
"capture_id"
:
"bags_top"
,
"hash_url"
:
"#/views/CKInventoryMonthlyReport/Bags?:iid=1"
,
"inner_frame_fragment"
:
"/views/CKInventoryMonthlyReport/Bags?"
,
"activate_sheet"
:
"Bags"
,
"filters"
:
s05_filters
,
"capture_mode"
:
"page"
,
"viewport"
:
{
"width"
:
1280
,
"height"
:
720
},
"capture_wait_ms"
:
3000
,
"note"
:
"S05 Bags 顶部区域,使用页面截图保留 Sales/YTM 图表。"
,
"raw_screenshot_name"
:
"inventory-s05-bags-page-top.png"
,
},
{
"capture_id"
:
"bags_bottom"
,
"hash_url"
:
"#/views/CKInventoryMonthlyReport/Bags?:iid=1"
,
"hash_url"
:
"#/views/CKInventoryMonthlyReport/Bags?:iid=1"
,
"inner_frame_fragment"
:
"/views/CKInventoryMonthlyReport/Bags?"
,
"inner_frame_fragment"
:
"/views/CKInventoryMonthlyReport/Bags?"
,
"activate_sheet"
:
"Bags"
,
"activate_sheet"
:
"Bags"
,
"note"
:
"S05 Bags 视图,仅展示包袋分类信息。"
,
"filters"
:
s05_filters
,
"raw_screenshot_name"
:
"inventory-s05-bags.png"
,
"capture_mode"
:
"page"
,
"viewport"
:
{
"width"
:
1280
,
"height"
:
720
},
"scroll_selector"
:
"#dashboard-viewport"
,
"scroll_top"
:
1160
,
"capture_wait_ms"
:
3000
,
"note"
:
"S05 Bags 下半部分,滚动 dashboard-viewport 后截图。"
,
"raw_screenshot_name"
:
"inventory-s05-bags-page-bottom.png"
,
},
{
"capture_id"
:
"shoes_top"
,
"hash_url"
:
"#/views/CKInventoryMonthlyReport/Shoes?:iid=1"
,
"inner_frame_fragment"
:
"/views/CKInventoryMonthlyReport/Shoes?"
,
"activate_sheet"
:
"Shoes"
,
"filters"
:
s06_filters
,
"capture_mode"
:
"page"
,
"viewport"
:
{
"width"
:
1280
,
"height"
:
720
},
"capture_wait_ms"
:
3000
,
"note"
:
"S06 Shoes 顶部区域,使用页面截图保留 Sales/YTM 图表。"
,
"raw_screenshot_name"
:
"inventory-s06-shoes-page-top.png"
,
},
},
{
{
"capture_id"
:
"shoes"
,
"capture_id"
:
"shoes
_bottom
"
,
"hash_url"
:
"#/views/CKInventoryMonthlyReport/Shoes?:iid=1"
,
"hash_url"
:
"#/views/CKInventoryMonthlyReport/Shoes?:iid=1"
,
"inner_frame_fragment"
:
"/views/CKInventoryMonthlyReport/Shoes?"
,
"inner_frame_fragment"
:
"/views/CKInventoryMonthlyReport/Shoes?"
,
"activate_sheet"
:
"Shoes"
,
"activate_sheet"
:
"Shoes"
,
"note"
:
"S06 Shoes 视图,展示鞋类趋势。"
,
"filters"
:
s06_filters
,
"raw_screenshot_name"
:
"inventory-s06-shoes.png"
,
"capture_mode"
:
"page"
,
"viewport"
:
{
"width"
:
1280
,
"height"
:
720
},
"scroll_selector"
:
"#dashboard-viewport"
,
"scroll_top"
:
1180
,
"capture_wait_ms"
:
3000
,
"note"
:
"S06 Shoes 下半部分,滚动 dashboard-viewport 后截图。"
,
"raw_screenshot_name"
:
"inventory-s06-shoes-page-bottom.png"
,
},
},
{
{
"capture_id"
:
"discount_regular"
,
"capture_id"
:
"discount_regular"
,
...
@@ -291,6 +394,16 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
...
@@ -291,6 +394,16 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"raw_screenshot_name"
:
info
[
"raw_screenshot_name"
],
"raw_screenshot_name"
:
info
[
"raw_screenshot_name"
],
"note"
:
info
[
"note"
],
"note"
:
info
[
"note"
],
}
}
for
optional_key
in
[
"capture_mode"
,
"viewport"
,
"ready_patterns"
,
"scroll_selector"
,
"scroll_top"
,
"capture_wait_ms"
,
]:
if
optional_key
in
info
:
spec
[
optional_key
]
=
info
[
optional_key
]
captures
.
append
(
spec
)
captures
.
append
(
spec
)
assets
=
[
assets
=
[
...
@@ -336,8 +449,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
...
@@ -336,8 +449,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_name"
:
"图片 1"
,
"shape_name"
:
"图片 1"
,
"shape_id"
:
2
,
"shape_id"
:
2
,
"asset_name"
:
"s05_bags_top"
,
"asset_name"
:
"s05_bags_top"
,
"capture_id"
:
"bags"
,
"capture_id"
:
"bags
_top
"
,
"crop"
:
{
"left"
:
100
,
"top"
:
128
,
"width"
:
1200
,
"height"
:
202
},
"crop"
:
{
"left"
:
55
,
"top"
:
175
,
"width"
:
1175
,
"height"
:
250
},
"resize_to"
:
{
"width"
:
1093
,
"height"
:
237
},
"resize_to"
:
{
"width"
:
1093
,
"height"
:
237
},
"source_view"
:
"Bags"
,
"source_view"
:
"Bags"
,
"note"
:
"S05 第一行:Sales LFL 与 YTM。"
,
"note"
:
"S05 第一行:Sales LFL 与 YTM。"
,
...
@@ -348,8 +461,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
...
@@ -348,8 +461,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_name"
:
"图片 2"
,
"shape_name"
:
"图片 2"
,
"shape_id"
:
3
,
"shape_id"
:
3
,
"asset_name"
:
"s05_bags_bottom"
,
"asset_name"
:
"s05_bags_bottom"
,
"capture_id"
:
"bags"
,
"capture_id"
:
"bags
_bottom
"
,
"crop"
:
{
"left"
:
100
,
"top"
:
1320
,
"width"
:
1200
,
"height"
:
365
},
"crop"
:
{
"left"
:
55
,
"top"
:
265
,
"width"
:
1175
,
"height"
:
400
},
"resize_to"
:
{
"width"
:
1093
,
"height"
:
376
},
"resize_to"
:
{
"width"
:
1093
,
"height"
:
376
},
"source_view"
:
"Bags"
,
"source_view"
:
"Bags"
,
"note"
:
"S05 第二/三行:Bags Qty
%
/ASP 与 Bags GP/PD。"
,
"note"
:
"S05 第二/三行:Bags Qty
%
/ASP 与 Bags GP/PD。"
,
...
@@ -360,8 +473,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
...
@@ -360,8 +473,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_name"
:
"图片 2"
,
"shape_name"
:
"图片 2"
,
"shape_id"
:
3
,
"shape_id"
:
3
,
"asset_name"
:
"s06_shoes_top"
,
"asset_name"
:
"s06_shoes_top"
,
"capture_id"
:
"shoes"
,
"capture_id"
:
"shoes
_top
"
,
"crop"
:
{
"left"
:
0
,
"top"
:
130
,
"width"
:
1400
,
"height"
:
36
0
},
"crop"
:
{
"left"
:
55
,
"top"
:
175
,
"width"
:
1175
,
"height"
:
25
0
},
"resize_to"
:
{
"width"
:
1132
,
"height"
:
248
},
"resize_to"
:
{
"width"
:
1132
,
"height"
:
248
},
"source_view"
:
"Shoes"
,
"source_view"
:
"Shoes"
,
"note"
:
"S06 第一行:Sales LFL 与 YTM。"
,
"note"
:
"S06 第一行:Sales LFL 与 YTM。"
,
...
@@ -372,8 +485,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
...
@@ -372,8 +485,8 @@ def build_specs(report_month: str, report_year: int, compare_year: int) -> dict[
"shape_name"
:
"图片 6"
,
"shape_name"
:
"图片 6"
,
"shape_id"
:
7
,
"shape_id"
:
7
,
"asset_name"
:
"s06_shoes_bottom"
,
"asset_name"
:
"s06_shoes_bottom"
,
"capture_id"
:
"shoes"
,
"capture_id"
:
"shoes
_bottom
"
,
"crop"
:
{
"left"
:
0
,
"top"
:
540
,
"width"
:
1400
,
"height"
:
520
},
"crop"
:
{
"left"
:
55
,
"top"
:
235
,
"width"
:
1175
,
"height"
:
405
},
"resize_to"
:
{
"width"
:
1132
,
"height"
:
388
},
"resize_to"
:
{
"width"
:
1132
,
"height"
:
388
},
"source_view"
:
"Shoes"
,
"source_view"
:
"Shoes"
,
"note"
:
"S06 第二/三行:Shoes Qty
%
Difference、Shoes ASP LFL、Shoes GP Difference、Shoes PD Difference。"
,
"note"
:
"S06 第二/三行:Shoes Qty
%
Difference、Shoes ASP LFL、Shoes GP Difference、Shoes PD Difference。"
,
...
@@ -566,9 +679,11 @@ def build_configure_view_js(spec: dict[str, Any]) -> str:
...
@@ -566,9 +679,11 @@ def build_configure_view_js(spec: dict[str, Any]) -> str:
const valueCandidates = filter.value_candidates || (filter.values ? [filter.values] : []);
const valueCandidates = filter.value_candidates || (filter.values ? [filter.values] : []);
const requireWorksheetMatch =
const requireWorksheetMatch =
(filter.require_worksheet_match !== false) && worksheets.length > 0;
(filter.require_worksheet_match !== false) && worksheets.length > 0;
const applyAllFields = !!filter.apply_all_fields;
let matched = false;
let matched = false;
for (const field of fieldCandidates) {{
for (const field of fieldCandidates) {{
let fieldMatched = false;
for (const values of valueCandidates) {{
for (const values of valueCandidates) {{
const result = await applyFilterCandidate(field, values);
const result = await applyFilterCandidate(field, values);
if (result.anySuccess && (!requireWorksheetMatch || result.anyWorksheetSuccess)) {{
if (result.anySuccess && (!requireWorksheetMatch || result.anyWorksheetSuccess)) {{
...
@@ -581,12 +696,16 @@ def build_configure_view_js(spec: dict[str, Any]) -> str:
...
@@ -581,12 +696,16 @@ def build_configure_view_js(spec: dict[str, Any]) -> str:
matched_on_worksheet: result.anyWorksheetSuccess,
matched_on_worksheet: result.anyWorksheetSuccess,
}});
}});
matched = true;
matched = true;
fieldMatched = true;
break;
break;
}}
}}
}}
}}
if (
matched
) {{
if (
fieldMatched && !applyAllFields
) {{
break;
break;
}}
}}
if (fieldMatched) {{
continue;
}}
}}
}}
if (!matched) {{
if (!matched) {{
...
@@ -629,14 +748,22 @@ def build_capture_js(
...
@@ -629,14 +748,22 @@ def build_capture_js(
*
,
*
,
ready_patterns
:
list
[
str
]
|
None
=
None
,
ready_patterns
:
list
[
str
]
|
None
=
None
,
viewport
:
dict
[
str
,
int
]
|
None
=
None
,
viewport
:
dict
[
str
,
int
]
|
None
=
None
,
capture_mode
:
str
=
"frame_body"
,
scroll_selector
:
str
|
None
=
None
,
scroll_top
:
int
|
None
=
None
,
capture_wait_ms
:
int
=
3000
,
)
->
str
:
)
->
str
:
"""
找到 inner frame,截取整个 body 作为截图
。"""
"""
支持 frame-body 与 page 两种截图模式
。"""
payload
=
json
.
dumps
(
payload
=
json
.
dumps
(
{
{
"inner_frame_fragment"
:
inner_frame_fragment
,
"inner_frame_fragment"
:
inner_frame_fragment
,
"screenshot_name"
:
screenshot_name
,
"screenshot_name"
:
screenshot_name
,
"ready_patterns"
:
ready_patterns
or
[],
"ready_patterns"
:
ready_patterns
or
[],
"viewport"
:
viewport
or
{},
"viewport"
:
viewport
or
{},
"capture_mode"
:
capture_mode
,
"scroll_selector"
:
scroll_selector
,
"scroll_top"
:
scroll_top
,
"capture_wait_ms"
:
capture_wait_ms
,
},
},
ensure_ascii
=
False
,
ensure_ascii
=
False
,
)
)
...
@@ -664,12 +791,28 @@ def build_capture_js(
...
@@ -664,12 +791,28 @@ def build_capture_js(
{{ timeout: 60000 }}
{{ timeout: 60000 }}
);
);
}}
}}
await page.waitForTimeout(3000);
if (frame && spec.scroll_selector) {{
await frame.evaluate((payload) => {{
const node = document.querySelector(payload.selector);
if (!node) {{
throw new Error(`Scroll target not found: ${{payload.selector}}`);
}}
node.scrollTop = Number(payload.scrollTop || 0);
}}, {{ selector: spec.scroll_selector, scrollTop: spec.scroll_top || 0 }});
}}
await page.waitForTimeout(Number(spec.capture_wait_ms || 3000));
if (spec.capture_mode === 'page') {{
await page.screenshot({{
path: spec.screenshot_name,
scale: 'css',
}});
}} else {{
const body = await target.$('body');
const body = await target.$('body');
await body.screenshot({{
await body.screenshot({{
path: spec.screenshot_name,
path: spec.screenshot_name,
scale: 'css',
scale: 'css',
}});
}});
}}
return {{
return {{
frameUrl: frame ? frame.url() : null,
frameUrl: frame ? frame.url() : null,
pageUrl: page.url(),
pageUrl: page.url(),
...
@@ -763,6 +906,30 @@ def crop_image(
...
@@ -763,6 +906,30 @@ def crop_image(
result
.
save
(
target
)
result
.
save
(
target
)
def
wait_for_capture_file
(
filename
:
str
,
*
,
workdir
:
Path
,
workspace_root
:
Path
,
session
:
str
,
timeout_seconds
:
int
=
45
,
)
->
Path
:
"""等待 Playwright 截图真正落盘,避免 run-code 返回早于文件可见。"""
deadline
=
time
.
time
()
+
timeout_seconds
local_output
=
workdir
/
filename
while
time
.
time
()
<
deadline
:
if
local_output
.
exists
():
return
local_output
try
:
located
=
locate_session_file
(
workspace_root
,
session
,
filename
)
if
located
.
exists
():
return
located
except
FileNotFoundError
:
pass
time
.
sleep
(
1
)
raise
FileNotFoundError
(
f
"Unable to locate {filename} under {workspace_root}."
)
def
parse_args
()
->
argparse
.
Namespace
:
def
parse_args
()
->
argparse
.
Namespace
:
parser
=
argparse
.
ArgumentParser
(
parser
=
argparse
.
ArgumentParser
(
description
=
"采集 CK Inventory Monthly 的 S04-S08 Tableau 视图素材。"
description
=
"采集 CK Inventory Monthly 的 S04-S08 Tableau 视图素材。"
...
@@ -844,6 +1011,10 @@ def capture_tableau_view(
...
@@ -844,6 +1011,10 @@ def capture_tableau_view(
capture_spec
[
"raw_screenshot_name"
],
capture_spec
[
"raw_screenshot_name"
],
ready_patterns
=
capture_spec
.
get
(
"ready_patterns"
),
ready_patterns
=
capture_spec
.
get
(
"ready_patterns"
),
viewport
=
capture_spec
.
get
(
"viewport"
,
VIEWPORT
),
viewport
=
capture_spec
.
get
(
"viewport"
,
VIEWPORT
),
capture_mode
=
capture_spec
.
get
(
"capture_mode"
,
"frame_body"
),
scroll_selector
=
capture_spec
.
get
(
"scroll_selector"
),
scroll_top
=
capture_spec
.
get
(
"scroll_top"
),
capture_wait_ms
=
int
(
capture_spec
.
get
(
"capture_wait_ms"
,
3000
)),
),
),
)
)
...
@@ -876,10 +1047,13 @@ def capture_tableau_view(
...
@@ -876,10 +1047,13 @@ def capture_tableau_view(
configure_script
.
unlink
(
missing_ok
=
True
)
configure_script
.
unlink
(
missing_ok
=
True
)
capture_script
.
unlink
(
missing_ok
=
True
)
capture_script
.
unlink
(
missing_ok
=
True
)
local_output
=
workdir
/
capture_spec
[
"raw_screenshot_name"
]
return
wait_for_capture_file
(
if
local_output
.
exists
():
capture_spec
[
"raw_screenshot_name"
],
return
local_output
workdir
=
workdir
,
return
locate_session_file
(
workspace_root
,
session
,
capture_spec
[
"raw_screenshot_name"
])
workspace_root
=
workspace_root
,
session
=
session
,
timeout_seconds
=
45
,
)
def
main
()
->
None
:
def
main
()
->
None
:
...
...
vip-report/scripts/sync_top_products_assets.py
View file @
d90a2b4d
...
@@ -2,6 +2,7 @@ from __future__ import annotations
...
@@ -2,6 +2,7 @@ from __future__ import annotations
import
argparse
import
argparse
import
calendar
import
calendar
import
datetime
import
json
import
json
import
re
import
re
import
subprocess
import
subprocess
...
@@ -36,8 +37,47 @@ MONTH_CN_MAP = {
...
@@ -36,8 +37,47 @@ MONTH_CN_MAP = {
12
:
"
\u5341\u4e8c\u6708
"
,
12
:
"
\u5341\u4e8c\u6708
"
,
}
}
MONTH_INDEX_MAP
=
{
label
:
index
for
index
,
label
in
MONTH_CN_MAP
.
items
()}
MONTH_INDEX_MAP
=
{
label
:
index
for
index
,
label
in
MONTH_CN_MAP
.
items
()}
S10_PRODUCT_IMAGE_URL
=
"https://api.charleskeith.cn/img/{articlecode}.jpg"
PRODUCT_IMAGE_URL
=
"https://api.charleskeith.cn/img/{articlecode}.jpg"
S10_PRODUCT_SLOTS
=
[
SLIDE_SPECS
:
dict
[
str
,
dict
[
str
,
Any
]]
=
{
"S09"
:
{
"category_filter"
:
"Bags"
,
"level1"
:
"BAGS"
,
"capture_id"
:
"top_products_bags"
,
"slide"
:
9
,
"shape_id"
:
3
,
"shape_name"
:
"Picture 2"
,
"asset_name"
:
"s09_top_products_chart"
,
"raw_screenshot_name"
:
"top-products-bags.png"
,
"resize_to"
:
{
"width"
:
1034
,
"height"
:
587
},
"crop"
:
{
"left"
:
0
,
"top"
:
180
,
"width"
:
1050
,
"height"
:
600
},
"note"
:
"S09 main Tableau chart area."
,
"product_slots"
:
[
{
"rank"
:
1
,
"shape_id"
:
4
,
"shape_name"
:
"Picture 2"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
2
,
"shape_id"
:
1042
,
"shape_name"
:
"Picture 18"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
3
,
"shape_id"
:
5
,
"shape_name"
:
"Picture 4"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
4
,
"shape_id"
:
1034
,
"shape_name"
:
"Picture 10"
,
"canvas_width"
:
707
,
"canvas_height"
:
472
},
{
"rank"
:
5
,
"shape_id"
:
1038
,
"shape_name"
:
"Picture 14"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
6
,
"shape_id"
:
1036
,
"shape_name"
:
"Picture 12"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
7
,
"shape_id"
:
1032
,
"shape_name"
:
"Picture 8"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
8
,
"shape_id"
:
7
,
"shape_name"
:
"Picture 8"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
9
,
"shape_id"
:
6
,
"shape_name"
:
"Picture 6"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
10
,
"shape_id"
:
8
,
"shape_name"
:
"Picture 10"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
],
},
"S10"
:
{
"category_filter"
:
"Shoes"
,
"level1"
:
"SHOES"
,
"capture_id"
:
"top_products_shoes"
,
"slide"
:
10
,
"shape_id"
:
2
,
"shape_name"
:
"Picture 1"
,
"asset_name"
:
"s10_top_products_chart"
,
"raw_screenshot_name"
:
"top-products-shoes.png"
,
"resize_to"
:
{
"width"
:
1039
,
"height"
:
585
},
"crop"
:
{
"left"
:
0
,
"top"
:
120
,
"width"
:
1400
,
"height"
:
900
},
"note"
:
"S10 main Tableau chart area."
,
"product_slots"
:
[
{
"rank"
:
1
,
"shape_id"
:
3
,
"shape_name"
:
"Picture 2"
,
"canvas_width"
:
708
,
"canvas_height"
:
500
},
{
"rank"
:
1
,
"shape_id"
:
3
,
"shape_name"
:
"Picture 2"
,
"canvas_width"
:
708
,
"canvas_height"
:
500
},
{
"rank"
:
2
,
"shape_id"
:
4
,
"shape_name"
:
"Picture 2"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
2
,
"shape_id"
:
4
,
"shape_name"
:
"Picture 2"
,
"canvas_width"
:
707
,
"canvas_height"
:
500
},
{
"rank"
:
3
,
"shape_id"
:
6
,
"shape_name"
:
"Picture 4"
,
"canvas_width"
:
750
,
"canvas_height"
:
500
},
{
"rank"
:
3
,
"shape_id"
:
6
,
"shape_name"
:
"Picture 4"
,
"canvas_width"
:
750
,
"canvas_height"
:
500
},
...
@@ -48,7 +88,9 @@ S10_PRODUCT_SLOTS = [
...
@@ -48,7 +88,9 @@ S10_PRODUCT_SLOTS = [
{
"rank"
:
8
,
"shape_id"
:
11
,
"shape_name"
:
"Picture 14"
,
"canvas_width"
:
500
,
"canvas_height"
:
707
},
{
"rank"
:
8
,
"shape_id"
:
11
,
"shape_name"
:
"Picture 14"
,
"canvas_width"
:
500
,
"canvas_height"
:
707
},
{
"rank"
:
9
,
"shape_id"
:
13
,
"shape_name"
:
"Picture 16"
,
"canvas_width"
:
750
,
"canvas_height"
:
500
},
{
"rank"
:
9
,
"shape_id"
:
13
,
"shape_name"
:
"Picture 16"
,
"canvas_width"
:
750
,
"canvas_height"
:
500
},
{
"rank"
:
10
,
"shape_id"
:
14
,
"shape_name"
:
"Picture 18"
,
"canvas_width"
:
750
,
"canvas_height"
:
500
},
{
"rank"
:
10
,
"shape_id"
:
14
,
"shape_name"
:
"Picture 18"
,
"canvas_width"
:
750
,
"canvas_height"
:
500
},
]
],
},
}
def
normalize_month_label
(
raw_month
:
Any
)
->
str
:
def
normalize_month_label
(
raw_month
:
Any
)
->
str
:
...
@@ -68,9 +110,19 @@ def normalize_month_label(raw_month: Any) -> str:
...
@@ -68,9 +110,19 @@ def normalize_month_label(raw_month: Any) -> str:
def
resolve_month_index
(
report_month
:
str
)
->
int
:
def
resolve_month_index
(
report_month
:
str
)
->
int
:
normalized
=
normalize_month_label
(
report_month
)
normalized
=
normalize_month_label
(
report_month
)
if
normalized
in
MONTH_INDEX_MAP
:
if
normalized
not
in
MONTH_INDEX_MAP
:
return
MONTH_INDEX_MAP
[
normalized
]
raise
ValueError
(
f
"Unsupported report month: {report_month}"
)
raise
ValueError
(
f
"Unsupported report month: {report_month}"
)
return
MONTH_INDEX_MAP
[
normalized
]
def
build_month_week_labels
(
report_year
:
int
,
report_month
:
str
)
->
list
[
str
]:
month_index
=
resolve_month_index
(
report_month
)
last_day
=
calendar
.
monthrange
(
report_year
,
month_index
)[
1
]
week_numbers
=
{
datetime
.
date
(
report_year
,
month_index
,
day
)
.
isocalendar
()[
1
]
for
day
in
range
(
1
,
last_day
+
1
)
}
return
[
f
"W{week_number}"
for
week_number
in
sorted
(
week_numbers
)]
def
resolve_report_period
(
args
:
argparse
.
Namespace
,
config
:
dict
[
str
,
Any
])
->
tuple
[
str
,
int
,
int
]:
def
resolve_report_period
(
args
:
argparse
.
Namespace
,
config
:
dict
[
str
,
Any
])
->
tuple
[
str
,
int
,
int
]:
...
@@ -81,58 +133,48 @@ def resolve_report_period(args: argparse.Namespace, config: dict[str, Any]) -> t
...
@@ -81,58 +133,48 @@ def resolve_report_period(args: argparse.Namespace, config: dict[str, Any]) -> t
return
report_month
,
report_year
,
report_compare_year
return
report_month
,
report_year
,
report_compare_year
def
build_tableau_filters
(
report_month
:
str
,
report_year
:
int
)
->
list
[
dict
[
str
,
Any
]]:
def
parse_args
()
->
argparse
.
Namespace
:
parser
=
argparse
.
ArgumentParser
(
description
=
"Generate S09/S10 top-products assets for the VIP report."
)
parser
.
add_argument
(
"--config"
,
required
=
True
,
help
=
"Path to config.yaml"
)
parser
.
add_argument
(
"--slides"
,
default
=
"S09,S10"
,
help
=
"Slide codes to generate."
)
parser
.
add_argument
(
"--report-month"
,
help
=
"Report month, for example 3 / 03 / 三月"
)
parser
.
add_argument
(
"--report-year"
,
type
=
int
,
help
=
"Report year"
)
parser
.
add_argument
(
"--compare-year"
,
type
=
int
,
help
=
"Compare year"
)
return
parser
.
parse_args
()
def
build_filter_actions
(
report_month
:
str
,
report_year
:
int
,
slide_code
:
str
)
->
list
[
dict
[
str
,
Any
]]:
level1
=
SLIDE_SPECS
[
slide_code
][
"level1"
]
week_labels
=
build_month_week_labels
(
report_year
,
report_month
)
return
[
return
[
{
"field"
:
"Category"
,
"values"
:
[
"Shoes"
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"TOP"
,
"field"
:
"year"
,
"values"
:
[
str
(
report_year
)]},
{
"field"
:
"Year"
,
"values"
:
[
str
(
report_year
)]},
{
"kind"
:
"filter"
,
"worksheet"
:
"TOP"
,
"field"
:
"月(Daily)"
,
"values"
:
[
report_month
]},
{
"field"
:
"Month"
,
"values"
:
[
report_month
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"TOP"
,
"field"
:
"ISO周数(Daily)"
,
"values"
:
week_labels
},
{
"field"
:
"storename (group)"
,
"values"
:
[
"CKC-VIP"
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"category contributions"
,
"field"
:
"月(Daily)"
,
"values"
:
[
report_month
]},
{
"field"
:
"Storename (group)"
,
"values"
:
[
"CKC-VIP"
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"Soldqty Trend"
,
"field"
:
"Category"
,
"values"
:
[
level1
]},
{
"field"
:
"storename"
,
"values"
:
[
"CKC-VIP"
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"OVERALL TOP"
,
"field"
:
"Category"
,
"values"
:
[
level1
]},
{
"field"
:
"Storename"
,
"values"
:
[
"CKC-VIP"
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"TOP"
,
"field"
:
"Category"
,
"values"
:
[
level1
]},
{
"field"
:
"week"
,
"values"
:
[
ALL_FILTER_VALUE
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"Soldqty and SOH details"
,
"field"
:
"Category"
,
"values"
:
[
level1
]},
{
"field"
:
"Week"
,
"values"
:
[
ALL_FILTER_VALUE
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"category contributions"
,
"field"
:
"storename (group)"
,
"values"
:
[
"CKC-VIP"
]},
{
"field"
:
"Wkt<=4"
,
"values"
:
[
ALL_FILTER_VALUE
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"category contributions"
,
"field"
:
"storename"
,
"values"
:
[
"CKC-VIP"
]},
{
"field"
:
"daily"
,
"values"
:
[
ALL_FILTER_VALUE
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"Soldqty Trend"
,
"field"
:
"storename (group)"
,
"values"
:
[
"CKC-VIP"
]},
{
"field"
:
"Daily"
,
"values"
:
[
ALL_FILTER_VALUE
]},
{
"kind"
:
"filter"
,
"worksheet"
:
"Soldqty Trend"
,
"field"
:
"storename"
,
"values"
:
[
"CKC-VIP"
]},
{
"kind"
:
"parameter"
,
"name"
:
"Top Parameter"
,
"value"
:
10
},
]
]
def
build_spec
(
report_month
:
str
,
report_year
:
int
)
->
tuple
[
dict
[
str
,
Any
],
dict
[
str
,
Any
]]:
def
build_capture_spec
(
slide_code
:
str
,
report_month
:
str
,
report_year
:
int
)
->
dict
[
str
,
Any
]:
capture
=
{
slide_spec
=
SLIDE_SPECS
[
slide_code
]
"capture_id"
:
"top_products_shoes"
,
return
{
"capture_id"
:
slide_spec
[
"capture_id"
],
"session"
:
SESSION_NAME
,
"session"
:
SESSION_NAME
,
"hash_url"
:
"#/views/CKTopProducts-General_16862068169500/TopProducts?:iid=1"
,
"hash_url"
:
"#/views/CKTopProducts-General_16862068169500/TopProducts?:iid=1"
,
"inner_frame_fragment"
:
"/views/CKTopProducts-General_16862068169500/TopProducts?"
,
"inner_frame_fragment"
:
"/views/CKTopProducts-General_16862068169500/TopProducts?"
,
"activate_sheet"
:
"Top Products"
,
"activate_sheet"
:
"Top Products"
,
"filters"
:
build_tableau_filters
(
report_month
,
report_year
),
"filter_actions"
:
build_filter_actions
(
report_month
,
report_year
,
slide_code
),
"params"
:
{
"Top Parameter"
:
10
},
"raw_screenshot_name"
:
slide_spec
[
"raw_screenshot_name"
],
"raw_screenshot_name"
:
"top-products-shoes.png"
,
"note"
:
f
"{slide_code} live Tableau screenshot with requested filters applied."
,
"note"
:
"S10 Tableau screenshot with year/month/week/daily/category/store filters applied."
,
}
asset
=
{
"slide_code"
:
"S10"
,
"slide"
:
10
,
"shape_id"
:
2
,
"shape_name"
:
"Picture 1"
,
"asset_name"
:
"s10_top_products_chart"
,
"capture_id"
:
capture
[
"capture_id"
],
"crop"
:
{
"left"
:
0
,
"top"
:
120
,
"width"
:
1400
,
"height"
:
900
},
"resize_to"
:
{
"width"
:
1039
,
"height"
:
585
},
"source_view"
:
"TopProducts"
,
"note"
:
"S10 main Tableau chart area."
,
}
}
return
capture
,
asset
def
parse_args
()
->
argparse
.
Namespace
:
parser
=
argparse
.
ArgumentParser
(
description
=
"Generate S10 top products image assets for the VIP report."
)
parser
.
add_argument
(
"--config"
,
required
=
True
,
help
=
"Path to config.yaml"
)
parser
.
add_argument
(
"--slides"
,
default
=
"S10"
,
help
=
"Slide codes to generate. This script currently supports S10 only."
)
parser
.
add_argument
(
"--report-month"
,
help
=
"Report month, for example 1 / 01 / 一月"
)
parser
.
add_argument
(
"--report-year"
,
type
=
int
,
help
=
"Report year"
)
parser
.
add_argument
(
"--compare-year"
,
type
=
int
,
help
=
"Compare year"
)
return
parser
.
parse_args
()
def
run_cmd
(
args
:
list
[
str
],
*
,
cwd
:
Path
,
timeout
:
int
=
120
,
check
:
bool
=
True
)
->
subprocess
.
CompletedProcess
[
str
]:
def
run_cmd
(
args
:
list
[
str
],
*
,
cwd
:
Path
,
timeout
:
int
=
120
,
check
:
bool
=
True
)
->
subprocess
.
CompletedProcess
[
str
]:
...
@@ -225,31 +267,36 @@ def build_configure_view_js(spec: dict[str, Any]) -> str:
...
@@ -225,31 +267,36 @@ def build_configure_view_js(spec: dict[str, Any]) -> str:
await workbook.activateSheetAsync(config.activate_sheet);
await workbook.activateSheetAsync(config.activate_sheet);
const activeSheet = workbook.getActiveSheet();
const activeSheet = workbook.getActiveSheet();
const worksheets = activeSheet && typeof activeSheet.getWorksheets === 'function'
const worksheets = activeSheet && typeof activeSheet.getWorksheets === 'function'
?
activeSheet.getWorksheets(
)
?
Object.fromEntries(activeSheet.getWorksheets().map((worksheet) => [worksheet.getName(), worksheet])
)
:
[activeSheet]
;
:
{{}}
;
const updateType = window.tableau.FilterUpdateType.REPLACE;
const updateType = window.tableau.FilterUpdateType.REPLACE;
const applyLog = [];
for (const filter of config.filters) {{
for (const action of config.filter_actions) {{
for (const worksheet of worksheets) {{
try {{
try {{
await worksheet.applyFilterAsync(filter.field, filter.values, updateType);
if (action.kind === 'parameter') {{
}} catch (error) {{
await workbook.changeParameterValueAsync(action.name, action.value);
}}
applyLog.push({{ kind: action.kind, name: action.name, ok: true }});
}}
continue;
}}
}}
for (const [name, value] of Object.entries(config.params)) {{
const worksheet = worksheets[action.worksheet];
try {{
if (!worksheet) {{
await workbook.changeParameterValueAsync(name, value);
applyLog.push({{ kind: action.kind, worksheet: action.worksheet, field: action.field, ok: false, error: 'worksheet not found' }});
continue;
}}
await worksheet.applyFilterAsync(action.field, action.values, updateType);
applyLog.push({{ kind: action.kind, worksheet: action.worksheet, field: action.field, values: action.values, ok: true }});
}} catch (error) {{
}} catch (error) {{
applyLog.push({{ kind: action.kind, worksheet: action.worksheet || null, field: action.field || null, name: action.name || null, ok: false, error: String(error) }});
}}
}}
}}
}}
await sleep(
2
0000);
await sleep(
6
0000);
return {{
return {{
activeSheet: activeSheet.getName(),
activeSheet: activeSheet.getName(),
filter
s: config.filter
s,
filter
Actions: config.filter_action
s,
params: config.params
,
applyLog
,
url: location.href,
url: location.href,
}};
}};
}}, spec);
}}, spec);
...
@@ -287,11 +334,7 @@ def build_capture_js(inner_frame_fragment: str, screenshot_name: str) -> str:
...
@@ -287,11 +334,7 @@ def build_capture_js(inner_frame_fragment: str, screenshot_name: str) -> str:
def
run_code
(
session
:
str
,
script_path
:
Path
,
*
,
cwd
:
Path
,
timeout
:
int
=
120
)
->
str
:
def
run_code
(
session
:
str
,
script_path
:
Path
,
*
,
cwd
:
Path
,
timeout
:
int
=
120
)
->
str
:
return
run_playwright
(
return
run_playwright
([
"--session"
,
session
,
"run-code"
,
"--filename"
,
str
(
script_path
)],
cwd
=
cwd
,
timeout
=
timeout
)
[
"--session"
,
session
,
"run-code"
,
"--filename"
,
str
(
script_path
)],
cwd
=
cwd
,
timeout
=
timeout
,
)
def
ensure_browser_session
(
session
:
str
,
*
,
cwd
:
Path
)
->
None
:
def
ensure_browser_session
(
session
:
str
,
*
,
cwd
:
Path
)
->
None
:
...
@@ -363,8 +406,7 @@ def capture_tableau_view(
...
@@ -363,8 +406,7 @@ def capture_tableau_view(
build_configure_view_js
(
build_configure_view_js
(
{
{
"activate_sheet"
:
capture_spec
[
"activate_sheet"
],
"activate_sheet"
:
capture_spec
[
"activate_sheet"
],
"filters"
:
capture_spec
[
"filters"
],
"filter_actions"
:
capture_spec
[
"filter_actions"
],
"params"
:
capture_spec
[
"params"
],
}
}
),
),
)
)
...
@@ -388,7 +430,6 @@ def capture_tableau_view(
...
@@ -388,7 +430,6 @@ def capture_tableau_view(
time
.
sleep
(
3
)
time
.
sleep
(
3
)
continue
continue
raise
RuntimeError
(
f
"Failed to configure Tableau view: {configure_output}"
)
raise
RuntimeError
(
f
"Failed to configure Tableau view: {configure_output}"
)
run_code
(
session
,
capture_script
,
cwd
=
workdir
,
timeout
=
180
)
run_code
(
session
,
capture_script
,
cwd
=
workdir
,
timeout
=
180
)
break
break
finally
:
finally
:
...
@@ -404,10 +445,7 @@ def capture_tableau_view(
...
@@ -404,10 +445,7 @@ def capture_tableau_view(
def
month_date_range
(
report_year
:
int
,
report_month
:
str
)
->
tuple
[
str
,
str
]:
def
month_date_range
(
report_year
:
int
,
report_month
:
str
)
->
tuple
[
str
,
str
]:
month_index
=
resolve_month_index
(
report_month
)
month_index
=
resolve_month_index
(
report_month
)
last_day
=
calendar
.
monthrange
(
report_year
,
month_index
)[
1
]
last_day
=
calendar
.
monthrange
(
report_year
,
month_index
)[
1
]
return
(
return
f
"{report_year}-{month_index:02d}-01"
,
f
"{report_year}-{month_index:02d}-{last_day:02d}"
f
"{report_year}-{month_index:02d}-01"
,
f
"{report_year}-{month_index:02d}-{last_day:02d}"
,
)
def
resolve_shop
(
config
:
dict
[
str
,
Any
],
store_name
:
str
)
->
tuple
[
int
,
str
]:
def
resolve_shop
(
config
:
dict
[
str
,
Any
],
store_name
:
str
)
->
tuple
[
int
,
str
]:
...
@@ -441,12 +479,13 @@ def resolve_shop(config: dict[str, Any], store_name: str) -> tuple[int, str]:
...
@@ -441,12 +479,13 @@ def resolve_shop(config: dict[str, Any], store_name: str) -> tuple[int, str]:
return
int
(
row
[
"shopid"
]),
str
(
row
[
"shopname"
])
return
int
(
row
[
"shopid"
]),
str
(
row
[
"shopname"
])
def
fetch_
s10_
top_products
(
def
fetch_top_products
(
config
:
dict
[
str
,
Any
],
config
:
dict
[
str
,
Any
],
*
,
*
,
store_name
:
str
,
store_name
:
str
,
report_month
:
str
,
report_month
:
str
,
report_year
:
int
,
report_year
:
int
,
level1
:
str
,
limit
:
int
,
limit
:
int
,
)
->
dict
[
str
,
Any
]:
)
->
dict
[
str
,
Any
]:
shop_id
,
resolved_shop_name
=
resolve_shop
(
config
,
store_name
)
shop_id
,
resolved_shop_name
=
resolve_shop
(
config
,
store_name
)
...
@@ -468,20 +507,17 @@ def fetch_s10_top_products(
...
@@ -468,20 +507,17 @@ def fetch_s10_top_products(
SELECT
SELECT
articlecode,
articlecode,
SUM(CASE WHEN qty > 0 THEN qty ELSE 0 END) AS qty,
SUM(CASE WHEN qty > 0 THEN qty ELSE 0 END) AS qty,
SUM(CASE WHEN sales > 0 THEN sales ELSE 0 END) AS sales,
SUM(CASE WHEN sales > 0 THEN sales ELSE 0 END) AS sales
level1,
level3,
level4
FROM oms_daily_report_detail
FROM oms_daily_report_detail
WHERE shopid =
%
s
WHERE shopid =
%
s
AND daily BETWEEN
%
s AND
%
s
AND daily BETWEEN
%
s AND
%
s
AND level1 =
'SHOES'
AND level1 =
%
s
GROUP BY articlecode
, level1, level3, level4
GROUP BY articlecode
HAVING qty > 0 OR sales > 0
HAVING qty > 0 OR sales > 0
ORDER BY qty DESC, sales DESC, articlecode ASC
ORDER BY qty DESC, sales DESC, articlecode ASC
LIMIT
%
s
LIMIT
%
s
"""
,
"""
,
(
shop_id
,
start_date
,
end_date
,
limit
),
(
shop_id
,
start_date
,
end_date
,
l
evel1
,
l
imit
),
)
)
rows
=
cursor
.
fetchall
()
rows
=
cursor
.
fetchall
()
finally
:
finally
:
...
@@ -492,16 +528,12 @@ def fetch_s10_top_products(
...
@@ -492,16 +528,12 @@ def fetch_s10_top_products(
"articlecode"
:
str
(
row
[
"articlecode"
]),
"articlecode"
:
str
(
row
[
"articlecode"
]),
"qty"
:
int
(
row
[
"qty"
]
or
0
),
"qty"
:
int
(
row
[
"qty"
]
or
0
),
"sales"
:
float
(
row
[
"sales"
]
or
0
),
"sales"
:
float
(
row
[
"sales"
]
or
0
),
"level1"
:
str
(
row
.
get
(
"level1"
)
or
""
),
"level3"
:
str
(
row
.
get
(
"level3"
)
or
""
),
"level4"
:
str
(
row
.
get
(
"level4"
)
or
""
),
}
}
for
row
in
rows
for
row
in
rows
if
row
.
get
(
"articlecode"
)
if
row
.
get
(
"articlecode"
)
]
]
if
len
(
normalized_rows
)
<
limit
:
if
len
(
normalized_rows
)
<
limit
:
raise
RuntimeError
(
f
"
S10
top products rows are insufficient: expected {limit}, got {len(normalized_rows)}"
)
raise
RuntimeError
(
f
"
{level1}
top products rows are insufficient: expected {limit}, got {len(normalized_rows)}"
)
return
{
return
{
"store_name"
:
store_name
,
"store_name"
:
store_name
,
...
@@ -509,17 +541,26 @@ def fetch_s10_top_products(
...
@@ -509,17 +541,26 @@ def fetch_s10_top_products(
"shop_id"
:
shop_id
,
"shop_id"
:
shop_id
,
"start_date"
:
start_date
,
"start_date"
:
start_date
,
"end_date"
:
end_date
,
"end_date"
:
end_date
,
"level1"
:
level1
,
"rows"
:
normalized_rows
[:
limit
],
"rows"
:
normalized_rows
[:
limit
],
}
}
def
download_image
(
url
:
str
)
->
Image
.
Image
:
def
download_image
(
url
:
str
,
*
,
attempts
:
int
=
4
,
timeout
:
int
=
60
)
->
Image
.
Image
:
last_error
:
Exception
|
None
=
None
for
attempt
in
range
(
1
,
attempts
+
1
):
try
:
request
=
urllib
.
request
.
Request
(
url
,
headers
=
{
"User-Agent"
:
"Mozilla/5.0"
})
request
=
urllib
.
request
.
Request
(
url
,
headers
=
{
"User-Agent"
:
"Mozilla/5.0"
})
with
urllib
.
request
.
urlopen
(
request
,
timeout
=
60
)
as
response
:
with
urllib
.
request
.
urlopen
(
request
,
timeout
=
timeout
)
as
response
:
payload
=
response
.
read
()
payload
=
response
.
read
()
image
=
Image
.
open
(
BytesIO
(
payload
))
image
=
Image
.
open
(
BytesIO
(
payload
))
image
=
ImageOps
.
exif_transpose
(
image
)
.
convert
(
"RGBA"
)
return
ImageOps
.
exif_transpose
(
image
)
.
convert
(
"RGBA"
)
return
image
except
Exception
as
exc
:
# pragma: no cover - network retry path
last_error
=
exc
if
attempt
<
attempts
:
time
.
sleep
(
min
(
12
,
attempt
*
3
))
continue
raise
RuntimeError
(
f
"Failed to download product image from {url}"
)
from
last_error
def
render_product_image
(
source
:
Image
.
Image
,
target
:
Path
,
*
,
canvas_width
:
int
,
canvas_height
:
int
)
->
None
:
def
render_product_image
(
source
:
Image
.
Image
,
target
:
Path
,
*
,
canvas_width
:
int
,
canvas_height
:
int
)
->
None
:
...
@@ -539,33 +580,30 @@ def sanitize_file_token(value: str) -> str:
...
@@ -539,33 +580,30 @@ def sanitize_file_token(value: str) -> str:
return
re
.
sub
(
r"[^A-Za-z0-9_-]+"
,
"_"
,
value
)
.
strip
(
"_"
)
or
"item"
return
re
.
sub
(
r"[^A-Za-z0-9_-]+"
,
"_"
,
value
)
.
strip
(
"_"
)
or
"item"
def
build_s10_product_assets
(
config
:
dict
[
str
,
Any
],
*
,
asset_dir
:
Path
,
report_month
:
str
,
report_year
:
int
)
->
tuple
[
list
[
dict
[
str
,
Any
]],
list
[
dict
[
str
,
Any
]],
dict
[
str
,
Any
]]:
def
build_product_assets
(
source_payload
=
fetch_s10_top_products
(
slide_code
:
str
,
config
,
*
,
store_name
=
"ckc-vip"
,
asset_dir
:
Path
,
report_month
=
report_month
,
source_payload
:
dict
[
str
,
Any
],
report_year
=
report_year
,
)
->
tuple
[
list
[
dict
[
str
,
Any
]],
list
[
dict
[
str
,
Any
]]]:
limit
=
10
,
slide_spec
=
SLIDE_SPECS
[
slide_code
]
)
operations
:
list
[
dict
[
str
,
Any
]]
=
[]
operations
:
list
[
dict
[
str
,
Any
]]
=
[]
manifest_items
:
list
[
dict
[
str
,
Any
]]
=
[]
manifest_items
:
list
[
dict
[
str
,
Any
]]
=
[]
for
slot
,
row
in
zip
(
S10_PRODUCT_SLOTS
,
source_payload
[
"rows"
]):
for
slot
,
row
in
zip
(
slide_spec
[
"product_slots"
],
source_payload
[
"rows"
]):
articlecode
=
row
[
"articlecode"
]
articlecode
=
row
[
"articlecode"
]
image_url
=
S10_
PRODUCT_IMAGE_URL
.
format
(
articlecode
=
articlecode
)
image_url
=
PRODUCT_IMAGE_URL
.
format
(
articlecode
=
articlecode
)
asset_name
=
f
"
s10
_top_product_{slot['rank']:02d}_{sanitize_file_token(articlecode)}"
asset_name
=
f
"
{slide_code.lower()}
_top_product_{slot['rank']:02d}_{sanitize_file_token(articlecode)}"
asset_path
=
asset_dir
/
f
"{asset_name}.png"
asset_path
=
asset_dir
/
f
"{asset_name}.png"
render_product_image
(
render_product_image
(
download_image
(
image_url
),
download_image
(
image_url
),
asset_path
,
asset_path
,
canvas_width
=
slot
[
"canvas_width"
],
canvas_width
=
slot
[
"canvas_width"
],
canvas_height
=
slot
[
"canvas_height"
],
canvas_height
=
slot
[
"canvas_height"
],
)
)
operations
.
append
(
operations
.
append
(
{
{
"slide"
:
10
,
"slide"
:
slide_spec
[
"slide"
]
,
"shape_id"
:
slot
[
"shape_id"
],
"shape_id"
:
slot
[
"shape_id"
],
"shape_name"
:
slot
[
"shape_name"
],
"shape_name"
:
slot
[
"shape_name"
],
"image_path"
:
str
(
asset_path
),
"image_path"
:
str
(
asset_path
),
...
@@ -573,15 +611,15 @@ def build_s10_product_assets(config: dict[str, Any], *, asset_dir: Path, report_
...
@@ -573,15 +611,15 @@ def build_s10_product_assets(config: dict[str, Any], *, asset_dir: Path, report_
)
)
manifest_items
.
append
(
manifest_items
.
append
(
{
{
"slide_code"
:
"S10"
,
"slide_code"
:
slide_code
,
"slide"
:
10
,
"slide"
:
slide_spec
[
"slide"
]
,
"shape_id"
:
slot
[
"shape_id"
],
"shape_id"
:
slot
[
"shape_id"
],
"shape_name"
:
slot
[
"shape_name"
],
"shape_name"
:
slot
[
"shape_name"
],
"asset_name"
:
asset_name
,
"asset_name"
:
asset_name
,
"asset_path"
:
str
(
asset_path
),
"asset_path"
:
str
(
asset_path
),
"source_capture_id"
:
"mysql-top-products-shoes
"
,
"source_capture_id"
:
f
"url-top-products-{slide_code.lower()}
"
,
"source_view"
:
"oms_daily_report_detail"
,
"source_view"
:
"oms_daily_report_detail"
,
"note"
:
f
"
S10
product image for Top {slot['rank']}."
,
"note"
:
f
"
{slide_code}
product image for Top {slot['rank']}."
,
"top_rank"
:
slot
[
"rank"
],
"top_rank"
:
slot
[
"rank"
],
"articlecode"
:
articlecode
,
"articlecode"
:
articlecode
,
"qty"
:
row
[
"qty"
],
"qty"
:
row
[
"qty"
],
...
@@ -591,7 +629,7 @@ def build_s10_product_assets(config: dict[str, Any], *, asset_dir: Path, report_
...
@@ -591,7 +629,7 @@ def build_s10_product_assets(config: dict[str, Any], *, asset_dir: Path, report_
}
}
)
)
return
operations
,
manifest_items
,
source_payload
return
operations
,
manifest_items
def
main
()
->
None
:
def
main
()
->
None
:
...
@@ -600,11 +638,12 @@ def main() -> None:
...
@@ -600,11 +638,12 @@ def main() -> None:
config
=
yaml
.
safe_load
(
config_path
.
read_text
(
encoding
=
"utf-8"
))
config
=
yaml
.
safe_load
(
config_path
.
read_text
(
encoding
=
"utf-8"
))
report_month
,
report_year
,
compare_year
=
resolve_report_period
(
args
,
config
)
report_month
,
report_year
,
compare_year
=
resolve_report_period
(
args
,
config
)
requested
=
{
item
.
strip
()
.
upper
()
for
item
in
args
.
slides
.
split
(
","
)
if
item
.
strip
()}
requested
=
[
item
.
strip
()
.
upper
()
for
item
in
args
.
slides
.
split
(
","
)
if
item
.
strip
()]
if
"S10"
not
in
requested
:
requested
=
[
item
for
item
in
requested
if
item
in
SLIDE_SPECS
]
raise
SystemExit
(
"This generator currently supports S10 only. Pass --slides S10."
)
if
not
requested
:
raise
SystemExit
(
"No supported slides requested. Use S09 and/or S10."
)
capture_spec
,
asset_spec
=
build_spec
(
report_month
,
report_year
)
capture_spec
s
=
{
slide_code
:
build_capture_spec
(
slide_code
,
report_month
,
report_year
)
for
slide_code
in
requested
}
vip_workdir
=
Path
(
config
[
"paths"
][
"workdir"
])
.
resolve
()
vip_workdir
=
Path
(
config
[
"paths"
][
"workdir"
])
.
resolve
()
workspace_root
=
vip_workdir
.
parents
[
1
]
workspace_root
=
vip_workdir
.
parents
[
1
]
...
@@ -619,7 +658,7 @@ def main() -> None:
...
@@ -619,7 +658,7 @@ def main() -> None:
load_state_if_present
(
session
,
state_path
,
cwd
=
vip_workdir
)
load_state_if_present
(
session
,
state_path
,
cwd
=
vip_workdir
)
base_url
=
config
[
"tableau"
][
"base_url"
]
.
rstrip
(
"/"
)
base_url
=
config
[
"tableau"
][
"base_url"
]
.
rstrip
(
"/"
)
first_target_url
=
f
"{base_url}{capture_spec['hash_url']}"
first_target_url
=
f
"{base_url}{capture_spec
s[requested[0]]
['hash_url']}"
run_playwright
([
"--session"
,
session
,
"goto"
,
first_target_url
],
cwd
=
vip_workdir
,
timeout
=
120
)
run_playwright
([
"--session"
,
session
,
"goto"
,
first_target_url
],
cwd
=
vip_workdir
,
timeout
=
120
)
login_script
=
write_js
(
login_script
=
write_js
(
...
@@ -632,8 +671,10 @@ def main() -> None:
...
@@ -632,8 +671,10 @@ def main() -> None:
finally
:
finally
:
login_script
.
unlink
(
missing_ok
=
True
)
login_script
.
unlink
(
missing_ok
=
True
)
raw_screenshot
=
capture_tableau_view
(
raw_screenshots
:
dict
[
str
,
Path
]
=
{}
capture_spec
,
for
slide_code
in
requested
:
raw_screenshots
[
slide_code
]
=
capture_tableau_view
(
capture_specs
[
slide_code
],
base_url
=
base_url
,
base_url
=
base_url
,
session
=
session
,
session
=
session
,
workdir
=
vip_workdir
,
workdir
=
vip_workdir
,
...
@@ -641,42 +682,55 @@ def main() -> None:
...
@@ -641,42 +682,55 @@ def main() -> None:
)
)
save_state
(
session
,
state_path
,
cwd
=
vip_workdir
)
save_state
(
session
,
state_path
,
cwd
=
vip_workdir
)
chart_path
=
asset_dir
/
f
"{asset_spec['asset_name']}.png"
operations
=
{
"replace_text"
:
[],
"replace_images"
:
[]}
crop_image
(
raw_screenshot
,
chart_path
,
asset_spec
[
"crop"
],
resize_to
=
asset_spec
[
"resize_to"
])
manifest_items
:
list
[
dict
[
str
,
Any
]]
=
[]
sql_sources
:
dict
[
str
,
Any
]
=
{}
operations
=
{
for
slide_code
in
requested
:
"replace_text"
:
[],
slide_spec
=
SLIDE_SPECS
[
slide_code
]
"replace_images"
:
[
raw_screenshot
=
raw_screenshots
[
slide_code
]
chart_path
=
asset_dir
/
f
"{slide_spec['asset_name']}.png"
crop_image
(
raw_screenshot
,
chart_path
,
slide_spec
[
"crop"
],
resize_to
=
slide_spec
[
"resize_to"
])
operations
[
"replace_images"
]
.
append
(
{
{
"slide"
:
10
,
"slide"
:
slide_spec
[
"slide"
]
,
"shape_id"
:
asset
_spec
[
"shape_id"
],
"shape_id"
:
slide
_spec
[
"shape_id"
],
"shape_name"
:
asset
_spec
[
"shape_name"
],
"shape_name"
:
slide
_spec
[
"shape_name"
],
"image_path"
:
str
(
chart_path
),
"image_path"
:
str
(
chart_path
),
}
}
],
)
}
manifest_items
.
append
(
manifest_items
:
list
[
dict
[
str
,
Any
]]
=
[
{
{
"slide_code"
:
asset_spec
[
"slide_code"
]
,
"slide_code"
:
slide_code
,
"slide"
:
asset
_spec
[
"slide"
],
"slide"
:
slide
_spec
[
"slide"
],
"shape_id"
:
asset
_spec
[
"shape_id"
],
"shape_id"
:
slide
_spec
[
"shape_id"
],
"shape_name"
:
asset
_spec
[
"shape_name"
],
"shape_name"
:
slide
_spec
[
"shape_name"
],
"asset_name"
:
asset
_spec
[
"asset_name"
],
"asset_name"
:
slide
_spec
[
"asset_name"
],
"asset_path"
:
str
(
chart_path
),
"asset_path"
:
str
(
chart_path
),
"source_capture_id"
:
captur
e_spec
[
"capture_id"
],
"source_capture_id"
:
slid
e_spec
[
"capture_id"
],
"source_view"
:
asset_spec
[
"source_view"
]
,
"source_view"
:
"TopProducts"
,
"note"
:
asset
_spec
[
"note"
],
"note"
:
slide
_spec
[
"note"
],
"raw_screenshot"
:
str
(
raw_screenshot
),
"raw_screenshot"
:
str
(
raw_screenshot
),
"crop"
:
asset
_spec
[
"crop"
],
"crop"
:
slide
_spec
[
"crop"
],
"resize_to"
:
asset
_spec
[
"resize_to"
],
"resize_to"
:
slide
_spec
[
"resize_to"
],
}
}
]
)
product_operations
,
product_manifest_items
,
product_source
=
build_s10_product_asse
ts
(
top_source
=
fetch_top_produc
ts
(
config
,
config
,
asset_dir
=
asset_dir
,
store_name
=
"ckc-vip"
,
report_month
=
report_month
,
report_month
=
report_month
,
report_year
=
report_year
,
report_year
=
report_year
,
level1
=
slide_spec
[
"level1"
],
limit
=
len
(
slide_spec
[
"product_slots"
]),
)
sql_sources
[
slide_code
]
=
top_source
product_operations
,
product_manifest_items
=
build_product_assets
(
slide_code
,
asset_dir
=
asset_dir
,
source_payload
=
top_source
,
)
)
operations
[
"replace_images"
]
.
extend
(
product_operations
)
operations
[
"replace_images"
]
.
extend
(
product_operations
)
manifest_items
.
extend
(
product_manifest_items
)
manifest_items
.
extend
(
product_manifest_items
)
...
@@ -689,16 +743,16 @@ def main() -> None:
...
@@ -689,16 +743,16 @@ def main() -> None:
"source"
:
{
"source"
:
{
"captures"
:
[
"captures"
:
[
{
{
"capture_id"
:
capture_spec
[
"capture_id"
],
"capture_id"
:
capture_specs
[
slide_code
][
"capture_id"
],
"hash_url"
:
capture_spec
[
"hash_url"
],
"hash_url"
:
capture_specs
[
slide_code
][
"hash_url"
],
"activate_sheet"
:
capture_spec
[
"activate_sheet"
],
"activate_sheet"
:
capture_specs
[
slide_code
][
"activate_sheet"
],
"filters"
:
capture_spec
[
"filters"
],
"filter_actions"
:
capture_specs
[
slide_code
][
"filter_actions"
],
"params"
:
capture_spec
[
"params"
],
"raw_screenshot"
:
str
(
raw_screenshots
[
slide_code
]),
"raw_screenshot"
:
str
(
raw_screenshot
),
"note"
:
capture_specs
[
slide_code
][
"note"
],
"note"
:
capture_spec
[
"note"
],
}
}
for
slide_code
in
requested
],
],
"
sql_top_products"
:
product_source
,
"
url_product_sources"
:
sql_sources
,
"config_path"
:
str
(
config_path
),
"config_path"
:
str
(
config_path
),
"report_period"
:
{
"report_period"
:
{
"month"
:
report_month
,
"month"
:
report_month
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment