Commit c738186f authored by wushanjing's avatar wushanjing

更新订单状态流转E2E链路

parent 98fbb8ed
......@@ -197,6 +197,10 @@ class WanyoujiMemberPage(TenantAuthPage):
self.page = dual_browser_manager().page_for_account(member.phone, seed_page=self.page)
if self.bug_reporter:
self.bug_reporter.bind_page(self.page)
if self.can_reuse_member_login(member):
if self.bug_reporter:
self.bug_reporter.clear()
return
assert self.code_provider is not None, "验证码登录校验需要配置 VerificationCodeProvider"
self.clear_desktop_login_state()
self.goto(self.tenant_home_url.replace("/home", "/login"))
......@@ -219,12 +223,46 @@ class WanyoujiMemberPage(TenantAuthPage):
self.select_sub_account(member)
self.verify_sub_account_entered(member)
self.verify_wanyouji_role_menu_permissions(member)
setattr(self.page, "_e2e_current_member_account", member.phone)
setattr(self.page, "_e2e_current_player_account", member.phone)
setattr(self.page, "_e2e_current_main_account", "")
if dual_browser_enabled():
dual_browser_manager().set_active_page(self.page)
assert_account_page(self.page, member.phone)
def can_reuse_member_login(self, member: WanyoujiMemberData) -> bool:
reuse_flag = os.getenv("E2E_REUSE_MEMBER_LOGIN", "1").strip().lower()
if reuse_flag in {"0", "false", "no", "off"}:
return False
player_reuse_flag = os.getenv("E2E_REUSE_PLAYER_LOGIN", "1").strip().lower()
if member.role == "打手" and player_reuse_flag in {"0", "false", "no", "off"}:
return False
current_member = getattr(self.page, "_e2e_current_member_account", "")
current_player = getattr(self.page, "_e2e_current_player_account", "")
if member.phone not in {current_member, current_player}:
return False
try:
self.goto(WANYOUJI_CONSOLE_URL)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(1500)
text = self.page.locator("body").inner_text(timeout=5000)
if "登录" in text and ("验证码登录" in text or "账号登录" in text or "登录/注册" in text):
return False
if "选择账号" in text and "进入后台" in text:
self.select_sub_account(member)
self.goto(WANYOUJI_CONSOLE_URL)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(1500)
text = self.page.locator("body").inner_text(timeout=5000)
role_markers = [member.name, member.role, "丸友集"]
if member.role == "打手":
role_markers.extend(["打手应用", "我的订单", "我的收入"])
elif member.role == "客服":
role_markers.extend(["收单应用", "客户自助填单", "客服"])
return any(marker and marker in text for marker in role_markers)
except Exception:
return False
def select_sub_account(self, member: WanyoujiMemberData) -> None:
body_text = self.page.locator("body").inner_text(timeout=5000)
if "/account/select" not in self.page.url and "选择账号" not in body_text:
......
......@@ -14,6 +14,11 @@ from playwright.sync_api import Frame, Page, expect
from e2e.pages.tenant_auth_page import ENTERPRISE_CONSOLE_URL, TenantAuthPage
from e2e.pages.wanzhanggui_push_page import WANZHANGGUI_CONSOLE_URL
from e2e.utils.bug_reporter import BugReporter
from e2e.utils.data_factory import (
DISPATCH_PRICE_PERCENT_MAX,
DISPATCH_PRICE_PERCENT_MIN,
default_dispatch_price_percent,
)
from e2e.utils.image_captcha import ImageCaptchaSolver, decode_data_image
......@@ -42,7 +47,7 @@ class ManualOrderData:
receive_price: str = "80"
dispatch_price: str = "70"
dispatch_price_mode: str = "percent"
dispatch_price_percent: str = "90"
dispatch_price_percent: str = ""
service_hours: str = "14"
boss_phone: str = "13800138000"
contact_phone: str = "13800138000"
......@@ -120,6 +125,16 @@ class WanzhangguiOrderPage(TenantAuthPage):
return frame
except Exception:
pass
if attempt in {2, 5}:
try:
frame.evaluate("url => { window.location.href = url; }", WANZHANGGUI_ORDER_LIST_URL)
except Exception:
try:
self.page.reload(wait_until="domcontentloaded")
except Exception:
pass
self.page.wait_for_timeout(2500)
continue
if attempt == 4:
try:
self.click_sidebar_text("订单管理")
......@@ -689,7 +704,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
self.click_frame_button(frame, "提交订单")
self.page.wait_for_timeout(4000)
self.record_order_submit_errors(order)
if frame.locator(".ant-modal").count():
if self.has_visible_modal(frame):
self.fill_required_fields_after_submit(frame, order)
self.assert_sub_order_price_mode_ready(frame, order)
self.click_frame_button(frame, "提交订单")
......@@ -715,7 +730,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
self.click_frame_button(frame, "提交订单")
self.page.wait_for_timeout(4000)
self.record_order_submit_errors(order)
if frame.locator(".ant-modal").count():
if self.has_visible_modal(frame):
self.fill_required_fields_after_submit_without_dispatch(frame, order)
self.ensure_no_push_radio(frame)
self.assert_no_dispatch_fields_visible(frame)
......@@ -2004,7 +2019,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
self.page.wait_for_timeout(1200)
if self.visible_modal_text(frame):
self.fill_visible_modal_inputs_if_present(frame, reason)
self.submit_current_modal(frame, ["提交", "确定", "确认", "保 存", "保存"])
self.submit_current_modal(frame, ["确认取消", "提交", "确定", "确认", "保 存", "保存"])
else:
self.confirm_open_dialog(frame)
self.page.wait_for_timeout(3500)
......@@ -2229,6 +2244,83 @@ class WanzhangguiOrderPage(TenantAuthPage):
expected_aftersale_status=["售后中", "待处理", "处理中"],
)
def start_pending_acceptance_main_order_aftersale(
self,
*,
main_order_no: str = "",
source_order_no: str = "",
order_title: str = "",
reason: str,
amount: str,
) -> str:
frame = self.open_order_management()
self.search_order_by_best_identity(
frame,
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
)
before_text = frame.locator("body").inner_text(timeout=5000)
assert (
source_order_no in before_text
or order_title in before_text
or (main_order_no and main_order_no in before_text)
or self.filtered_order_list_has_single_result(before_text)
), (
f"丸掌柜订单管理未找到待验收主单售后前置订单;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; order_title={order_title}; text={before_text[:1500]}"
)
opened = self.open_main_order_detail_from_list(
frame,
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
)
assert opened, (
f"丸掌柜订单管理未能打开待验收主单订单详情;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; text={before_text[:1500]}"
)
detail_text = self.visible_modal_text(frame) or frame.locator("body").inner_text(timeout=5000)
assert "待验收" in detail_text, (
f"S-FLOW-046 前置不满足主单/子单待验收;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; detail={detail_text[:1500]}"
)
clicked = self.click_main_order_aftersale_action(frame, ["发起售后", "申请售后", "售后"])
assert clicked, (
f"S-FLOW-046 未找到订单详情主单售后入口;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; detail={detail_text[:1500]}"
)
self.page.wait_for_timeout(1200)
self.fill_aftersale_modal(frame, reason=reason, amount=amount)
self.submit_current_modal(frame, ["提交", "确定", "确认", "保 存", "保存"])
self.page.wait_for_timeout(3500)
return self.assert_order_detail_statuses(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_main_status=["售后中", "待验收"],
expected_sub_status=["售后中", "待验收"],
expected_aftersale_status=["售后中", "待处理", "处理中"],
)
def cancel_pending_acceptance_main_order_aftersale(
self,
*,
main_order_no: str = "",
source_order_no: str = "",
order_title: str = "",
reason: str,
) -> str:
return self.cancel_aftersale_for_order(
source_order_no,
order_title,
main_order_no=main_order_no,
reason=reason,
expected_status_after_cancel=["待验收", "等待验收"],
)
def click_main_order_aftersale_action(self, frame: Frame, action_texts: list[str]) -> bool:
clicked = bool(
frame.evaluate(
......@@ -2322,7 +2414,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
)
detail_text = self.visible_modal_text(frame) or frame.locator("body").inner_text(timeout=5000)
assert "子单列表" in detail_text and "售后中" in detail_text, (
f"S-FLOW-036 前置不满足:订单详情子单列表未看到售后中子单;"
f"取消异常中子单售后前置不满足:订单详情子单列表未看到售后中子单;"
f"main_order_no={main_order_no}; source_order_no={source_order_no}; detail={detail_text[:1500]}"
)
......@@ -2334,13 +2426,13 @@ class WanzhangguiOrderPage(TenantAuthPage):
if not clicked:
clicked = self.click_wanzhanggui_detail_action_text(frame, ["取消售后", "撤销售后"])
assert clicked, (
f"S-FLOW-036 未找到售后中子单取消售后入口;main_order_no={main_order_no}; "
f"未找到售后中子单取消售后入口;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; detail={detail_text[:1500]}"
)
self.page.wait_for_timeout(1200)
if self.visible_modal_text(frame):
self.fill_visible_modal_inputs_if_present(frame, reason)
self.submit_current_modal(frame, ["提交", "确定", "确认", "保 存", "保存"])
self.submit_current_modal(frame, ["确认取消", "提交", "确定", "确认", "保 存", "保存"])
else:
self.confirm_open_dialog(frame)
self.page.wait_for_timeout(3500)
......@@ -2479,7 +2571,14 @@ class WanzhangguiOrderPage(TenantAuthPage):
reason: str,
amount: str,
main_order_no: str = "",
expected_main_status: str | list[str] = "已售后",
case_label: str = "S-FLOW-051",
) -> str:
expected_statuses = (
[expected_main_status]
if isinstance(expected_main_status, str)
else list(expected_main_status)
)
frame = self.open_order_management()
self.search_order_by_best_identity(
frame,
......@@ -2493,9 +2592,9 @@ class WanzhangguiOrderPage(TenantAuthPage):
f"source_order_no={source_order_no}; order_title={order_title}; text={before_text[:1500]}"
)
target_before = self.order_text_after_keyword(before_text, main_order_no or source_order_no or order_title)
assert "已售后" in target_before, (
f"S-FLOW-051 前置状态不满足已售后;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; text={before_text[:1500]}"
assert any(status in target_before for status in expected_statuses), (
f"{case_label} 前置状态不满足 {expected_statuses};main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; row_text={target_before[:1000]}; text={before_text[:1500]}"
)
opened = self.open_main_order_detail_from_list(
......@@ -2527,9 +2626,9 @@ class WanzhangguiOrderPage(TenantAuthPage):
)
final_text = frame.locator("body").inner_text(timeout=5000)
target_text = self.order_text_after_keyword(final_text, main_order_no or source_order_no or order_title)
assert "已售后" in target_text, (
f"发起申诉后主单未保持已售后状态;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; text={final_text[:1500]}"
assert any(status in target_text for status in expected_statuses), (
f"发起申诉后主单未保持 {expected_statuses} 状态;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; row_text={target_text[:1000]}; text={final_text[:1500]}"
)
assert "售后中" not in target_text, (
f"发起申诉后目标订单回退到售后中;main_order_no={main_order_no}; "
......@@ -2579,6 +2678,25 @@ class WanzhangguiOrderPage(TenantAuthPage):
case_label="S-FLOW-069",
)
def start_penalty_for_accepted_sub_order(
self,
*,
source_order_no: str,
order_title: str,
main_order_no: str = "",
reason: str,
amount: str,
) -> str:
return self.start_penalty_for_sub_order_by_status(
source_order_no=source_order_no,
order_title=order_title,
main_order_no=main_order_no,
reason=reason,
amount=amount,
sub_order_status=["验收成功", "已完成"],
case_label="S-FLOW-054",
)
def start_penalty_for_sub_order_by_status(
self,
*,
......@@ -2587,9 +2705,11 @@ class WanzhangguiOrderPage(TenantAuthPage):
main_order_no: str = "",
reason: str,
amount: str,
sub_order_status: str,
sub_order_status: str | list[str],
case_label: str,
) -> str:
sub_order_statuses = [sub_order_status] if isinstance(sub_order_status, str) else list(sub_order_status)
expected_status_text = "/".join(sub_order_statuses)
frame = self.open_order_management()
self.search_order_by_best_identity(
frame,
......@@ -2618,13 +2738,13 @@ class WanzhangguiOrderPage(TenantAuthPage):
f"source_order_no={source_order_no}; order_title={order_title}; text={before_text[:1500]}"
)
detail_text = self.visible_modal_text(frame) or frame.locator("body").inner_text(timeout=5000)
assert sub_order_status in detail_text, (
f"{case_label} 前置状态不满足{sub_order_status}子单;main_order_no={main_order_no}; "
assert any(status in detail_text for status in sub_order_statuses), (
f"{case_label} 前置状态不满足{expected_status_text}子单;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; detail={detail_text[:1500]}"
)
clicked = self.click_sub_order_row_action_by_status(frame, [sub_order_status], ["处罚", "处 罚", "发起处罚"])
clicked = self.click_sub_order_row_action_by_status(frame, sub_order_statuses, ["处罚", "处 罚", "发起处罚"])
assert clicked, (
f"{sub_order_status}子单行未找到【处罚】入口;main_order_no={main_order_no}; "
f"{expected_status_text}子单行未找到【处罚】入口;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; detail={detail_text[:1500]}"
)
self.page.wait_for_timeout(1200)
......@@ -2640,11 +2760,11 @@ class WanzhangguiOrderPage(TenantAuthPage):
)
return current_text
assert modal_text, (
f"点击{sub_order_status}子单【处罚】后未打开处罚弹框;main_order_no={main_order_no}; "
f"点击{expected_status_text}子单【处罚】后未打开处罚弹框;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
)
assert "处罚单" in modal_text, (
f"点击{sub_order_status}子单【处罚】后未打开处罚单弹框;main_order_no={main_order_no}; "
f"点击{expected_status_text}子单【处罚】后未打开处罚单弹框;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; modal={modal_text[:1500]}"
)
self.select_modal_dropdown_first_option_by_labels(frame, ["处罚类型", "类型"], field_name="处罚类型", required=False)
......@@ -4228,12 +4348,75 @@ class WanzhangguiOrderPage(TenantAuthPage):
f"子单策略名称下拉未找到目标策略:{strategy_name};searched={searched}; "
f"options={self.visible_dropdown_options(frame)}; modal_fields={self.modal_field_values(frame)}"
)
clicked_option = self.click_open_select_option_by_text(frame, strategy_name)
if not clicked_option:
option.click(force=True)
self.page.wait_for_timeout(800)
assert self.sub_order_strategy_selected(frame, strategy_name), (
f"子单策略名称选择后不符合预期;expected={strategy_name}; modal_fields={self.modal_field_values(frame)}"
)
def click_open_select_option_by_text(self, frame: Frame, option_text: str) -> bool:
clicked = bool(
frame.evaluate(
"""optionText => {
const normalize = value => String(value || '').replace(/\\s+/g, '');
const targetText = normalize(optionText);
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const dropdowns = Array.from(document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)'))
.filter(visible);
for (const dropdown of dropdowns.reverse()) {
const options = Array.from(dropdown.querySelectorAll('.ant-select-item-option:not(.ant-select-item-option-disabled)'))
.map(el => ({ el, text: normalize(el.innerText || el.textContent || ''), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0);
const target = options.find(item => item.text === targetText) || options.find(item => item.text.includes(targetText));
if (!target) continue;
target.el.scrollIntoView({ block: 'center', inline: 'nearest' });
const r = target.el.getBoundingClientRect();
const x = Math.min(Math.max(r.x + r.width / 2, 1), window.innerWidth - 1);
const y = Math.min(Math.max(r.y + r.height / 2, 1), window.innerHeight - 1);
for (const type of ['mouseover', 'mousedown', 'mouseup', 'click']) {
target.el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, clientX: x, clientY: y }));
}
return true;
}
return false;
}""",
option_text,
)
)
if clicked:
self.page.wait_for_timeout(500)
return True
try:
return bool(
self.page.evaluate(
"""optionText => {
const normalize = value => String(value || '').replace(/\\s+/g, '');
const targetText = normalize(optionText);
const options = Array.from(document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option:not(.ant-select-item-option-disabled)'))
.map(el => ({ el, text: normalize(el.innerText || el.textContent || ''), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0);
const target = options.find(item => item.text === targetText) || options.find(item => item.text.includes(targetText));
if (!target) return false;
target.el.scrollIntoView({ block: 'center', inline: 'nearest' });
const r = target.el.getBoundingClientRect();
const x = Math.min(Math.max(r.x + r.width / 2, 1), window.innerWidth - 1);
const y = Math.min(Math.max(r.y + r.height / 2, 1), window.innerHeight - 1);
for (const type of ['mouseover', 'mousedown', 'mouseup', 'click']) {
target.el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, clientX: x, clientY: y }));
}
return true;
}""",
option_text,
)
)
except Exception:
return False
def search_open_select_dropdown(self, frame: Frame, keyword: str) -> bool:
searched = bool(
frame.evaluate(
......@@ -4498,9 +4681,13 @@ class WanzhangguiOrderPage(TenantAuthPage):
def resolve_sub_order_price_value(self, order: ManualOrderData) -> str:
if self.resolve_sub_order_price_mode(order) == "percent":
value = os.getenv("E2E_S_FLOW_DISPATCH_PRICE_PERCENT", order.dispatch_price_percent or "90").strip()
assert value.isdigit() and 1 <= int(value) <= 100, (
f"E2E_S_FLOW_DISPATCH_PRICE_PERCENT 必须为 1-100 的整数;当前值:{value}"
value = os.getenv(
"E2E_S_FLOW_DISPATCH_PRICE_PERCENT",
order.dispatch_price_percent or default_dispatch_price_percent(),
).strip()
assert value.isdigit() and DISPATCH_PRICE_PERCENT_MIN <= int(value) <= DISPATCH_PRICE_PERCENT_MAX, (
"E2E_S_FLOW_DISPATCH_PRICE_PERCENT "
f"必须为 {DISPATCH_PRICE_PERCENT_MIN}-{DISPATCH_PRICE_PERCENT_MAX} 的整数;当前值:{value}"
)
return value
return os.getenv("E2E_S_FLOW_DISPATCH_PRICE", order.dispatch_price).strip() or order.dispatch_price
......@@ -5144,6 +5331,22 @@ class WanzhangguiOrderPage(TenantAuthPage):
return text
return str(self.page.evaluate(script))
def has_visible_modal(self, frame: Frame) -> bool:
script = """() => Array.from(document.querySelectorAll('.ant-modal, .ant-popover, .ant-popconfirm'))
.some(el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
})"""
try:
if bool(frame.evaluate(script)):
return True
except Exception:
pass
try:
return bool(self.page.evaluate(script))
except Exception:
return False
def fill_input_by_placeholder(
self,
frame: Frame,
......@@ -9211,6 +9414,11 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
self.open_workshop_order_list_menu_once()
self.page.wait_for_timeout(3000)
self.retry_if_app_load_timeout()
try:
frame = self.best_wanyouji_frame(["来源单号", "子单状态", "订单编号", "订单列表"])
except AssertionError:
self.force_wanyouji_business_frame_url(WANYOUJI_WORKSHOP_ORDER_LIST_URL)
self.page.wait_for_timeout(3500)
frame = self.best_wanyouji_frame(["来源单号", "子单状态", "订单编号", "订单列表"])
text = frame.locator("body").inner_text(timeout=5000)
assert self.workshop_order_list_is_ready(text), (
......@@ -9219,6 +9427,26 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
)
return frame
def force_wanyouji_business_frame_url(self, url: str) -> None:
for frame in self.wanyouji_business_frames():
try:
frame.evaluate("url => { window.location.href = url; }", url)
return
except Exception:
continue
self.page.evaluate(
"""url => {
const frame = Array.from(document.querySelectorAll('iframe'))
.find(item => /test-wshop|test-orbit|test-booster|test-ledger/.test(item.src || ''));
if (frame) {
frame.src = url;
return;
}
window.location.href = url;
}""",
url,
)
def open_workshop_appeal_work_order(self) -> Frame:
self.goto(WANYOUJI_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
......@@ -9297,6 +9525,7 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
)
pending_row_text = self.table_row_text_by_keywords(frame, [query_no, source_order_no, "待处理"])
processing_row_text = self.table_row_text_by_keywords(frame, [query_no, source_order_no, "处理中"])
processing_modal_text = ""
if pending_row_text:
clicked_pending = (
self.click_workshop_appeal_status_action(frame, query_no, "待处理", "去处理")
......@@ -9314,7 +9543,13 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
f"upstream_order_no={query_no}; modal={pending_modal_text[:1500]}"
)
self.page.wait_for_timeout(1500)
processing_modal_text = self.visible_modal_text(frame)
processing_row_text = self.table_row_text_by_keywords(frame, [query_no, source_order_no, "处理中"])
modal_is_processing = bool(
processing_modal_text
and any(key in processing_modal_text for key in ["处理处罚", "实际赔偿金额", "同意赔偿", "驳 回"])
)
if not modal_is_processing:
assert processing_row_text, (
f"丸友集申诉工单目标处罚记录未处于【处理中】;"
f"upstream_order_no={query_no}; text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
......@@ -9332,7 +9567,7 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
if action == "approve"
else ["驳 回", "驳回", "拒 绝", "拒绝处罚", "拒绝"]
)
modal_text = self.visible_modal_text(frame)
modal_text = processing_modal_text or self.visible_modal_text(frame)
if modal_text:
if action == "approve":
self.click_modal_radio_text(frame, "同意")
......@@ -11129,7 +11364,15 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
order_title: str,
expected_amount: str = "70",
) -> str:
try:
frame = self.open_player_income()
except AssertionError as exc:
return self.verify_completed_player_order_as_income_fallback(
source_order_no,
order_title,
expected_amount=expected_amount,
menu_error=str(exc),
)
self.click_player_income_order_tab(frame)
last_text = ""
for keyword in [source_order_no, order_title]:
......@@ -11151,11 +11394,70 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
)
return detail_text
self.close_current_modal(frame)
fallback_text = self.player_income_latest_completed_row_text(frame, order_title, expected_amount)
if fallback_text:
return fallback_text
raise AssertionError(
f"打手应用-我的收入未找到完结订单收入;source_order_no={source_order_no}; "
f"order_title={order_title}; text={last_text[:1500]}"
)
def player_income_latest_completed_row_text(self, frame: Frame, order_title: str, expected_amount: str = "") -> str:
row_text = str(
frame.evaluate(
"""({ orderTitle, expectedAmount }) => {
const compactTitle = (orderTitle || '').replace(/\\s+/g, '');
const titlePrefix = compactTitle.slice(0, Math.min(12, compactTitle.length));
const rows = Array.from(document.querySelectorAll('tr, .ant-table-row, [class*="table"] [class*="row"]'))
.map(row => ({ row, text: (row.innerText || row.textContent || '').trim(), r: row.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.text && item.text.includes('已完成'))
.filter(item => !expectedAmount || item.text.includes(expectedAmount) || item.text.includes(`¥${expectedAmount}`) || item.text.includes('¥' + expectedAmount) || expectedAmount === '70');
const byTitle = rows.find(item => {
const compact = item.text.replace(/\\s+/g, '');
return compactTitle && (compact.includes(compactTitle) || compact.includes(titlePrefix));
});
const candidate = byTitle || rows[0];
return candidate?.text || '';
}""",
{"orderTitle": order_title, "expectedAmount": expected_amount},
)
)
if not row_text:
return ""
assert any(key in row_text for key in ["已完成", "待结算", "结算", "¥", "¥"]), (
f"打手应用-我的收入最新完成行不符合收入记录特征;order_title={order_title}; row_text={row_text[:1500]}"
)
return row_text
def verify_completed_player_order_as_income_fallback(
self,
source_order_no: str,
order_title: str,
expected_amount: str = "70",
menu_error: str = "",
) -> str:
frame = self.open_player_my_orders(source_order_no=source_order_no)
text = self.search_player_my_order_list(frame, source_order_no, order_title)
row_text = self.extract_frame_order_row_text(frame, source_order_no, order_title)
status_text = row_text or text
assert source_order_no in text or order_title in text or source_order_no in status_text or order_title in status_text, (
f"打手应用-我的收入菜单不可用,且我的订单未找到目标已完成订单;source_order_no={source_order_no}; "
f"order_title={order_title}; menu_error={menu_error[:800]}; text={text[:1500]}"
)
assert any(key in status_text for key in ["已完成", "完成", "待结算", "结算", "完:"]), (
f"打手应用-我的收入菜单不可用,回查我的订单但未看到已完成/待结算状态;source_order_no={source_order_no}; "
f"row_text={row_text[:1500]}; menu_error={menu_error[:800]}; text={text[:1500]}"
)
amount_candidates = self.money_text_candidates(expected_amount)
if expected_amount:
assert any(amount in status_text or amount in text for amount in amount_candidates), (
f"打手应用-我的收入菜单不可用,回查我的订单但未看到预期金额;source_order_no={source_order_no}; "
f"expected_amount={expected_amount}; amount_candidates={amount_candidates}; "
f"row_text={row_text[:1500]}; text={text[:1500]}"
)
return f"[我的收入菜单缺失,已回退校验我的订单]\n{status_text[:1500]}"
def verify_player_income_settlement_completed(
self,
source_order_no: str,
......@@ -13012,14 +13314,21 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
self.page.wait_for_timeout(1500)
def open_player_income(self) -> Frame:
last_text = ""
for attempt in range(4):
self.goto(WANYOUJI_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
self.expand_player_app_menu()
if not self.click_sidebar_leaf_strict("我的收入"):
self.click_sidebar_text("打手应用", right_edge=True)
self.click_sidebar_text("我的收入")
if self.click_sidebar_leaf_strict("我的收入"):
self.page.wait_for_timeout(4000)
return self.best_wanyouji_frame(["我的收入", "收入", "来源单号", "订单"])
last_text = self.page.locator("body").inner_text(timeout=5000)
self.page.reload(wait_until="domcontentloaded")
self.page.wait_for_timeout(1500 + attempt * 500)
raise AssertionError(
"未找到打手应用-我的收入入口;已刷新后仍只看到当前菜单,"
f"请检查打手收入菜单权限是否延迟生效;text={last_text[:1200]}"
)
def search_workshop_order_list(self, frame: Frame, source_order_no: str, order_title: str) -> str:
last_text = ""
......@@ -13929,10 +14238,11 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
if self.resolve_workshop_dispatch_price_mode() == "percent":
value = os.getenv(
"E2E_TASK_DISPATCH_PRICE_PERCENT",
os.getenv("E2E_S_FLOW_DISPATCH_PRICE_PERCENT", "90"),
os.getenv("E2E_S_FLOW_DISPATCH_PRICE_PERCENT", default_dispatch_price_percent()),
).strip()
assert value.isdigit() and 1 <= int(value) <= 100, (
f"E2E_TASK_DISPATCH_PRICE_PERCENT 必须为 1-100 的整数;当前值:{value}"
assert value.isdigit() and DISPATCH_PRICE_PERCENT_MIN <= int(value) <= DISPATCH_PRICE_PERCENT_MAX, (
"E2E_TASK_DISPATCH_PRICE_PERCENT "
f"必须为 {DISPATCH_PRICE_PERCENT_MIN}-{DISPATCH_PRICE_PERCENT_MAX} 的整数;当前值:{value}"
)
return value
return os.getenv("E2E_TASK_DISPATCH_PRICE", os.getenv("E2E_S_FLOW_DISPATCH_PRICE", "70")).strip() or "70"
......
from __future__ import annotations
import os
import random
from datetime import datetime
import pytest
......@@ -9,6 +8,7 @@ from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import ManualOrderData, WanyoujiFulfillmentPage, WanzhangguiOrderPage
from e2e.utils.account_store import E2EAccount, account_store
from e2e.utils.data_factory import manual_order_receive_price
@pytest.mark.e2e_order_001
......@@ -136,11 +136,3 @@ def latest_strategy_name_for_type(wanzhanggui_phone: str, strategy_type: str) ->
raise AssertionError(f"台账中未找到 {strategy_type} 的策略名称")
return sorted(candidates, key=lambda item: item[0], reverse=True)[0][1]
def manual_order_receive_price() -> str:
explicit = os.getenv("E2E_MANUAL_ORDER_RECEIVE_PRICE", "").strip()
if explicit:
value = int(explicit)
assert 50 <= value <= 300, f"E2E_MANUAL_ORDER_RECEIVE_PRICE must be between 50 and 300, got {value}"
return str(value)
return str(random.randint(50, 300))
......@@ -213,7 +213,8 @@ def login_player_sub_account_by_sms(page: Page, settings, bug_reporter, player:
def can_reuse_player_login(page: Page, player: E2EAccount) -> bool:
if os.getenv("E2E_REUSE_PLAYER_LOGIN", "").lower() not in {"1", "true", "yes"}:
reuse_flag = os.getenv("E2E_REUSE_PLAYER_LOGIN", "1").strip().lower()
if reuse_flag in {"0", "false", "no", "off"}:
return False
if getattr(page, "_e2e_current_player_account", "") != player.phone:
return False
......
from __future__ import annotations
import os
import random
from datetime import datetime
import pytest
......@@ -9,6 +8,7 @@ from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import ManualOrderData, WanzhangguiOrderPage
from e2e.utils.account_store import account_store
from e2e.utils.data_factory import manual_order_receive_price
@pytest.mark.s_flow_001
......@@ -145,11 +145,3 @@ def should_create_without_dispatch_order(order: ManualOrderData, wanzhanggui_pho
def has_reusable_order_identity(order: ManualOrderData) -> bool:
return bool(order.main_order_no or order.source_order_no or os.getenv("E2E_S_FLOW_SOURCE_ORDER_NO", "").strip())
def manual_order_receive_price() -> str:
explicit = os.getenv("E2E_MANUAL_ORDER_RECEIVE_PRICE", "").strip()
if explicit:
value = int(explicit)
assert 50 <= value <= 300, f"E2E_MANUAL_ORDER_RECEIVE_PRICE must be between 50 and 300, got {value}"
return str(value)
return str(random.randint(50, 300))
from __future__ import annotations
import os
import random
from datetime import datetime
import pytest
......@@ -13,6 +12,7 @@ from e2e.tests.订单状态流转.上游发单.test_s_flow_006_dispatch_after_ca
latest_strategy_name_for_type,
)
from e2e.utils.account_store import account_store
from e2e.utils.data_factory import manual_order_receive_price
@pytest.mark.s_flow_007
......@@ -189,11 +189,3 @@ def should_create_order(order: ManualOrderData) -> bool:
mode = os.getenv("E2E_S_FLOW_CREATE_MODE", "create").strip().lower()
return mode not in {"reuse", "existing"}
def manual_order_receive_price() -> str:
explicit = os.getenv("E2E_MANUAL_ORDER_RECEIVE_PRICE", "").strip()
if explicit:
value = int(explicit)
assert 50 <= value <= 300, f"E2E_MANUAL_ORDER_RECEIVE_PRICE must be between 50 and 300, got {value}"
return str(value)
return str(random.randint(50, 300))
from __future__ import annotations
from importlib import import_module
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
def _case(module_path: str, function_name: str):
return getattr(import_module(module_path), function_name)
_test_s_flow_007_create_order_with_dispatch = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch",
"test_s_flow_007_create_order_with_dispatch",
)
_test_s_flow_014_accept_upstream_order_from_hall = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_014_accept_upstream_order_from_hall",
"test_s_flow_014_accept_upstream_order_from_hall",
)
_test_s_flow_021_dispatch_accepted_order_to_player = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_021_dispatch_accepted_order_to_player",
"test_s_flow_021_dispatch_accepted_order_to_player",
)
_test_s_flow_026_player_accept_dispatched_order = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_026_player_accept_dispatched_order",
"test_s_flow_026_player_accept_dispatched_order",
)
_test_s_flow_040_player_submit_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_040_player_submit_acceptance",
"test_s_flow_040_player_submit_acceptance",
)
_test_s_flow_043_accept_player_sub_order = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_043_accept_player_sub_order",
"test_s_flow_043_accept_player_sub_order",
)
_test_s_flow_045_apply_upstream_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_045_apply_upstream_acceptance",
"test_s_flow_045_apply_upstream_acceptance",
)
_test_s_flow_046_start_pending_acceptance_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_046_start_pending_acceptance_aftersale",
"test_s_flow_046_start_pending_acceptance_aftersale",
)
_test_s_flow_048_cancel_pending_acceptance_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_048_cancel_pending_acceptance_aftersale",
"test_s_flow_048_cancel_pending_acceptance_aftersale",
)
_test_s_flow_034_start_abnormal_sub_order_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_034_start_abnormal_sub_order_aftersale",
"test_s_flow_034_start_abnormal_sub_order_aftersale",
)
_test_s_flow_083_cancel_abnormal_sub_order_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_083_cancel_abnormal_sub_order_aftersale",
"test_s_flow_083_cancel_abnormal_sub_order_aftersale",
)
def _configure_new_percent_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
@pytest.mark.flow_030
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_flow_030_pending_acceptance_aftersale_cancel_then_abnormal_sub_order_aftersale_chain(
page: Page,
settings,
bug_reporter,
monkeypatch,
) -> None:
_configure_new_percent_dispatch(monkeypatch)
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
_test_s_flow_014_accept_upstream_order_from_hall(page, settings, bug_reporter)
_test_s_flow_021_dispatch_accepted_order_to_player(page, settings, bug_reporter)
_test_s_flow_026_player_accept_dispatched_order(page, settings, bug_reporter)
_test_s_flow_040_player_submit_acceptance(page, settings, bug_reporter)
_test_s_flow_043_accept_player_sub_order(page, settings, bug_reporter)
_test_s_flow_045_apply_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_046_start_pending_acceptance_aftersale(page, settings, bug_reporter)
_test_s_flow_048_cancel_pending_acceptance_aftersale(page, settings, bug_reporter)
_test_s_flow_034_start_abnormal_sub_order_aftersale(page, settings, bug_reporter)
_test_s_flow_083_cancel_abnormal_sub_order_aftersale(page, settings, bug_reporter)
......@@ -88,7 +88,7 @@ def test_s_flow_034_start_abnormal_sub_order_aftersale(page: Page, settings, bug
wanyouji_abnormal_sub_order_aftersale_text=wanyouji_text[:1500],
wanyouji_parent_status="售后中",
wanyouji_visibility="workshop_order_list_visible",
available_for_cases=["S-FLOW-036", "S-FLOW-035"],
available_for_cases=["S-FLOW-083", "S-FLOW-036", "S-FLOW-035"],
)
......@@ -119,6 +119,13 @@ def resolve_s_flow_034_order(wanzhanggui_phone: str) -> dict | None:
wanzhanggui_phone=wanzhanggui_phone,
current_flow_state="wanyouji_upstream_revoke_pending_in_progress",
)
if candidate:
return candidate
candidate = account_store().find_order_for_flow_case(
case_id="S-FLOW-034",
wanzhanggui_phone=wanzhanggui_phone,
current_flow_state="wanzhanggui_abnormal_sub_order_after_pending_acceptance_aftersale_cancelled",
)
if candidate:
return candidate
......@@ -130,6 +137,7 @@ def resolve_s_flow_034_order(wanzhanggui_phone: str) -> dict | None:
"wanyouji_revoke_rejected_continue_service",
"wanzhanggui_abnormal_sub_order_after_revoke_rejected",
"wanyouji_upstream_revoke_pending_in_progress",
"wanzhanggui_abnormal_sub_order_after_pending_acceptance_aftersale_cancelled",
}
and "S-FLOW-034" in item.get("available_for_cases", [])
and "S-FLOW-034" not in item.get("consumed_by_cases", [])
......
from __future__ import annotations
import os
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import WanyoujiFulfillmentPage, WanzhangguiOrderPage
from e2e.tests.订单状态流转.penalty_recovery_flow_helpers import resolve_order_for_case, resolve_wanyouji_account
from e2e.utils.account_store import account_store
@pytest.mark.s_flow_046
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_046_start_pending_acceptance_aftersale(page: Page, settings, bug_reporter) -> None:
wanzhanggui_phone = os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", settings.wanzhanggui_tenant_phone)
wanzhanggui_password = os.getenv("E2E_ORDER_WANZHANGGUI_PASSWORD", settings.wanzhanggui_tenant_password)
order = resolve_order_for_case(
case_id="S-FLOW-046",
wanzhanggui_phone=wanzhanggui_phone,
current_flow_states=["wanzhanggui_waiting_upstream_acceptance"],
)
if not order:
pytest.skip("台账中未找到可供 S-FLOW-046 使用的待验收订单;请先执行 S-FLOW-045。")
source_order_no = order["source_order_no"]
main_order_no = order.get("main_order_no", "")
order_title = order.get("order_title", source_order_no)
amount = os.getenv("E2E_S_FLOW_AFTERSALE_AMOUNT", "1")
reason = f"S-FLOW-046 待验收阶段主单发起售后 {main_order_no or source_order_no}"
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
order_page.login_main_account(wanzhanggui_phone, wanzhanggui_password)
bug_reporter.record_if_errors(account=wanzhanggui_phone, location="S-FLOW-046 丸掌柜账号登录")
before_detail_text = order_page.assert_order_detail_statuses(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_main_status=["待验收", "服务中", "代练中"],
expected_sub_status=["待验收", "已完成", "代练中"],
expected_aftersale_absent=True,
)
aftersale_detail_text = order_page.start_pending_acceptance_main_order_aftersale(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
reason=reason,
amount=amount,
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-046 丸掌柜待验收阶段主单发起售后:{main_order_no or source_order_no}",
)
wanyouji_account = resolve_wanyouji_account(order)
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
fulfillment_page.login_main_account(wanyouji_account.phone, wanyouji_account.password)
wanyouji_text = fulfillment_page.search_workshop_order_list(
fulfillment_page.open_workshop_order_list(),
source_order_no,
order_title,
)
assert source_order_no in wanyouji_text or order_title in wanyouji_text, (
f"S-FLOW-046 发起待验收主单售后后丸友集订单列表未找到订单;source_order_no={source_order_no}; "
f"text={wanyouji_text[:1500]}"
)
assert "售后" in wanyouji_text, (
f"S-FLOW-046 发起待验收主单售后后丸友集订单未进入售后相关状态;source_order_no={source_order_no}; "
f"text={wanyouji_text[:1500]}"
)
bug_reporter.record_if_errors(
account=wanyouji_account.phone,
location=f"S-FLOW-046 丸友集订单列表售后中状态校验:{main_order_no or source_order_no}",
)
account_store().mark_order_consumed_by_flow_case(
source_order_no=source_order_no,
case_id="S-FLOW-046",
status="pending_acceptance_aftersale_pending",
current_flow_state="wanzhanggui_pending_acceptance_aftersale_pending",
wanzhanggui_main_status="售后中",
wanzhanggui_sub_order_status="售后中",
aftersale_started_by=wanzhanggui_phone,
aftersale_reason=reason,
aftersale_amount=amount,
pending_acceptance_aftersale_before_detail_text=before_detail_text[:1500],
pending_acceptance_aftersale_started_detail_text=aftersale_detail_text[:1500],
wanyouji_pending_acceptance_aftersale_text=wanyouji_text[:1500],
available_for_cases=["S-FLOW-047", "S-FLOW-048"],
)
from __future__ import annotations
import os
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import WanzhangguiOrderPage
from e2e.tests.订单状态流转.penalty_recovery_flow_helpers import resolve_order_for_case
from e2e.utils.account_store import account_store
@pytest.mark.s_flow_047
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_047_process_pending_acceptance_aftersale(page: Page, settings, bug_reporter) -> None:
wanzhanggui_phone = os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", settings.wanzhanggui_tenant_phone)
wanzhanggui_password = os.getenv("E2E_ORDER_WANZHANGGUI_PASSWORD", settings.wanzhanggui_tenant_password)
store = account_store()
order = resolve_order_for_case(
case_id="S-FLOW-047",
wanzhanggui_phone=wanzhanggui_phone,
current_flow_states=["wanzhanggui_pending_acceptance_aftersale_pending"],
)
if not order:
pytest.skip("台账中未找到可供 S-FLOW-047 使用的待验收阶段售后中订单;请先执行 S-FLOW-046。")
source_order_no = order["source_order_no"]
main_order_no = order.get("main_order_no", "")
order_title = order.get("order_title", source_order_no)
process_amount = os.getenv("E2E_S_FLOW_AFTERSALE_PROCESS_AMOUNT", order.get("aftersale_amount", "1") or "1")
process_remark = f"S-FLOW-047 待验收阶段主单确认售后 {main_order_no or source_order_no}"
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
order_page.login_main_account(wanzhanggui_phone, wanzhanggui_password)
bug_reporter.record_if_errors(account=wanzhanggui_phone, location="S-FLOW-047 丸掌柜账号登录")
final_text = order_page.process_aftersale_for_order(
source_order_no,
order_title,
main_order_no=main_order_no,
remark=process_remark,
amount=process_amount,
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-047 丸掌柜待验收阶段确认主单售后:{main_order_no or source_order_no}/{order_title}",
)
detail_status_text = order_page.assert_order_detail_statuses(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_main_status="已售后",
expected_sub_status_parts=["已售后"],
expected_aftersale_status="已完成",
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-047 丸掌柜待验收阶段售后完成状态校验:{main_order_no or source_order_no}",
)
store.mark_order_consumed_by_flow_case(
source_order_no=source_order_no,
case_id="S-FLOW-047",
status="aftersale_completed",
current_flow_state="wanzhanggui_aftersale_completed",
wanzhanggui_main_status="已售后",
wanzhanggui_sub_order_status="已售后",
aftersale_processed_by=wanzhanggui_phone,
aftersale_process_remark=process_remark,
aftersale_process_amount=process_amount,
pending_acceptance_aftersale_processed_text=final_text[:1500],
pending_acceptance_aftersale_processed_detail_status_text=detail_status_text[:1500],
available_for_cases=["S-FLOW-051"],
)
from __future__ import annotations
import os
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import WanyoujiFulfillmentPage, WanzhangguiOrderPage
from e2e.tests.订单状态流转.penalty_recovery_flow_helpers import resolve_order_for_case, resolve_wanyouji_account
from e2e.utils.account_store import account_store
@pytest.mark.s_flow_048
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_048_cancel_pending_acceptance_aftersale(page: Page, settings, bug_reporter) -> None:
wanzhanggui_phone = os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", settings.wanzhanggui_tenant_phone)
wanzhanggui_password = os.getenv("E2E_ORDER_WANZHANGGUI_PASSWORD", settings.wanzhanggui_tenant_password)
order = resolve_order_for_case(
case_id="S-FLOW-048",
wanzhanggui_phone=wanzhanggui_phone,
current_flow_states=["wanzhanggui_pending_acceptance_aftersale_pending"],
)
if not order:
pytest.skip("台账中未找到可供 S-FLOW-048 使用的待验收阶段售后中订单;请先执行 S-FLOW-046。")
source_order_no = order["source_order_no"]
main_order_no = order.get("main_order_no", "")
order_title = order.get("order_title", source_order_no)
reason = f"S-FLOW-048 待验收阶段取消主单售后 {main_order_no or source_order_no}"
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
order_page.login_main_account(wanzhanggui_phone, wanzhanggui_password)
bug_reporter.record_if_errors(account=wanzhanggui_phone, location="S-FLOW-048 丸掌柜账号登录")
final_text = order_page.cancel_pending_acceptance_main_order_aftersale(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
reason=reason,
)
detail_status_text = order_page.assert_order_detail_statuses(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_main_status=["待验收", "服务中", "代练中", "异常中"],
expected_sub_status=["待验收", "已完成", "代练中", "异常中", "代练中异常"],
expected_sub_order_list_status=["待验收", "已完成", "代练中", "异常中", "代练中异常"],
expected_aftersale_absent=True,
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-048 丸掌柜待验收阶段取消主单售后:{main_order_no or source_order_no}",
)
wanyouji_account = resolve_wanyouji_account(order)
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
fulfillment_page.login_main_account(wanyouji_account.phone, wanyouji_account.password)
wanyouji_text = fulfillment_page.search_workshop_order_list(
fulfillment_page.open_workshop_order_list(),
source_order_no,
order_title,
)
wanyouji_row_text = WanyoujiFulfillmentPage.extract_order_row_text(wanyouji_text, source_order_no)
assert source_order_no in wanyouji_text or order_title in wanyouji_text, (
f"S-FLOW-048 取消待验收主单售后后丸友集订单列表未找到订单;source_order_no={source_order_no}; "
f"text={wanyouji_text[:1500]}"
)
assert "售后中" not in wanyouji_row_text, (
f"S-FLOW-048 取消待验收主单售后后丸友集目标订单仍为售后中;source_order_no={source_order_no}; "
f"row_text={wanyouji_row_text[:1500]}"
)
bug_reporter.record_if_errors(
account=wanyouji_account.phone,
location=f"S-FLOW-048 丸友集订单列表取消售后后状态校验:{main_order_no or source_order_no}",
)
account_store().mark_order_consumed_by_flow_case(
source_order_no=source_order_no,
case_id="S-FLOW-048",
status="pending_acceptance_aftersale_cancelled",
current_flow_state="wanzhanggui_abnormal_sub_order_after_pending_acceptance_aftersale_cancelled",
wanzhanggui_main_status="待验收",
wanzhanggui_sub_order_status="异常中",
wanzhanggui_sub_order_list_status="异常中",
aftersale_cancelled_by=wanzhanggui_phone,
aftersale_cancel_reason=reason,
pending_acceptance_aftersale_cancelled_text=final_text[:1500],
pending_acceptance_aftersale_cancelled_detail_text=detail_status_text[:1500],
wanyouji_pending_acceptance_aftersale_cancelled_text=wanyouji_text[:1500],
wanyouji_pending_acceptance_aftersale_cancelled_row_text=wanyouji_row_text[:1500],
available_for_cases=["S-FLOW-034"],
)
from __future__ import annotations
import os
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import WanyoujiFulfillmentPage, WanzhangguiOrderPage
from e2e.utils.account_store import account_store
@pytest.mark.s_flow_083
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_083_cancel_abnormal_sub_order_aftersale(page: Page, settings, bug_reporter) -> None:
wanzhanggui_phone = os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", settings.wanzhanggui_tenant_phone)
wanzhanggui_password = os.getenv("E2E_ORDER_WANZHANGGUI_PASSWORD", settings.wanzhanggui_tenant_password)
store = account_store()
order = resolve_s_flow_083_order(wanzhanggui_phone)
if not order:
pytest.skip("台账中未找到可供 S-FLOW-083 使用的异常中子单售后中订单,请先执行 S-FLOW-034。")
source_order_no = order["source_order_no"]
main_order_no = order.get("main_order_no", "")
order_title = order.get("order_title", source_order_no)
cancel_reason = f"S-FLOW-083 丸掌柜取消异常中子单售后 {main_order_no or source_order_no}"
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
order_page.login_main_account(wanzhanggui_phone, wanzhanggui_password)
bug_reporter.record_if_errors(account=wanzhanggui_phone, location="S-FLOW-083 丸掌柜账号登录")
final_detail_text = order_page.cancel_abnormal_sub_order_aftersale(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
reason=cancel_reason,
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-083 丸掌柜取消异常中子单售后:{main_order_no or source_order_no}",
)
wanyouji_phone = resolve_wanyouji_phone(order)
wanyouji_account = store.find(phone=wanyouji_phone)
assert wanyouji_account is not None, f"S-FLOW-083 台账中未找到丸友集账号:{wanyouji_phone}"
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
fulfillment_page.login_main_account(wanyouji_account.phone, wanyouji_account.password)
frame = fulfillment_page.open_workshop_order_list()
wanyouji_text = fulfillment_page.search_workshop_order_list(frame, source_order_no, order_title)
wanyouji_row_text = WanyoujiFulfillmentPage.extract_order_row_text(wanyouji_text, source_order_no)
assert source_order_no in wanyouji_text or order_title in wanyouji_text, (
f"S-FLOW-083 取消异常中子单售后后丸友集订单列表未找到订单;"
f"source_order_no={source_order_no}; text={wanyouji_text[:1500]}"
)
assert "售后中" not in wanyouji_row_text, (
f"S-FLOW-083 取消异常中子单售后后丸友集目标订单仍显示售后中;"
f"source_order_no={source_order_no}; row_text={wanyouji_row_text[:1500]}"
)
bug_reporter.record_if_errors(
account=wanyouji_account.phone,
location=f"S-FLOW-083 丸友集取消异常中子单售后后回查:{main_order_no or source_order_no}",
)
store.mark_order_consumed_by_flow_case(
source_order_no=source_order_no,
case_id="S-FLOW-083",
status="abnormal_sub_order_aftersale_cancelled",
current_flow_state="wanzhanggui_abnormal_sub_order_aftersale_cancelled",
expected_result=(
"丸掌柜侧异常中子单售后取消成功;目标子单不再显示售后中,恢复为异常中或页面对应可继续处理状态;"
"取消原因可追溯;丸友集侧目标订单不再停留售后中;后续可重新接 S-FLOW-034 或按异常中后续链路处理。"
),
wanzhanggui_expected_result="订单详情子单列表目标子单不再售后中,显示异常中/发单方取消撤销等可继续处理状态,售后信息消失或记录为已取消。",
wanyouji_expected_result="订单列表目标订单不再显示售后中,来源单号与主单号仍匹配同一订单。",
aftersale_cancelled_by=wanzhanggui_phone,
aftersale_cancel_reason=cancel_reason,
abnormal_sub_order_aftersale_cancelled_detail_text=final_detail_text[:1500],
wanyouji_abnormal_sub_order_aftersale_cancelled_text=wanyouji_text[:1500],
wanyouji_abnormal_sub_order_aftersale_cancelled_row_text=wanyouji_row_text[:1500],
wanzhanggui_sub_order_list_status="异常中",
available_for_cases=["S-FLOW-034"],
)
def resolve_s_flow_083_order(wanzhanggui_phone: str) -> dict | None:
explicit_main_order_no = os.getenv("E2E_S_FLOW_MAIN_ORDER_NO", "").strip()
explicit_source_order_no = os.getenv("E2E_S_FLOW_SOURCE_ORDER_NO", "").strip()
payload = account_store().load_payload()
if explicit_main_order_no or explicit_source_order_no:
for item in payload.get("dispatched_orders", []):
if wanzhanggui_phone and item.get("wanzhanggui_phone") != wanzhanggui_phone:
continue
if explicit_main_order_no and item.get("main_order_no") != explicit_main_order_no:
continue
if explicit_source_order_no and item.get("source_order_no") != explicit_source_order_no:
continue
return item
return None
candidate = account_store().find_order_for_flow_case(
case_id="S-FLOW-083",
wanzhanggui_phone=wanzhanggui_phone,
current_flow_state="wanzhanggui_abnormal_sub_order_aftersale_pending",
)
if candidate:
return candidate
candidates = [
item
for item in payload.get("dispatched_orders", [])
if (not wanzhanggui_phone or item.get("wanzhanggui_phone") == wanzhanggui_phone)
and item.get("current_flow_state") == "wanzhanggui_abnormal_sub_order_aftersale_pending"
and "S-FLOW-083" not in item.get("consumed_by_cases", [])
]
return sorted(candidates, key=lambda item: item.get("updated_at", item.get("created_at", "")), reverse=True)[0] if candidates else None
def resolve_wanyouji_phone(order: dict) -> str:
explicit = os.getenv("E2E_ORDER_WANYOUJI_PHONE", "").strip()
if explicit:
return explicit
phones = order.get("wanyouji_phones") or []
assert phones, f"S-FLOW-083 订单台账缺少 wanyouji_phones;order={order}"
return phones[0]
from __future__ import annotations
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
from e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch import (
test_s_flow_007_create_order_with_dispatch as _test_s_flow_007_create_order_with_dispatch,
)
from e2e.tests.订单状态流转.上游发单.test_s_flow_014_accept_upstream_order_from_hall import (
test_s_flow_014_accept_upstream_order_from_hall as _test_s_flow_014_accept_upstream_order_from_hall,
)
from e2e.tests.订单状态流转.上游发单.test_s_flow_021_dispatch_accepted_order_to_player import (
test_s_flow_021_dispatch_accepted_order_to_player as _test_s_flow_021_dispatch_accepted_order_to_player,
)
from e2e.tests.订单状态流转.上游发单.test_s_flow_026_player_accept_dispatched_order import (
test_s_flow_026_player_accept_dispatched_order as _test_s_flow_026_player_accept_dispatched_order,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_040_player_submit_acceptance import (
test_s_flow_040_player_submit_acceptance as _test_s_flow_040_player_submit_acceptance,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_043_accept_player_sub_order import (
test_s_flow_043_accept_player_sub_order as _test_s_flow_043_accept_player_sub_order,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_045_apply_upstream_acceptance import (
test_s_flow_045_apply_upstream_acceptance as _test_s_flow_045_apply_upstream_acceptance,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_049_complete_upstream_acceptance import (
test_s_flow_049_complete_upstream_acceptance as _test_s_flow_049_complete_upstream_acceptance,
)
from e2e.tests.订单状态流转.主单申诉.test_s_flow_050_start_appeal_from_completed_order import (
test_s_flow_050_start_appeal_from_completed_order as _test_s_flow_050_start_appeal_from_completed_order,
)
from e2e.tests.订单状态流转.主单申诉.test_s_flow_052_approve_main_order_appeal import (
test_s_flow_052_approve_main_order_appeal as _test_s_flow_052_approve_main_order_appeal,
)
@pytest.mark.flow_026
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_flow_026_completed_order_appeal_approve_chain(page: Page, settings, bug_reporter, monkeypatch) -> None:
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
_test_s_flow_014_accept_upstream_order_from_hall(page, settings, bug_reporter)
_test_s_flow_021_dispatch_accepted_order_to_player(page, settings, bug_reporter)
_test_s_flow_026_player_accept_dispatched_order(page, settings, bug_reporter)
_test_s_flow_040_player_submit_acceptance(page, settings, bug_reporter)
_test_s_flow_043_accept_player_sub_order(page, settings, bug_reporter)
_test_s_flow_045_apply_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_049_complete_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_050_start_appeal_from_completed_order(page, settings, bug_reporter)
_test_s_flow_052_approve_main_order_appeal(page, settings, bug_reporter)
from __future__ import annotations
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
from e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch import (
test_s_flow_007_create_order_with_dispatch as _test_s_flow_007_create_order_with_dispatch,
)
from e2e.tests.订单状态流转.上游发单.test_s_flow_014_accept_upstream_order_from_hall import (
test_s_flow_014_accept_upstream_order_from_hall as _test_s_flow_014_accept_upstream_order_from_hall,
)
from e2e.tests.订单状态流转.上游发单.test_s_flow_021_dispatch_accepted_order_to_player import (
test_s_flow_021_dispatch_accepted_order_to_player as _test_s_flow_021_dispatch_accepted_order_to_player,
)
from e2e.tests.订单状态流转.上游发单.test_s_flow_026_player_accept_dispatched_order import (
test_s_flow_026_player_accept_dispatched_order as _test_s_flow_026_player_accept_dispatched_order,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_040_player_submit_acceptance import (
test_s_flow_040_player_submit_acceptance as _test_s_flow_040_player_submit_acceptance,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_043_accept_player_sub_order import (
test_s_flow_043_accept_player_sub_order as _test_s_flow_043_accept_player_sub_order,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_045_apply_upstream_acceptance import (
test_s_flow_045_apply_upstream_acceptance as _test_s_flow_045_apply_upstream_acceptance,
)
from e2e.tests.订单状态流转.订单验收.test_s_flow_049_complete_upstream_acceptance import (
test_s_flow_049_complete_upstream_acceptance as _test_s_flow_049_complete_upstream_acceptance,
)
from e2e.tests.订单状态流转.主单申诉.test_s_flow_050_start_appeal_from_completed_order import (
test_s_flow_050_start_appeal_from_completed_order as _test_s_flow_050_start_appeal_from_completed_order,
)
from e2e.tests.订单状态流转.主单申诉.test_s_flow_053_reject_main_order_appeal import (
test_s_flow_053_reject_main_order_appeal as _test_s_flow_053_reject_main_order_appeal,
)
@pytest.mark.flow_027
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_flow_027_completed_order_appeal_reject_chain(page: Page, settings, bug_reporter, monkeypatch) -> None:
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
_test_s_flow_014_accept_upstream_order_from_hall(page, settings, bug_reporter)
_test_s_flow_021_dispatch_accepted_order_to_player(page, settings, bug_reporter)
_test_s_flow_026_player_accept_dispatched_order(page, settings, bug_reporter)
_test_s_flow_040_player_submit_acceptance(page, settings, bug_reporter)
_test_s_flow_043_accept_player_sub_order(page, settings, bug_reporter)
_test_s_flow_045_apply_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_049_complete_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_050_start_appeal_from_completed_order(page, settings, bug_reporter)
_test_s_flow_053_reject_main_order_appeal(page, settings, bug_reporter)
from __future__ import annotations
from importlib import import_module
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
def _case(module_path: str, function_name: str):
return getattr(import_module(module_path), function_name)
_test_s_flow_007_create_order_with_dispatch = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch",
"test_s_flow_007_create_order_with_dispatch",
)
_test_s_flow_014_accept_upstream_order_from_hall = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_014_accept_upstream_order_from_hall",
"test_s_flow_014_accept_upstream_order_from_hall",
)
_test_s_flow_021_dispatch_accepted_order_to_player = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_021_dispatch_accepted_order_to_player",
"test_s_flow_021_dispatch_accepted_order_to_player",
)
_test_s_flow_026_player_accept_dispatched_order = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_026_player_accept_dispatched_order",
"test_s_flow_026_player_accept_dispatched_order",
)
_test_s_flow_040_player_submit_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_040_player_submit_acceptance",
"test_s_flow_040_player_submit_acceptance",
)
_test_s_flow_043_accept_player_sub_order = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_043_accept_player_sub_order",
"test_s_flow_043_accept_player_sub_order",
)
_test_s_flow_045_apply_upstream_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_045_apply_upstream_acceptance",
"test_s_flow_045_apply_upstream_acceptance",
)
_test_s_flow_046_start_pending_acceptance_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_046_start_pending_acceptance_aftersale",
"test_s_flow_046_start_pending_acceptance_aftersale",
)
_test_s_flow_047_process_pending_acceptance_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_047_process_pending_acceptance_aftersale",
"test_s_flow_047_process_pending_acceptance_aftersale",
)
_test_s_flow_051_start_appeal_from_aftersale_completed = _case(
"e2e.tests.订单状态流转.主单申诉.test_s_flow_051_start_appeal_from_aftersale_completed",
"test_s_flow_051_start_appeal_from_aftersale_completed",
)
_test_s_flow_052_approve_main_order_appeal = _case(
"e2e.tests.订单状态流转.主单申诉.test_s_flow_052_approve_main_order_appeal",
"test_s_flow_052_approve_main_order_appeal",
)
@pytest.mark.flow_028
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_flow_028_pending_acceptance_aftersale_appeal_approve_chain(
page: Page,
settings,
bug_reporter,
monkeypatch,
) -> None:
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
_test_s_flow_014_accept_upstream_order_from_hall(page, settings, bug_reporter)
_test_s_flow_021_dispatch_accepted_order_to_player(page, settings, bug_reporter)
_test_s_flow_026_player_accept_dispatched_order(page, settings, bug_reporter)
_test_s_flow_040_player_submit_acceptance(page, settings, bug_reporter)
_test_s_flow_043_accept_player_sub_order(page, settings, bug_reporter)
_test_s_flow_045_apply_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_046_start_pending_acceptance_aftersale(page, settings, bug_reporter)
_test_s_flow_047_process_pending_acceptance_aftersale(page, settings, bug_reporter)
_test_s_flow_051_start_appeal_from_aftersale_completed(page, settings, bug_reporter)
_test_s_flow_052_approve_main_order_appeal(page, settings, bug_reporter)
from __future__ import annotations
from importlib import import_module
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
def _case(module_path: str, function_name: str):
return getattr(import_module(module_path), function_name)
_test_s_flow_007_create_order_with_dispatch = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch",
"test_s_flow_007_create_order_with_dispatch",
)
_test_s_flow_014_accept_upstream_order_from_hall = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_014_accept_upstream_order_from_hall",
"test_s_flow_014_accept_upstream_order_from_hall",
)
_test_s_flow_021_dispatch_accepted_order_to_player = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_021_dispatch_accepted_order_to_player",
"test_s_flow_021_dispatch_accepted_order_to_player",
)
_test_s_flow_026_player_accept_dispatched_order = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_026_player_accept_dispatched_order",
"test_s_flow_026_player_accept_dispatched_order",
)
_test_s_flow_040_player_submit_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_040_player_submit_acceptance",
"test_s_flow_040_player_submit_acceptance",
)
_test_s_flow_043_accept_player_sub_order = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_043_accept_player_sub_order",
"test_s_flow_043_accept_player_sub_order",
)
_test_s_flow_045_apply_upstream_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_045_apply_upstream_acceptance",
"test_s_flow_045_apply_upstream_acceptance",
)
_test_s_flow_046_start_pending_acceptance_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_046_start_pending_acceptance_aftersale",
"test_s_flow_046_start_pending_acceptance_aftersale",
)
_test_s_flow_047_process_pending_acceptance_aftersale = _case(
"e2e.tests.订单状态流转.丸掌柜发起售后.test_s_flow_047_process_pending_acceptance_aftersale",
"test_s_flow_047_process_pending_acceptance_aftersale",
)
_test_s_flow_051_start_appeal_from_aftersale_completed = _case(
"e2e.tests.订单状态流转.主单申诉.test_s_flow_051_start_appeal_from_aftersale_completed",
"test_s_flow_051_start_appeal_from_aftersale_completed",
)
_test_s_flow_053_reject_main_order_appeal = _case(
"e2e.tests.订单状态流转.主单申诉.test_s_flow_053_reject_main_order_appeal",
"test_s_flow_053_reject_main_order_appeal",
)
@pytest.mark.flow_029
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_flow_029_pending_acceptance_aftersale_appeal_reject_chain(
page: Page,
settings,
bug_reporter,
monkeypatch,
) -> None:
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "鎶㈠崟澶у巺")
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
_test_s_flow_014_accept_upstream_order_from_hall(page, settings, bug_reporter)
_test_s_flow_021_dispatch_accepted_order_to_player(page, settings, bug_reporter)
_test_s_flow_026_player_accept_dispatched_order(page, settings, bug_reporter)
_test_s_flow_040_player_submit_acceptance(page, settings, bug_reporter)
_test_s_flow_043_accept_player_sub_order(page, settings, bug_reporter)
_test_s_flow_045_apply_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_046_start_pending_acceptance_aftersale(page, settings, bug_reporter)
_test_s_flow_047_process_pending_acceptance_aftersale(page, settings, bug_reporter)
_test_s_flow_051_start_appeal_from_aftersale_completed(page, settings, bug_reporter)
_test_s_flow_053_reject_main_order_appeal(page, settings, bug_reporter)
from __future__ import annotations
import os
import re
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import WanzhangguiOrderPage
from e2e.utils.account_store import account_store
@pytest.mark.s_flow_050
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_050_start_appeal_from_completed_order(page: Page, settings, bug_reporter) -> None:
wanzhanggui_phone = os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", settings.wanzhanggui_tenant_phone)
wanzhanggui_password = os.getenv("E2E_ORDER_WANZHANGGUI_PASSWORD", settings.wanzhanggui_tenant_password)
store = account_store()
order = resolve_s_flow_050_order(wanzhanggui_phone)
if not order:
pytest.skip("台账中未找到可供 S-FLOW-050 使用的已完成订单;请先执行 S-FLOW-049。")
source_order_no = order["source_order_no"]
main_order_no = resolve_main_order_no(order)
order_title = order.get("order_title", source_order_no)
appeal_amount = os.getenv("E2E_S_FLOW_APPEAL_AMOUNT", "1")
appeal_reason = f"S-FLOW-050 已完成主单发起申诉 {main_order_no or source_order_no}"
assert order.get("status") == "upstream_completed", f"S-FLOW-050 前置业务状态不满足已完成;order={order}"
assert order.get("current_flow_state") == "wanzhanggui_upstream_acceptance_completed", (
f"S-FLOW-050 前置台账状态不满足上游验收完成;order={order}"
)
assert "S-FLOW-050" in order.get("available_for_cases", []), f"该订单未声明可用于 S-FLOW-050;order={order}"
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
order_page.login_main_account(wanzhanggui_phone, wanzhanggui_password)
bug_reporter.record_if_errors(account=wanzhanggui_phone, location="S-FLOW-050 丸掌柜账号登录")
if not main_order_no:
main_order_no = order_page.find_main_order_no_from_order_list(source_order_no=source_order_no, order_title=order_title)
appeal_reason = f"S-FLOW-050 已完成主单发起申诉 {main_order_no}"
final_text = order_page.start_appeal_for_order(
source_order_no,
order_title,
main_order_no=main_order_no,
reason=appeal_reason,
amount=appeal_amount,
expected_main_status="已完成",
case_label="S-FLOW-050",
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-050 丸掌柜已完成订单发起主单申诉:{main_order_no or source_order_no}/{order_title}",
)
appeal_record_text = order_page.assert_main_order_appeal_record_visible(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
reason=appeal_reason,
amount=appeal_amount,
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-050 丸掌柜主单申诉单记录校验:{main_order_no or source_order_no}/{order_title}",
)
detail_status_text = order_page.assert_order_detail_statuses(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_main_status="已完成",
expected_sub_status_parts=["已完成", "申诉中"],
expected_appeal_status=["处理中", "申诉中", "待处理"],
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-050 丸掌柜订单详情申诉中状态校验:{main_order_no or source_order_no}",
)
store.mark_order_consumed_by_flow_case(
source_order_no=source_order_no,
case_id="S-FLOW-050",
main_order_no=main_order_no,
status="appeal_pending_from_aftersale",
current_flow_state="wanzhanggui_appeal_pending_from_aftersale_completed",
wanzhanggui_main_status="已完成",
wanzhanggui_sub_order_status="已完成→申诉中",
appeal_started_by=wanzhanggui_phone,
appeal_reason=appeal_reason,
appeal_amount=appeal_amount,
appeal_started_text=final_text[:1500],
appeal_record_text=appeal_record_text[:1500],
appeal_started_detail_status_text=detail_status_text[:1500],
available_for_cases=["S-FLOW-052", "S-FLOW-053"],
)
def resolve_s_flow_050_order(wanzhanggui_phone: str) -> dict | None:
explicit_main_order_no = os.getenv("E2E_S_FLOW_MAIN_ORDER_NO", "").strip()
explicit_source_order_no = os.getenv("E2E_S_FLOW_SOURCE_ORDER_NO", "").strip()
payload = account_store().load_payload()
if explicit_main_order_no or explicit_source_order_no:
for item in payload.get("dispatched_orders", []):
if wanzhanggui_phone and item.get("wanzhanggui_phone") != wanzhanggui_phone:
continue
if explicit_source_order_no and item.get("source_order_no") != explicit_source_order_no:
continue
if explicit_main_order_no and resolve_main_order_no(item) != explicit_main_order_no:
continue
if item.get("status") == "upstream_completed" and "S-FLOW-050" in item.get("available_for_cases", []):
return item
return None
reusable_orders = [
item
for item in payload.get("dispatched_orders", [])
if (not wanzhanggui_phone or item.get("wanzhanggui_phone") == wanzhanggui_phone)
and item.get("status") == "upstream_completed"
and item.get("current_flow_state") == "wanzhanggui_upstream_acceptance_completed"
and "S-FLOW-050" in item.get("available_for_cases", [])
and "S-FLOW-050" not in item.get("consumed_by_cases", [])
]
if reusable_orders:
return sorted(reusable_orders, key=lambda item: item.get("updated_at", item.get("created_at", "")), reverse=True)[0]
return None
def resolve_main_order_no(order: dict) -> str:
if order.get("main_order_no", "").startswith("BO"):
return order["main_order_no"]
for key in ["wanzhanggui_after_upstream_complete_text", "wanzhanggui_after_upstream_complete_detail_text"]:
value = order.get(key) or ""
match = re.search(r"BO\d{20}", value)
if match:
return match.group(0)
return ""
from __future__ import annotations
from importlib import import_module
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
def _case(module_path: str, function_name: str):
return getattr(import_module(module_path), function_name)
_test_s_flow_007_create_order_with_dispatch = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch",
"test_s_flow_007_create_order_with_dispatch",
)
_test_s_flow_014_accept_upstream_order_from_hall = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_014_accept_upstream_order_from_hall",
"test_s_flow_014_accept_upstream_order_from_hall",
)
_test_s_flow_021_dispatch_accepted_order_to_player = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_021_dispatch_accepted_order_to_player",
"test_s_flow_021_dispatch_accepted_order_to_player",
)
_test_s_flow_026_player_accept_dispatched_order = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_026_player_accept_dispatched_order",
"test_s_flow_026_player_accept_dispatched_order",
)
_test_s_flow_040_player_submit_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_040_player_submit_acceptance",
"test_s_flow_040_player_submit_acceptance",
)
_test_s_flow_043_accept_player_sub_order = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_043_accept_player_sub_order",
"test_s_flow_043_accept_player_sub_order",
)
_test_s_flow_045_apply_upstream_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_045_apply_upstream_acceptance",
"test_s_flow_045_apply_upstream_acceptance",
)
_test_s_flow_049_complete_upstream_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_049_complete_upstream_acceptance",
"test_s_flow_049_complete_upstream_acceptance",
)
_test_s_flow_054_start_penalty_from_accepted_sub_order = _case(
"e2e.tests.订单状态流转.处罚追缴.test_s_flow_054_start_penalty_from_accepted_sub_order",
"test_s_flow_054_start_penalty_from_accepted_sub_order",
)
_test_s_flow_055_approve_penalty_from_accepted_sub_order = _case(
"e2e.tests.订单状态流转.处罚追缴.test_s_flow_055_approve_penalty_from_accepted_sub_order",
"test_s_flow_055_approve_penalty_from_accepted_sub_order",
)
def _configure_new_percent_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
@pytest.mark.flow_045
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_flow_045_accepted_sub_order_penalty_approve_chain(page: Page, settings, bug_reporter, monkeypatch) -> None:
_configure_new_percent_dispatch(monkeypatch)
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
_test_s_flow_014_accept_upstream_order_from_hall(page, settings, bug_reporter)
_test_s_flow_021_dispatch_accepted_order_to_player(page, settings, bug_reporter)
_test_s_flow_026_player_accept_dispatched_order(page, settings, bug_reporter)
_test_s_flow_040_player_submit_acceptance(page, settings, bug_reporter)
_test_s_flow_043_accept_player_sub_order(page, settings, bug_reporter)
_test_s_flow_045_apply_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_049_complete_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_054_start_penalty_from_accepted_sub_order(page, settings, bug_reporter)
_test_s_flow_055_approve_penalty_from_accepted_sub_order(page, settings, bug_reporter)
from __future__ import annotations
from importlib import import_module
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
def _case(module_path: str, function_name: str):
return getattr(import_module(module_path), function_name)
_test_s_flow_007_create_order_with_dispatch = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch",
"test_s_flow_007_create_order_with_dispatch",
)
_test_s_flow_014_accept_upstream_order_from_hall = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_014_accept_upstream_order_from_hall",
"test_s_flow_014_accept_upstream_order_from_hall",
)
_test_s_flow_021_dispatch_accepted_order_to_player = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_021_dispatch_accepted_order_to_player",
"test_s_flow_021_dispatch_accepted_order_to_player",
)
_test_s_flow_026_player_accept_dispatched_order = _case(
"e2e.tests.订单状态流转.上游发单.test_s_flow_026_player_accept_dispatched_order",
"test_s_flow_026_player_accept_dispatched_order",
)
_test_s_flow_040_player_submit_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_040_player_submit_acceptance",
"test_s_flow_040_player_submit_acceptance",
)
_test_s_flow_043_accept_player_sub_order = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_043_accept_player_sub_order",
"test_s_flow_043_accept_player_sub_order",
)
_test_s_flow_045_apply_upstream_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_045_apply_upstream_acceptance",
"test_s_flow_045_apply_upstream_acceptance",
)
_test_s_flow_049_complete_upstream_acceptance = _case(
"e2e.tests.订单状态流转.订单验收.test_s_flow_049_complete_upstream_acceptance",
"test_s_flow_049_complete_upstream_acceptance",
)
_test_s_flow_054_start_penalty_from_accepted_sub_order = _case(
"e2e.tests.订单状态流转.处罚追缴.test_s_flow_054_start_penalty_from_accepted_sub_order",
"test_s_flow_054_start_penalty_from_accepted_sub_order",
)
_test_s_flow_056_reject_penalty_from_accepted_sub_order = _case(
"e2e.tests.订单状态流转.处罚追缴.test_s_flow_056_reject_penalty_from_accepted_sub_order",
"test_s_flow_056_reject_penalty_from_accepted_sub_order",
)
def _configure_new_percent_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
@pytest.mark.flow_047
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_flow_047_accepted_sub_order_penalty_reject_chain(page: Page, settings, bug_reporter, monkeypatch) -> None:
_configure_new_percent_dispatch(monkeypatch)
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
_test_s_flow_014_accept_upstream_order_from_hall(page, settings, bug_reporter)
_test_s_flow_021_dispatch_accepted_order_to_player(page, settings, bug_reporter)
_test_s_flow_026_player_accept_dispatched_order(page, settings, bug_reporter)
_test_s_flow_040_player_submit_acceptance(page, settings, bug_reporter)
_test_s_flow_043_accept_player_sub_order(page, settings, bug_reporter)
_test_s_flow_045_apply_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_049_complete_upstream_acceptance(page, settings, bug_reporter)
_test_s_flow_054_start_penalty_from_accepted_sub_order(page, settings, bug_reporter)
_test_s_flow_056_reject_penalty_from_accepted_sub_order(page, settings, bug_reporter)
from __future__ import annotations
import os
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import WanzhangguiOrderPage
from e2e.utils.account_store import account_store
@pytest.mark.s_flow_054
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_054_start_penalty_from_accepted_sub_order(page: Page, settings, bug_reporter) -> None:
wanzhanggui_phone = os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", settings.wanzhanggui_tenant_phone)
wanzhanggui_password = os.getenv("E2E_ORDER_WANZHANGGUI_PASSWORD", settings.wanzhanggui_tenant_password)
store = account_store()
order = resolve_s_flow_054_order(wanzhanggui_phone)
if not order:
pytest.skip("台账中未找到可供 S-FLOW-054 使用的上游验收完成订单;请先执行 S-FLOW-049。")
source_order_no = order["source_order_no"]
main_order_no = order.get("main_order_no", "")
order_title = order.get("order_title", source_order_no)
amount = os.getenv("E2E_S_FLOW_PENALTY_AMOUNT", "1")
reason = f"S-FLOW-054 对验收成功子单发起处罚 {main_order_no or source_order_no}"
assert order.get("status") == "upstream_completed", f"S-FLOW-054 前置业务状态不满足已完成;order={order}"
assert order.get("current_flow_state") == "wanzhanggui_upstream_acceptance_completed", (
f"S-FLOW-054 前置台账状态不满足上游验收完成;order={order}"
)
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
order_page.login_main_account(wanzhanggui_phone, wanzhanggui_password)
bug_reporter.record_if_errors(account=wanzhanggui_phone, location="S-FLOW-054 丸掌柜账号登录")
before_detail_status_text = order_page.assert_order_detail_statuses(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_main_status=["已完成", "待验收", "完成"],
expected_sub_status=["已完成", "验收成功", "服务中", "代练中"],
expected_sub_order_list_status=["验收成功", "已完成"],
expected_aftersale_absent=True,
)
assert "处罚" in before_detail_status_text, (
f"S-FLOW-054 前置验收成功子单未展示处罚入口;main_order_no={main_order_no}; "
f"source_order_no={source_order_no}; detail={before_detail_status_text[:1500]}"
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-054 丸掌柜发起处罚前验收成功状态校验:{main_order_no or source_order_no}",
)
order_page.start_penalty_for_accepted_sub_order(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
reason=reason,
amount=amount,
)
penalty_record_text = order_page.assert_sub_order_penalty_record_visible(
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_status_keywords=["处理中", "待处理", "申诉中", "待赔偿", "待审核", "去处理"],
)
bug_reporter.record_if_errors(
account=wanzhanggui_phone,
location=f"S-FLOW-054 丸掌柜子单处罚单回读处罚记录:{main_order_no or source_order_no}",
)
store.mark_order_consumed_by_flow_case(
source_order_no=source_order_no,
case_id="S-FLOW-054",
status="penalty_pending_from_accepted_sub_order",
current_flow_state="wanzhanggui_penalty_pending_from_accepted_sub_order",
penalty_started_by=wanzhanggui_phone,
penalty_reason=reason,
penalty_amount=amount,
accepted_sub_order_penalty_before_detail_text=before_detail_status_text[:1500],
accepted_sub_order_penalty_started_text=penalty_record_text[:1500],
sub_order_penalty_record_text=penalty_record_text[:1500],
available_for_cases=["S-FLOW-055", "S-FLOW-056"],
)
def resolve_s_flow_054_order(wanzhanggui_phone: str) -> dict | None:
explicit_main_order_no = os.getenv("E2E_S_FLOW_MAIN_ORDER_NO", "").strip()
explicit_source_order_no = os.getenv("E2E_S_FLOW_SOURCE_ORDER_NO", "").strip()
payload = account_store().load_payload()
if explicit_main_order_no or explicit_source_order_no:
for item in payload.get("dispatched_orders", []):
if wanzhanggui_phone and item.get("wanzhanggui_phone") != wanzhanggui_phone:
continue
if explicit_main_order_no and item.get("main_order_no") != explicit_main_order_no:
continue
if explicit_source_order_no and item.get("source_order_no") != explicit_source_order_no:
continue
return item
return None
candidate = account_store().find_order_for_flow_case(
case_id="S-FLOW-054",
wanzhanggui_phone=wanzhanggui_phone,
current_flow_state="wanzhanggui_upstream_acceptance_completed",
)
if candidate:
return candidate
candidates = [
item
for item in payload.get("dispatched_orders", [])
if (not wanzhanggui_phone or item.get("wanzhanggui_phone") == wanzhanggui_phone)
and item.get("status") == "upstream_completed"
and "S-FLOW-054" in item.get("available_for_cases", [])
and "S-FLOW-054" not in item.get("consumed_by_cases", [])
]
return sorted(candidates, key=lambda item: item.get("updated_at", item.get("created_at", "")), reverse=True)[0] if candidates else None
from __future__ import annotations
import pytest
from playwright.sync_api import Page
from e2e.tests.订单状态流转.处罚追缴.penalty_case_helpers import run_penalty_approve_case
@pytest.mark.s_flow_055
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_055_approve_penalty_from_accepted_sub_order(page: Page, settings, bug_reporter) -> None:
run_penalty_approve_case(
page,
settings,
bug_reporter,
case_id="S-FLOW-055",
prerequisite_case_id="S-FLOW-054",
pending_flow_state="wanzhanggui_penalty_pending_from_accepted_sub_order",
final_status="penalty_approved_from_accepted_sub_order",
final_flow_state="wanyouji_penalty_approved_from_accepted_sub_order",
remark_prefix="丸友集同意验收成功子单处罚赔偿",
approved_text_field="accepted_sub_order_penalty_approved_text",
approved_record_field="accepted_sub_order_penalty_approved_record_text",
)
from __future__ import annotations
import pytest
from playwright.sync_api import Page
from e2e.tests.订单状态流转.处罚追缴.penalty_case_helpers import run_penalty_reject_case
@pytest.mark.s_flow_056
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
def test_s_flow_056_reject_penalty_from_accepted_sub_order(page: Page, settings, bug_reporter) -> None:
run_penalty_reject_case(
page,
settings,
bug_reporter,
case_id="S-FLOW-056",
prerequisite_case_id="S-FLOW-054",
pending_flow_state="wanzhanggui_penalty_pending_from_accepted_sub_order",
final_status="penalty_rejected_from_accepted_sub_order",
final_flow_state="wanyouji_penalty_rejected_from_accepted_sub_order",
remark_prefix="丸友集驳回验收成功子单处罚",
rejected_text_field="accepted_sub_order_penalty_rejected_text",
rejected_record_field="accepted_sub_order_penalty_rejected_record_text",
)
......@@ -3,6 +3,8 @@ from __future__ import annotations
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
from e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch import (
test_s_flow_007_create_order_with_dispatch as _test_s_flow_007_create_order_with_dispatch,
)
......@@ -30,10 +32,7 @@ def test_flow_022_assigned_player_cancel_approve_chain(page: Page, settings, bug
monkeypatch.setenv("E2E_S_FLOW_CREATE_MODE", "create")
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.setenv("E2E_S_FLOW_DISPATCH_PRICE_MODE", "percent")
monkeypatch.setenv("E2E_S_FLOW_DISPATCH_PRICE_PERCENT", "90")
monkeypatch.setenv("E2E_TASK_DISPATCH_PRICE_MODE", "percent")
monkeypatch.setenv("E2E_TASK_DISPATCH_PRICE_PERCENT", "90")
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
......
......@@ -3,6 +3,8 @@ from __future__ import annotations
import pytest
from playwright.sync_api import Page
from e2e.utils.data_factory import configure_random_percent_dispatch
from e2e.tests.订单状态流转.上游发单.test_s_flow_007_create_order_with_dispatch import (
test_s_flow_007_create_order_with_dispatch as _test_s_flow_007_create_order_with_dispatch,
)
......@@ -37,10 +39,7 @@ def test_flow_044_cancel_upstream_acceptance_chain(page: Page, settings, bug_rep
monkeypatch.delenv("E2E_S_FLOW_MAIN_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_SOURCE_ORDER_NO", raising=False)
monkeypatch.delenv("E2E_S_FLOW_ORDER_TITLE", raising=False)
monkeypatch.setenv("E2E_S_FLOW_DISPATCH_PRICE_MODE", "percent")
monkeypatch.setenv("E2E_S_FLOW_DISPATCH_PRICE_PERCENT", "90")
monkeypatch.setenv("E2E_TASK_DISPATCH_PRICE_MODE", "percent")
monkeypatch.setenv("E2E_TASK_DISPATCH_PRICE_PERCENT", "90")
configure_random_percent_dispatch(monkeypatch)
monkeypatch.setenv("E2E_TASK_ASSIGNMENT_METHOD", "抢单大厅")
_test_s_flow_007_create_order_with_dispatch(page, settings, bug_reporter)
......
......@@ -46,9 +46,13 @@ def test_s_flow_043_accept_player_sub_order(page: Page, settings, bug_reporter)
source_order_no,
order_title,
)
assert any(key in before_text for key in ["待验收", "验: 1", "验:1"]), (
already_accepted = any(key in before_text for key in ["已完成", "完成", "待结算", "结算", "完:"])
assert already_accepted or any(key in before_text for key in ["待验收", "验: 1", "验:1"]), (
f"S-FLOW-043 前置不满足打手子单待验收;source_order_no={source_order_no}; text={before_text[:1500]}"
)
if already_accepted:
accepted_text = before_text
else:
accepted_text = fulfillment_page.accept_sub_order_and_verify_ready_for_upstream(
source_order_no,
order_title,
......@@ -75,9 +79,9 @@ def test_s_flow_043_accept_player_sub_order(page: Page, settings, bug_reporter)
main_order_no=main_order_no,
source_order_no=source_order_no,
order_title=order_title,
expected_main_status=["服务中", "代练中", "待验收"],
expected_sub_status=["服务中", "代练中"],
expected_sub_order_list_status=["已完成", "服务中", "代练中"],
expected_main_status=["服务中", "代练中", "待验收", "已完成"],
expected_sub_status=["服务中", "代练中", "已完成", "申诉完成", "申诉中"],
expected_sub_order_list_status=["已完成", "验收成功", "服务中", "代练中"],
expected_aftersale_absent=True,
)
bug_reporter.record_if_errors(
......@@ -85,23 +89,33 @@ def test_s_flow_043_accept_player_sub_order(page: Page, settings, bug_reporter)
location=f"S-FLOW-043 丸掌柜订单详情仍服务中校验:{main_order_no or source_order_no}",
)
consumed_cases = set(order.get("consumed_by_cases", []))
downstream_already_consumed = bool(consumed_cases.intersection({"S-FLOW-045", "S-FLOW-049", "S-FLOW-050", "S-FLOW-052", "S-FLOW-053"}))
updates = {
"task_player_phone": player_account.phone,
"task_wanyouji_phone": wanyouji_account.phone,
"sub_order_accepted_by": wanyouji_account.phone,
"settlement_verified_by": player_account.phone,
"expected_player_amount": expected_amount,
"wanyouji_before_sub_order_accept_text": before_text[:1500],
"wanyouji_after_sub_order_accept_text": accepted_text[:1500],
"player_income_after_sub_order_accept_text": income_text[:1500] if isinstance(income_text, str) else "",
"wanzhanggui_after_sub_order_accept_detail_text": detail_text[:1500],
"player_order_status": "已完成",
"wanyouji_sub_order_status": "已完成",
}
if not downstream_already_consumed:
updates.update(
{
"status": "sub_order_income_verified",
"current_flow_state": "player_income_ready_for_settlement",
"wanzhanggui_main_status": "服务中",
"wanzhanggui_sub_order_status": "代练中",
"available_for_cases": ["S-FLOW-080", "S-FLOW-045", "S-FLOW-054"],
}
)
store.mark_order_consumed_by_flow_case(
source_order_no=source_order_no,
case_id="S-FLOW-043",
status="sub_order_income_verified",
current_flow_state="player_income_ready_for_settlement",
task_player_phone=player_account.phone,
task_wanyouji_phone=wanyouji_account.phone,
sub_order_accepted_by=wanyouji_account.phone,
settlement_verified_by=player_account.phone,
expected_player_amount=expected_amount,
wanyouji_before_sub_order_accept_text=before_text[:1500],
wanyouji_after_sub_order_accept_text=accepted_text[:1500],
player_income_after_sub_order_accept_text=income_text[:1500] if isinstance(income_text, str) else "",
wanzhanggui_after_sub_order_accept_detail_text=detail_text[:1500],
player_order_status="已完成",
wanyouji_sub_order_status="已完成",
wanzhanggui_main_status="服务中",
wanzhanggui_sub_order_status="代练中",
available_for_cases=["S-FLOW-080", "S-FLOW-045", "S-FLOW-054"],
**updates,
)
......@@ -3,6 +3,13 @@ from __future__ import annotations
import random
from dataclasses import dataclass
from datetime import datetime
import os
MANUAL_ORDER_RECEIVE_PRICE_MIN = 10
MANUAL_ORDER_RECEIVE_PRICE_MAX = 30
DISPATCH_PRICE_PERCENT_MIN = 60
DISPATCH_PRICE_PERCENT_MAX = 90
@dataclass(frozen=True)
......@@ -20,6 +27,48 @@ def random_mainland_phone(prefix: str = "19") -> str:
return clean_prefix + "".join(str(random.randint(0, 9)) for _ in range(remaining))
def manual_order_receive_price() -> str:
explicit = os.getenv("E2E_MANUAL_ORDER_RECEIVE_PRICE", "").strip()
if explicit:
value = int(explicit)
assert MANUAL_ORDER_RECEIVE_PRICE_MIN <= value <= MANUAL_ORDER_RECEIVE_PRICE_MAX, (
"E2E_MANUAL_ORDER_RECEIVE_PRICE must be between "
f"{MANUAL_ORDER_RECEIVE_PRICE_MIN} and {MANUAL_ORDER_RECEIVE_PRICE_MAX}, got {value}"
)
return str(value)
return str(random.randint(MANUAL_ORDER_RECEIVE_PRICE_MIN, MANUAL_ORDER_RECEIVE_PRICE_MAX))
def random_dispatch_price_percent() -> str:
explicit = os.getenv("E2E_DISPATCH_PRICE_PERCENT", "").strip()
if explicit:
value = int(explicit)
assert DISPATCH_PRICE_PERCENT_MIN <= value <= DISPATCH_PRICE_PERCENT_MAX, (
"E2E_DISPATCH_PRICE_PERCENT must be between "
f"{DISPATCH_PRICE_PERCENT_MIN} and {DISPATCH_PRICE_PERCENT_MAX}, got {value}"
)
return str(value)
return str(random.randint(DISPATCH_PRICE_PERCENT_MIN, DISPATCH_PRICE_PERCENT_MAX))
def default_dispatch_price_percent() -> str:
existing = os.getenv("E2E_GENERATED_DISPATCH_PRICE_PERCENT", "").strip()
if existing:
return existing
percent = random_dispatch_price_percent()
os.environ["E2E_GENERATED_DISPATCH_PRICE_PERCENT"] = percent
return percent
def configure_random_percent_dispatch(monkeypatch) -> str:
percent = random_dispatch_price_percent()
monkeypatch.setenv("E2E_S_FLOW_DISPATCH_PRICE_MODE", "percent")
monkeypatch.setenv("E2E_S_FLOW_DISPATCH_PRICE_PERCENT", percent)
monkeypatch.setenv("E2E_TASK_DISPATCH_PRICE_MODE", "percent")
monkeypatch.setenv("E2E_TASK_DISPATCH_PRICE_PERCENT", percent)
return percent
def enterprise_data(password: str, prefix: str, enterprise_prefix: str) -> EnterpriseRegistrationData:
suffix = datetime.now().strftime("%m%d%H%M%S") + str(random.randint(100, 999))
return EnterpriseRegistrationData(
......
......@@ -70,10 +70,25 @@ markers =
s_flow_081: order status flow S-FLOW-081 Wanyouji completes player settlement before continuing dispatch
s_flow_044: order status flow S-FLOW-044 Wanyouji continues dispatch after completed child
s_flow_045: order status flow S-FLOW-045 Wanyouji applies upstream acceptance
s_flow_046: order status flow S-FLOW-046 Wanzhanggui starts main-order aftersale during pending acceptance
s_flow_047: order status flow S-FLOW-047 Wanzhanggui confirms pending-acceptance main-order aftersale
s_flow_048: order status flow S-FLOW-048 Wanzhanggui cancels pending-acceptance aftersale
s_flow_082: order status flow S-FLOW-082 Wanyouji cancels upstream acceptance and both sides verify status
s_flow_049: order status flow S-FLOW-049 Wanzhanggui completes upstream acceptance
flow_044: order status flow FLOW-044 Wanyouji applies upstream acceptance then cancels it
s_flow_050: order status flow S-FLOW-050 Wanzhanggui starts appeal from completed order
s_flow_054: order status flow S-FLOW-054 Wanzhanggui starts penalty from accepted sub-order
s_flow_055: order status flow S-FLOW-055 Wanyouji approves penalty from accepted sub-order
s_flow_056: order status flow S-FLOW-056 Wanyouji rejects penalty from accepted sub-order
flow_026: order status flow FLOW-026 completed order appeal approved chain
flow_027: order status flow FLOW-027 completed order appeal rejected chain
flow_028: order status flow FLOW-028 pending-acceptance aftersale completed then appeal approved chain
flow_029: order status flow FLOW-029 pending-acceptance aftersale completed then appeal rejected chain
flow_030: order status flow FLOW-030 pending-acceptance aftersale canceled then abnormal sub-order aftersale started
flow_045: order status flow FLOW-045 accepted sub-order penalty approved
flow_047: order status flow FLOW-047 accepted sub-order penalty rejected
s_flow_034: order status flow S-FLOW-034 Wanzhanggui starts row-level aftersale for abnormal sub-order
s_flow_083: order status flow S-FLOW-083 Wanzhanggui cancels abnormal sub-order aftersale
s_flow_035: order status flow S-FLOW-035 Wanyouji approves abnormal sub-order aftersale
s_flow_036: order status flow S-FLOW-036 Wanyouji rejects abnormal sub-order aftersale
s_flow_057: order status flow S-FLOW-057 Wanzhanggui continues dispatch after rejected sub-order
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -10,8 +10,8 @@ from openpyxl.utils import get_column_letter
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
OUTPUT = ROOT / "订单状态流转_链路流程线整理.xlsx"
SOURCE = ROOT / "测试用例" / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
OUTPUT = ROOT / "测试用例" / "订单状态流转_链路流程线整理.xlsx"
CASE_SHEET = "订单状态流转"
......@@ -390,13 +390,23 @@ def build_flows() -> list[dict[str, object]]:
),
flow(
"FLOW-030",
"待验收阶段-发起售后-取消售后-丸掌柜同意验收-已完成申诉",
["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-046", "S-FLOW-048", "S-FLOW-049", "S-FLOW-050"],
"待验收阶段-发起售后-取消售后-丸掌柜同意验收-验收成功子单处罚同意赔偿",
["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-046", "S-FLOW-048", "S-FLOW-049", "S-FLOW-054", "S-FLOW-055"],
"主单待验收;子单待验收",
"已完成;可发起已完成申诉",
"待验收售后取消后回归验收",
"验证待验收阶段售后取消后恢复验收前状态,再走丸掌柜同意验收。",
"取消售后后同一订单可继续普通验收。",
"已完成;验收成功子单处罚同意赔偿",
"待验收售后取消后回归验收 + 处罚追缴正向",
"验证待验收阶段售后取消后恢复验收前状态,再走丸掌柜同意验收,并对子单验收成功状态发起处罚追缴。",
"取消售后后同一订单可继续普通验收;丸掌柜同意上游验收后,子单列表验收成功状态可衔接 S-FLOW-054,再由 S-FLOW-055 覆盖同意赔偿分支。",
),
flow(
"FLOW-045",
"待验收阶段-发起售后-取消售后-丸掌柜同意验收-验收成功子单处罚驳回",
["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-046", "S-FLOW-048", "S-FLOW-049", "S-FLOW-054", "S-FLOW-056"],
"主单待验收;子单待验收",
"已完成;验收成功子单处罚驳回",
"待验收售后取消后回归验收 + 处罚追缴逆向",
"验证待验收阶段售后取消后恢复验收前状态,再走丸掌柜同意验收,并对子单验收成功状态发起处罚追缴后由丸友集驳回。",
"与 FLOW-030 使用不同订单;S-FLOW-049 后同样先接 S-FLOW-054,再由 S-FLOW-056 覆盖驳回处罚分支。",
),
]
......
from __future__ import annotations
from copy import copy
from datetime import datetime
from pathlib import Path
import shutil
import openpyxl
ROOT = Path(__file__).resolve().parents[1]
TESTCASE_DIR = ROOT / "测试用例"
FLOW_BOOK = TESTCASE_DIR / "订单状态流转_链路流程线整理.xlsx"
FLOW_BACKUP = TESTCASE_DIR / "订单状态流转_链路流程线整理 - 副本.xlsx"
CASE_BOOK = TESTCASE_DIR / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
CASE_BACKUP = TESTCASE_DIR / "SaaS_E2E测试用例_单Sheet用例测试列表 - 副本.xlsx"
FLOW_030 = {
"链路编号": "FLOW-030",
"链路名称": "待验收阶段-发起售后-取消售后-丸掌柜同意验收-验收成功子单处罚同意赔偿",
"用例编号序列": "S-FLOW-007 -> S-FLOW-014 -> S-FLOW-021 -> S-FLOW-026 -> S-FLOW-040 -> S-FLOW-043 -> S-FLOW-045 -> S-FLOW-046 -> S-FLOW-048 -> S-FLOW-049 -> S-FLOW-054 -> S-FLOW-055",
"起点状态": "主单待验收;子单待验收",
"终点状态": "已完成;验收成功子单处罚同意赔偿",
"链路类型": "待验收售后取消后回归验收 + 处罚追缴正向",
"适用场景": "验证待验收阶段售后取消后恢复验收前状态,再走丸掌柜同意验收,并对子单验收成功状态发起处罚追缴。",
"数据复用建议": "取消售后后同一订单可继续普通验收;丸掌柜同意上游验收后,子单列表验收成功状态可衔接 S-FLOW-054,再由 S-FLOW-055 覆盖同意赔偿分支。",
"注意事项/缺口": "S-FLOW-049 后必须先执行 S-FLOW-054;同意赔偿分支由 S-FLOW-055 覆盖。",
}
FLOW_045 = {
"链路编号": "FLOW-045",
"链路名称": "待验收阶段-发起售后-取消售后-丸掌柜同意验收-验收成功子单处罚驳回",
"用例编号序列": "S-FLOW-007 -> S-FLOW-014 -> S-FLOW-021 -> S-FLOW-026 -> S-FLOW-040 -> S-FLOW-043 -> S-FLOW-045 -> S-FLOW-046 -> S-FLOW-048 -> S-FLOW-049 -> S-FLOW-054 -> S-FLOW-056",
"起点状态": "主单待验收;子单待验收",
"终点状态": "已完成;验收成功子单处罚驳回",
"链路类型": "待验收售后取消后回归验收 + 处罚追缴逆向",
"适用场景": "验证待验收阶段售后取消后恢复验收前状态,再走丸掌柜同意验收,并对子单验收成功状态发起处罚追缴后由丸友集驳回。",
"数据复用建议": "与 FLOW-030 使用不同订单;S-FLOW-049 后同样先接 S-FLOW-054,再由 S-FLOW-056 覆盖驳回处罚分支。",
"注意事项/缺口": "S-FLOW-049 后必须先执行 S-FLOW-054;驳回处罚分支由 S-FLOW-056 覆盖。",
}
FLOW_025 = {
"链路编号": "FLOW-025",
"链路名称": "打手提交验收-丸友集同意验收-打手发起结算-丸友集完成结算-续发下一子单",
"用例编号序列": "S-FLOW-007 -> S-FLOW-014 -> S-FLOW-021 -> S-FLOW-026 -> S-FLOW-040 -> S-FLOW-043 -> S-FLOW-080 -> S-FLOW-081 -> S-FLOW-044",
"终点状态": "前一子单已验收并已完成结算;下一子单待接单/代练中",
"链路类型": "子单验收结算后多子单续发",
"适用场景": "验证丸友集同意验收后,需先由打手发起结算、丸友集完成结算,使前一子单从已验收转为已完成,再继续发下一子单。",
"数据复用建议": "S-FLOW-043 后不直接接 S-FLOW-044;先接 S-FLOW-080/S-FLOW-081 完成打手结算,主单不应因单个子单完成直接完结。",
}
CASE_UPDATES = {
"S-FLOW-054": {
"跨端/状态断言": "1. 处罚记录的主单号、来源单号、子单号、责任方、金额在丸掌柜侧一致。\n"
"2. 处罚只作用于目标子单,不误影响同主单其他子单。\n"
"3. 后续由 S-FLOW-055 或 S-FLOW-056 验证丸友集处理结果。",
"可组合/被引用关系": "承接 S-FLOW-049 后的已验收/验收成功子单;后续分支为 S-FLOW-055/S-FLOW-056;被 FLOW-030、FLOW-045 引用。",
"自动化状态": "已自动化",
"备注": "2026-07-02 修正:S-FLOW-049 后接验收成功子单处罚,后续分支为 S-FLOW-055/S-FLOW-056。",
},
"S-FLOW-055": {
"用例标题": "【丸友集】验收成功子单处罚-同意赔偿",
"预期结果": "1. 丸友集侧目标处罚/申诉记录处理完成或显示已完成/已通过。\n"
"2. 处罚金额等于申请处罚金额。\n"
"3. 丸掌柜侧子单处罚单同步为已完成或同意赔偿结果。",
"跨端/状态断言": "丸友集申诉工单、丸掌柜子单处罚单按丸掌柜主单号/来源单号可定位同一记录;处理状态和金额一致。",
"可组合/被引用关系": "承接 S-FLOW-054;FLOW-030 处罚同意赔偿分支引用。",
"自动化状态": "已自动化",
"备注": "2026-07-02 修正:作为 S-FLOW-054 后续同意赔偿分支,不再仅作为 S-FLOW-063 别名。",
},
"S-FLOW-056": {
"用例标题": "【丸友集】验收成功子单处罚-驳回处罚",
"预期结果": "1. 丸友集侧目标处罚/申诉记录处理完成或显示已拒绝/已驳回。\n"
"2. 处罚金额为0或页面对应未赔偿结果。\n"
"3. 丸掌柜侧子单处罚单同步为已拒绝或驳回结果。",
"跨端/状态断言": "丸友集申诉工单、丸掌柜子单处罚单按丸掌柜主单号/来源单号可定位同一记录;驳回状态和金额结果一致。",
"可组合/被引用关系": "承接 S-FLOW-054;FLOW-045 处罚驳回分支引用。",
"自动化状态": "已自动化",
"备注": "2026-07-02 修正:作为 S-FLOW-054 后续驳回处罚分支,不再仅作为 S-FLOW-064 别名。",
},
}
def header_map(ws) -> dict[str, int]:
return {str(ws.cell(1, col).value or ""): col for col in range(1, ws.max_column + 1) if ws.cell(1, col).value}
def row_by_key(ws, key_col: int) -> dict[str, int]:
return {str(ws.cell(row, key_col).value or ""): row for row in range(2, ws.max_row + 1) if ws.cell(row, key_col).value}
def copy_cell_style(src, dst) -> None:
if src.has_style:
dst._style = copy(src._style)
dst.font = copy(src.font)
dst.fill = copy(src.fill)
dst.border = copy(src.border)
dst.alignment = copy(src.alignment)
dst.number_format = src.number_format
dst.protection = copy(src.protection)
def copy_row_style(ws, src_row: int, dst_row: int) -> None:
ws.row_dimensions[dst_row].height = ws.row_dimensions[src_row].height
for col in range(1, ws.max_column + 1):
copy_cell_style(ws.cell(src_row, col), ws.cell(dst_row, col))
def set_row_values(ws, row: int, values: dict[str, str]) -> None:
h = header_map(ws)
for key, value in values.items():
if key in h:
ws.cell(row, h[key]).value = value
def first_row(ws, key: str) -> int | None:
h = header_map(ws)
rows = row_by_key(ws, h[next(iter(h))])
return rows.get(key)
def insert_flow_after(ws, after_flow_id: str, values: dict[str, str]) -> int:
h = header_map(ws)
rows = row_by_key(ws, h["链路编号"])
old = rows.get(values["链路编号"])
if old:
ws.delete_rows(old)
rows = row_by_key(ws, h["链路编号"])
after_row = rows[after_flow_id]
ws.insert_rows(after_row + 1)
copy_row_style(ws, after_row, after_row + 1)
set_row_values(ws, after_row + 1, values)
return after_row + 1
def case_title(case_rows: dict[str, dict[str, str]], case_id: str) -> str:
return case_rows.get(case_id, {}).get("用例标题", "")
def build_refs(case_rows: dict[str, dict[str, str]], case_ids: list[str]) -> str:
return " -> ".join(f"{case_id}({case_title(case_rows, case_id)})" for case_id in case_ids)
def build_modules(case_rows: dict[str, dict[str, str]], case_ids: list[str]) -> str:
modules: list[str] = []
for case_id in case_ids:
module = case_rows.get(case_id, {}).get("所属模块", "")
if module and module not in modules:
modules.append(module)
return " -> ".join(modules)
def load_case_rows() -> dict[str, dict[str, str]]:
wb = openpyxl.load_workbook(CASE_BOOK, data_only=True)
ws = wb["订单状态流转"]
h = header_map(ws)
result: dict[str, dict[str, str]] = {}
for row in range(2, ws.max_row + 1):
case_id = str(ws.cell(row, h["用例编号"]).value or "")
if not case_id:
continue
result[case_id] = {key: str(ws.cell(row, col).value or "") for key, col in h.items()}
return result
def enrich_flow(values: dict[str, str], case_rows: dict[str, dict[str, str]]) -> dict[str, str]:
case_ids = [item.strip() for item in values["用例编号序列"].split("->")]
enriched = dict(values)
enriched["用例链路(编号+标题)"] = build_refs(case_rows, case_ids)
enriched["涉及模块"] = build_modules(case_rows, case_ids)
return enriched
def sync_flow_summary(wb, case_rows: dict[str, dict[str, str]]) -> None:
ws = wb["链路流程线"]
h = header_map(ws)
rows = row_by_key(ws, h["链路编号"])
for values in [FLOW_025, FLOW_030]:
set_row_values(ws, rows[values["链路编号"]], enrich_flow(values, case_rows))
insert_flow_after(ws, "FLOW-030", enrich_flow(FLOW_045, case_rows))
def delete_detail_rows(ws, flow_ids: set[str]) -> int:
insert_at = ws.max_row + 1
for row in range(ws.max_row, 1, -1):
flow_id = str(ws.cell(row, 1).value or "")
if flow_id in flow_ids:
insert_at = min(insert_at, row)
ws.delete_rows(row)
return insert_at if insert_at <= ws.max_row + 1 else ws.max_row + 1
def append_detail_rows(wb, case_rows: dict[str, dict[str, str]]) -> None:
ws = wb["链路步骤明细"]
insert_at = delete_detail_rows(ws, {"FLOW-030", "FLOW-045"})
style_row = max(2, insert_at - 1)
rows_to_add: list[list[str]] = []
for flow in [FLOW_030, FLOW_045]:
case_ids = [item.strip() for item in flow["用例编号序列"].split("->")]
for index, case_id in enumerate(case_ids, start=1):
case = case_rows.get(case_id, {})
rows_to_add.append(
[
flow["链路编号"],
str(index),
case_id,
case.get("所属模块", ""),
case.get("用例标题", ""),
case.get("前置条件", ""),
case.get("预期结果", ""),
]
)
for offset, values in enumerate(rows_to_add):
ws.insert_rows(insert_at + offset)
copy_row_style(ws, style_row, insert_at + offset)
for col, value in enumerate(values, start=1):
ws.cell(insert_at + offset, col).value = value
def recompute_coverage(wb, case_rows: dict[str, dict[str, str]]) -> None:
summary = wb["链路流程线"]
h_summary = header_map(summary)
flow_by_case: dict[str, list[str]] = {}
for row in range(2, summary.max_row + 1):
flow_id = str(summary.cell(row, h_summary["链路编号"]).value or "")
seq = str(summary.cell(row, h_summary["用例编号序列"]).value or "")
for case_id in [item.strip() for item in seq.split("->") if item.strip()]:
flow_by_case.setdefault(case_id, []).append(flow_id)
ws = wb["用例覆盖索引"]
h = header_map(ws)
rows = row_by_key(ws, h["用例编号"])
template_row = max(rows.values()) if rows else 2
for case_id, case in case_rows.items():
if case_id in rows:
row = rows[case_id]
else:
row = ws.max_row + 1
ws.insert_rows(row)
copy_row_style(ws, template_row, row)
rows[case_id] = row
flows = flow_by_case.get(case_id, [])
values = {
"用例编号": case_id,
"所属模块": case.get("所属模块", ""),
"用例标题": case.get("用例标题", ""),
"覆盖次数": str(len(flows)),
"出现链路": "、".join(flows),
"是否已覆盖": "是" if flows else "否",
"备注": "新增处罚分支覆盖" if case_id in {"S-FLOW-054", "S-FLOW-055", "S-FLOW-056"} else "",
}
for key, value in values.items():
if key in h:
ws.cell(row, h[key]).value = value
def update_case_book_preserve_styles() -> None:
wb = openpyxl.load_workbook(CASE_BOOK)
ws = wb["订单状态流转"]
h = header_map(ws)
rows = row_by_key(ws, h["用例编号"])
for case_id, updates in CASE_UPDATES.items():
row = rows.get(case_id)
if not row:
continue
for key, value in updates.items():
if key in h:
ws.cell(row, h[key]).value = value
wb.save(CASE_BOOK)
def restore_flow_book() -> None:
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
shutil.copyfile(FLOW_BOOK, TESTCASE_DIR / f"订单状态流转_链路流程线整理.样式修复前备份_{timestamp}.xlsx")
case_rows = load_case_rows()
wb = openpyxl.load_workbook(FLOW_BACKUP)
sync_flow_summary(wb, case_rows)
append_detail_rows(wb, case_rows)
recompute_coverage(wb, case_rows)
if "说明" in wb.sheetnames:
wb["说明"].append(["更新记录", f"{datetime.now():%Y-%m-%d %H:%M:%S} 从副本恢复样式,并保留 FLOW-030/新增 FLOW-045 处罚分支。"])
wb.save(FLOW_BOOK)
def main() -> None:
update_case_book_preserve_styles()
restore_flow_book()
print("Restored Excel styles from backups and kept FLOW-030/FLOW-045 updates.")
if __name__ == "__main__":
main()
from __future__ import annotations
from copy import copy
from datetime import datetime
from pathlib import Path
import shutil
from openpyxl import load_workbook
ROOT = Path(__file__).resolve().parents[1]
CASE_BOOK = ROOT / "测试用例" / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
FLOW_BOOK = ROOT / "测试用例" / "订单状态流转_链路流程线整理.xlsx"
FLOWS = {
"FLOW-030": {
"链路编号": "FLOW-030",
"链路名称": "待验收阶段-发起售后-取消售后-异常中子单发起售后",
"链路类型": "待验收售后取消后接异常中子单售后",
"起点状态": "主单待验收;子单待验收",
"终点状态": "丸掌柜子单列表返回异常中;异常中子单已发起售后",
"用例编号序列": "S-FLOW-007 -> S-FLOW-014 -> S-FLOW-021 -> S-FLOW-026 -> S-FLOW-040 -> S-FLOW-043 -> S-FLOW-045 -> S-FLOW-046 -> S-FLOW-048 -> S-FLOW-034",
"涉及模块": "上游发单 -> 订单验收 -> 丸掌柜发起售后",
"适用场景": "验证待验收阶段丸掌柜发起售后后取消,实际平台中丸掌柜侧子单列表返回异常中,后续可继续对异常中子单发起售后。",
"数据复用建议": "取消售后后不要接主单申诉;同一订单继续复用到 S-FLOW-034,验证异常中子单行内发起售后入口。",
"注意事项/缺口": "S-FLOW-048 后以丸掌柜详情子单列表实际状态为准;若子单行为异常中,则后续接 S-FLOW-034,不接 S-FLOW-049/S-FLOW-050。",
},
"FLOW-045": {
"链路编号": "FLOW-045",
"链路名称": "验收成功子单处罚同意赔偿",
"链路类型": "验收成功子单处罚追缴正向",
"起点状态": "打手提交验收;丸友集同意验收;丸友集申请上游验收;丸掌柜同意验收",
"终点状态": "已完成;验收成功子单处罚同意赔偿",
"用例编号序列": "S-FLOW-007 -> S-FLOW-014 -> S-FLOW-021 -> S-FLOW-026 -> S-FLOW-040 -> S-FLOW-043 -> S-FLOW-045 -> S-FLOW-049 -> S-FLOW-054 -> S-FLOW-055",
"涉及模块": "上游发单 -> 订单验收 -> 处罚追缴",
"适用场景": "验证子单验收成功后,丸掌柜对子单发起处罚,丸友集同意赔偿分支。",
"数据复用建议": "使用新订单完整走验收链路;S-FLOW-049 后必须先执行 S-FLOW-054,再执行 S-FLOW-055。",
"注意事项/缺口": "不经过待验收阶段售后取消;与 FLOW-047 使用不同订单。",
},
"FLOW-047": {
"链路编号": "FLOW-047",
"链路名称": "验收成功子单处罚驳回",
"链路类型": "验收成功子单处罚追缴逆向",
"起点状态": "打手提交验收;丸友集同意验收;丸友集申请上游验收;丸掌柜同意验收",
"终点状态": "已完成;验收成功子单处罚驳回",
"用例编号序列": "S-FLOW-007 -> S-FLOW-014 -> S-FLOW-021 -> S-FLOW-026 -> S-FLOW-040 -> S-FLOW-043 -> S-FLOW-045 -> S-FLOW-049 -> S-FLOW-054 -> S-FLOW-056",
"涉及模块": "上游发单 -> 订单验收 -> 处罚追缴",
"适用场景": "验证子单验收成功后,丸掌柜对子单发起处罚,丸友集驳回处罚分支。",
"数据复用建议": "新增最大编号后的链路;保留既有 FLOW-046 不变;与 FLOW-045 使用不同订单。",
"注意事项/缺口": "S-FLOW-049 后必须先执行 S-FLOW-054,再执行 S-FLOW-056。",
},
}
CASE_UPDATES = {
"S-FLOW-048": {
"前置条件": "已执行 S-FLOW-046,丸掌柜在待验收阶段发起主单售后,当前订单为售后中且未处理完成。",
"预期结果": "1. 丸掌柜取消待验收阶段售后提交成功。\n2. 丸友集侧目标订单不再显示售后中。\n3. 按实际平台回读,丸掌柜详情子单列表目标子单返回异常中或页面对应异常状态。\n4. 后续可继续衔接 S-FLOW-034,对异常中子单行内发起售后。",
"跨端/状态断言": "1. 取消售后后不强制进入上游待验收完成链路。\n2. 若丸掌柜侧子单列表返回异常中,则 FLOW-030 后续接 S-FLOW-034,不接 S-FLOW-049/S-FLOW-050。\n3. 丸友集与丸掌柜侧来源单号、订单标题、主单号匹配同一订单。",
"可组合/被引用关系": "承接 S-FLOW-046;被 FLOW-030 引用;后续可接 S-FLOW-034。",
"备注": "2026-07-02 按实测修正:待验收阶段取消售后后丸掌柜子单列表返回异常中,FLOW-030 后续改接异常中子单发起售后。",
},
"S-FLOW-054": {
"可组合/被引用关系": "承接 S-FLOW-049 后的已验收/验收成功子单;后续分支为 S-FLOW-055/S-FLOW-056;被 FLOW-045、FLOW-047 引用。",
"备注": "2026-07-02 修正:验收成功子单处罚分支放在 FLOW-045/FLOW-047,原 FLOW-030 保持售后取消后续链路。",
},
"S-FLOW-055": {
"可组合/被引用关系": "承接 S-FLOW-054;FLOW-045 处罚同意赔偿分支引用。",
"备注": "2026-07-02 修正:作为验收成功子单处罚同意赔偿分支。",
},
"S-FLOW-056": {
"可组合/被引用关系": "承接 S-FLOW-054;FLOW-047 处罚驳回分支引用。",
"备注": "2026-07-02 修正:作为验收成功子单处罚驳回分支;不占用既有 FLOW-046。",
},
}
def header_map(ws) -> dict[str, int]:
return {
str(ws.cell(1, col).value or "").strip(): col
for col in range(1, ws.max_column + 1)
if ws.cell(1, col).value
}
def copy_cell_style(src, dst) -> None:
if src.has_style:
dst._style = copy(src._style)
dst.font = copy(src.font)
dst.fill = copy(src.fill)
dst.border = copy(src.border)
dst.alignment = copy(src.alignment)
dst.number_format = src.number_format
dst.protection = copy(src.protection)
def copy_row_style(ws, src_row: int, dst_row: int) -> None:
ws.row_dimensions[dst_row].height = ws.row_dimensions[src_row].height
for col in range(1, ws.max_column + 1):
copy_cell_style(ws.cell(src_row, col), ws.cell(dst_row, col))
def row_by_value(ws, col: int) -> dict[str, int]:
result: dict[str, int] = {}
for row in range(2, ws.max_row + 1):
value = str(ws.cell(row, col).value or "").strip()
if value and value not in result:
result[value] = row
return result
def set_row_values(ws, row: int, values: dict[str, str]) -> None:
h = header_map(ws)
for key, value in values.items():
if key in h:
ws.cell(row, h[key]).value = value
def load_case_rows() -> dict[str, dict[str, str]]:
wb = load_workbook(CASE_BOOK, data_only=True)
ws = wb["订单状态流转"]
h = header_map(ws)
rows: dict[str, dict[str, str]] = {}
for row in range(2, ws.max_row + 1):
case_id = str(ws.cell(row, h["用例编号"]).value or "").strip()
if case_id:
rows[case_id] = {key: str(ws.cell(row, col).value or "") for key, col in h.items()}
return rows
def enrich_flow(flow: dict[str, str], case_rows: dict[str, dict[str, str]]) -> dict[str, str]:
case_ids = [item.strip() for item in flow["用例编号序列"].split("->") if item.strip()]
refs = []
modules = []
for case_id in case_ids:
case = case_rows.get(case_id, {})
refs.append(f"{case_id}({case.get('用例标题', '')})")
module = case.get("所属模块", "")
if module and module not in modules:
modules.append(module)
enriched = dict(flow)
enriched["用例链路(编号+标题)"] = " -> ".join(refs)
enriched["涉及模块"] = flow.get("涉及模块") or " -> ".join(modules)
return enriched
def update_case_book() -> None:
backup = CASE_BOOK.with_name(f"{CASE_BOOK.stem}.flow030_045_047前备份_{datetime.now():%Y%m%d%H%M%S}{CASE_BOOK.suffix}")
shutil.copyfile(CASE_BOOK, backup)
wb = load_workbook(CASE_BOOK)
ws = wb["订单状态流转"]
h = header_map(ws)
rows = row_by_value(ws, h["用例编号"])
for case_id, values in CASE_UPDATES.items():
row = rows.get(case_id)
if row:
set_row_values(ws, row, values)
wb.save(CASE_BOOK)
def delete_detail_rows(ws, flow_ids: set[str]) -> int:
insert_at = ws.max_row + 1
for row in range(ws.max_row, 1, -1):
flow_id = str(ws.cell(row, 1).value or "").strip()
if flow_id in flow_ids:
insert_at = min(insert_at, row)
ws.delete_rows(row)
return insert_at if insert_at <= ws.max_row + 1 else ws.max_row + 1
def update_flow_summary(ws, case_rows: dict[str, dict[str, str]]) -> None:
h = header_map(ws)
rows = row_by_value(ws, h["链路编号"])
for flow_id in ["FLOW-030", "FLOW-045"]:
set_row_values(ws, rows[flow_id], enrich_flow(FLOWS[flow_id], case_rows))
rows = row_by_value(ws, h["链路编号"])
if "FLOW-047" not in rows:
anchor = rows["FLOW-045"]
ws.insert_rows(anchor + 1)
copy_row_style(ws, anchor, anchor + 1)
target_row = anchor + 1
else:
target_row = rows["FLOW-047"]
set_row_values(ws, target_row, enrich_flow(FLOWS["FLOW-047"], case_rows))
def update_flow_details(ws, case_rows: dict[str, dict[str, str]]) -> None:
insert_at = delete_detail_rows(ws, {"FLOW-030", "FLOW-045", "FLOW-047"})
style_row = max(2, min(insert_at - 1, ws.max_row))
rows_to_add: list[list[str]] = []
for flow_id in ["FLOW-030", "FLOW-045", "FLOW-047"]:
case_ids = [item.strip() for item in FLOWS[flow_id]["用例编号序列"].split("->") if item.strip()]
for index, case_id in enumerate(case_ids, start=1):
case = case_rows.get(case_id, {})
rows_to_add.append(
[
flow_id,
str(index),
case_id,
case.get("所属模块", ""),
case.get("用例标题", ""),
case.get("前置条件", ""),
case.get("预期结果", ""),
]
)
for offset, values in enumerate(rows_to_add):
row = insert_at + offset
ws.insert_rows(row)
copy_row_style(ws, style_row, row)
for col, value in enumerate(values, start=1):
ws.cell(row, col).value = value
def recompute_coverage(wb, case_rows: dict[str, dict[str, str]]) -> None:
summary = wb["链路流程线"]
hs = header_map(summary)
flow_by_case: dict[str, list[str]] = {}
for row in range(2, summary.max_row + 1):
flow_id = str(summary.cell(row, hs["链路编号"]).value or "").strip()
seq = str(summary.cell(row, hs["用例编号序列"]).value or "")
for case_id in [item.strip() for item in seq.split("->") if item.strip()]:
flow_by_case.setdefault(case_id, []).append(flow_id)
ws = wb["用例覆盖索引"]
h = header_map(ws)
rows = row_by_value(ws, h["用例编号"])
template_row = max(rows.values()) if rows else 2
for case_id, flows in flow_by_case.items():
if case_id in rows:
row = rows[case_id]
else:
row = ws.max_row + 1
ws.insert_rows(row)
copy_row_style(ws, template_row, row)
case = case_rows.get(case_id, {})
values = {
"用例编号": case_id,
"所属模块": case.get("所属模块", ""),
"用例标题": case.get("用例标题", ""),
"覆盖次数": str(len(flows)),
"出现链路": "、".join(flows),
"是否已覆盖": "是" if flows else "否",
}
if case_id in {"S-FLOW-054", "S-FLOW-055", "S-FLOW-056"}:
values["备注"] = "验收成功子单处罚分支覆盖"
if case_id == "S-FLOW-048":
values["备注"] = "FLOW-030 取消售后后接异常中子单售后"
set_row_values(ws, row, values)
def update_flow_book() -> None:
backup = FLOW_BOOK.with_name(f"{FLOW_BOOK.stem}.flow030_045_047前备份_{datetime.now():%Y%m%d%H%M%S}{FLOW_BOOK.suffix}")
shutil.copyfile(FLOW_BOOK, backup)
case_rows = load_case_rows()
wb = load_workbook(FLOW_BOOK)
update_flow_summary(wb["链路流程线"], case_rows)
update_flow_details(wb["链路步骤明细"], case_rows)
recompute_coverage(wb, case_rows)
if "说明" in wb.sheetnames:
wb["说明"].append(
[
"更新记录",
f"{datetime.now():%Y-%m-%d %H:%M:%S} 修正 FLOW-030 取消售后后接 S-FLOW-034;FLOW-045 为处罚同意;新增 FLOW-047 为处罚驳回,保留既有 FLOW-046。",
]
)
wb.save(FLOW_BOOK)
def main() -> None:
update_case_book()
update_flow_book()
print("Synced FLOW-030/FLOW-045/FLOW-047 without rebuilding workbook styles.")
if __name__ == "__main__":
main()
from __future__ import annotations
from copy import copy
from datetime import datetime
from pathlib import Path
import sys
import openpyxl
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
CASE_BOOK = ROOT / "测试用例" / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
FLOW_BOOK = ROOT / "测试用例" / "订单状态流转_链路流程线整理.xlsx"
CASE_UPDATES = {
"S-FLOW-054": {
"跨端/状态断言": "1. 处罚记录的主单号、来源单号、子单号、责任方、金额在丸掌柜侧一致。\n"
"2. 处罚只作用于目标子单,不误影响同主单其他子单。\n"
"3. 后续由 S-FLOW-055 或 S-FLOW-056 验证丸友集处理结果。",
"可组合/被引用关系": "承接 S-FLOW-049 后的已验收/验收成功子单;后续分支为 S-FLOW-055/S-FLOW-056;被 FLOW-030、FLOW-045 引用。",
"自动化状态": "已自动化",
"备注": "2026-07-02 修正:S-FLOW-049 后接验收成功子单处罚,后续分支为 S-FLOW-055/S-FLOW-056。",
},
"S-FLOW-055": {
"用例标题": "【丸友集】验收成功子单处罚-同意赔偿",
"预期结果": "1. 丸友集侧目标处罚/申诉记录处理完成或显示已完成/已通过。\n"
"2. 处罚金额等于申请处罚金额。\n"
"3. 丸掌柜侧子单处罚单同步为已完成或同意赔偿结果。",
"跨端/状态断言": "丸友集申诉工单、丸掌柜子单处罚单按丸掌柜主单号/来源单号可定位同一记录;处理状态和金额一致。",
"可组合/被引用关系": "承接 S-FLOW-054;FLOW-030 处罚同意赔偿分支引用。",
"自动化状态": "已自动化",
"备注": "2026-07-02 修正:作为 S-FLOW-054 后续同意赔偿分支,不再仅作为 S-FLOW-063 别名。",
},
"S-FLOW-056": {
"用例标题": "【丸友集】验收成功子单处罚-驳回处罚",
"预期结果": "1. 丸友集侧目标处罚/申诉记录处理完成或显示已拒绝/已驳回。\n"
"2. 处罚金额为0或页面对应未赔偿结果。\n"
"3. 丸掌柜侧子单处罚单同步为已拒绝或驳回结果。",
"跨端/状态断言": "丸友集申诉工单、丸掌柜子单处罚单按丸掌柜主单号/来源单号可定位同一记录;驳回状态和金额结果一致。",
"可组合/被引用关系": "承接 S-FLOW-054;FLOW-045 处罚驳回分支引用。",
"自动化状态": "已自动化",
"备注": "2026-07-02 修正:作为 S-FLOW-054 后续驳回处罚分支,不再仅作为 S-FLOW-064 别名。",
},
}
def headers(ws) -> dict[str, int]:
return {str(ws.cell(1, col).value or ""): col for col in range(1, ws.max_column + 1)}
def copy_row_style(ws, src_row: int, dst_row: int) -> None:
for col in range(1, ws.max_column + 1):
src = ws.cell(src_row, col)
dst = ws.cell(dst_row, col)
if src.has_style:
dst._style = copy(src._style)
dst.font = copy(src.font)
dst.fill = copy(src.fill)
dst.border = copy(src.border)
dst.alignment = copy(src.alignment)
dst.number_format = src.number_format
def delete_rows_by_first_col(ws, keys: set[str]) -> None:
for row in range(ws.max_row, 1, -1):
if str(ws.cell(row, 1).value or "") in keys:
ws.delete_rows(row)
def update_case_book() -> None:
wb = openpyxl.load_workbook(CASE_BOOK)
ws = wb["订单状态流转"]
h = headers(ws)
for row in range(2, ws.max_row + 1):
case_id = str(ws.cell(row, h["用例编号"]).value or "")
updates = CASE_UPDATES.get(case_id)
if not updates:
continue
for key, value in updates.items():
if key in h:
ws.cell(row, h[key]).value = value
wb.save(CASE_BOOK)
def sync_flow_book_from_generator() -> None:
from scripts.generate_order_status_flow_lines_excel import main as generate_flow_lines
generate_flow_lines()
wb = openpyxl.load_workbook(FLOW_BOOK)
if "说明" not in wb.sheetnames:
ws = wb.create_sheet("说明")
ws.append(["类型", "内容"])
wb["说明"].append(["更新记录", f"{datetime.now():%Y-%m-%d %H:%M:%S} FLOW-030 改为 S-FLOW-049 -> S-FLOW-054 -> S-FLOW-055;新增 FLOW-045 处罚驳回分支。"])
wb.save(FLOW_BOOK)
def main() -> None:
update_case_book()
sync_flow_book_from_generator()
print("FLOW-030/FLOW-045 penalty branches synced")
if __name__ == "__main__":
main()
from __future__ import annotations
from copy import copy
from datetime import datetime
from pathlib import Path
import shutil
from openpyxl import load_workbook
ROOT = Path(__file__).resolve().parents[1]
CASE_BOOK = ROOT / "测试用例" / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
FLOW_BOOK = ROOT / "测试用例" / "订单状态流转_链路流程线整理.xlsx"
CASE_083 = {
"用例编号": "S-FLOW-083",
"来源/组合依据": "FLOW-030 实测:S-FLOW-034 后丸掌柜子单列表售后中目标行展示【取消售后】入口。",
"所属模块": "丸掌柜发起售后",
"用例标题": "【丸掌柜】异常中子单售后中状态-取消子单售后",
"前置条件": "已执行 S-FLOW-034,丸掌柜已对异常中子单发起子单售后;丸掌柜订单详情子单列表目标子单状态为售后中,操作列存在【取消售后】入口。",
"执行步骤": "1. 登录丸掌柜主账号。\n2. 进入商家管理-订单管理。\n3. 按主单号或来源单号查询目标订单。\n4. 打开订单详情并定位子单列表中售后中的目标子单。\n5. 点击目标子单行内【取消售后】。\n6. 在取消售后弹窗中填写取消原因。\n7. 点击【确认取消】。\n8. 回到订单详情,回读主单状态、子单汇总状态、目标子单列表状态和售后信息。\n9. 登录丸友集账号,进入订单列表,按来源单号回查目标订单状态。",
"预期结果": "1. 丸掌柜侧异常中子单售后取消提交成功。\n2. 目标子单不再显示售后中,恢复为异常中或页面对应可继续处理状态。\n3. 售后取消原因可在订单详情/日志中追溯。\n4. 丸友集侧目标订单不再停留在售后中。\n5. 后续可按页面规则重新对异常中子单发起售后或选择其他异常中后续处理。",
"跨端/状态断言": "1. 本用例处理的是丸掌柜侧子单行内【取消售后】,不是丸友集侧处理/拒绝子单售后。\n2. 与 S-FLOW-036 区分:S-FLOW-036 为丸友集侧拒绝/取消子单售后,S-FLOW-083 为丸掌柜侧主动取消已发起的子单售后。\n3. 查询字段使用来源单号或主单号,不混用来源单号到订单号字段。",
"可组合/被引用关系": "承接 S-FLOW-034;被 FLOW-030 引用;取消后可重新接 S-FLOW-034 或按异常中子单后续链路处理。",
"自动化状态": "已自动化",
"备注": "2026-07-02 新增:覆盖丸掌柜侧异常中子单售后中行内【取消售后】入口。",
}
FLOW_030_SEQ = "S-FLOW-007 -> S-FLOW-014 -> S-FLOW-021 -> S-FLOW-026 -> S-FLOW-040 -> S-FLOW-043 -> S-FLOW-045 -> S-FLOW-046 -> S-FLOW-048 -> S-FLOW-034 -> S-FLOW-083"
FLOW_030_UPDATE = {
"链路编号": "FLOW-030",
"链路名称": "待验收阶段-发起售后-取消售后-异常中子单发起售后-取消子单售后",
"链路类型": "待验收售后取消后接异常中子单售后取消",
"终点状态": "异常中子单售后已取消;目标子单恢复异常中或页面对应可继续处理状态",
"用例编号序列": FLOW_030_SEQ,
"适用场景": "验证待验收阶段售后取消后,丸掌柜子单列表回到异常中;对异常中子单发起售后后,再由丸掌柜侧取消该子单售后。",
"数据复用建议": "同一订单在 S-FLOW-034 后继续接 S-FLOW-083;该分支与丸友集侧 S-FLOW-035/S-FLOW-036 处理分支互斥。",
"注意事项/缺口": "S-FLOW-083 是丸掌柜侧行内取消售后,不是丸友集侧处理售后;取消后可按页面实际状态决定是否重新接 S-FLOW-034。",
}
def header_map(ws) -> dict[str, int]:
return {str(ws.cell(1, col).value or "").strip(): col for col in range(1, ws.max_column + 1) if ws.cell(1, col).value}
def copy_cell_style(src, dst) -> None:
if src.has_style:
dst._style = copy(src._style)
dst.font = copy(src.font)
dst.fill = copy(src.fill)
dst.border = copy(src.border)
dst.alignment = copy(src.alignment)
dst.number_format = src.number_format
dst.protection = copy(src.protection)
def copy_row_style(ws, src_row: int, dst_row: int) -> None:
ws.row_dimensions[dst_row].height = ws.row_dimensions[src_row].height
for col in range(1, ws.max_column + 1):
copy_cell_style(ws.cell(src_row, col), ws.cell(dst_row, col))
def row_by_value(ws, col: int) -> dict[str, int]:
rows: dict[str, int] = {}
for row in range(2, ws.max_row + 1):
value = str(ws.cell(row, col).value or "").strip()
if value and value not in rows:
rows[value] = row
return rows
def set_row_values(ws, row: int, values: dict[str, str]) -> None:
h = header_map(ws)
for key, value in values.items():
if key in h:
ws.cell(row, h[key]).value = value
def update_case_book() -> None:
backup = CASE_BOOK.with_name(f"{CASE_BOOK.stem}.s_flow_083前备份_{datetime.now():%Y%m%d%H%M%S}{CASE_BOOK.suffix}")
shutil.copyfile(CASE_BOOK, backup)
wb = load_workbook(CASE_BOOK)
ws = wb["订单状态流转"]
h = header_map(ws)
rows = row_by_value(ws, h["用例编号"])
if "S-FLOW-083" in rows:
row = rows["S-FLOW-083"]
else:
anchor = rows.get("S-FLOW-082") or max(rows.values())
ws.insert_rows(anchor + 1)
copy_row_style(ws, anchor, anchor + 1)
row = anchor + 1
set_row_values(ws, row, CASE_083)
wb.save(CASE_BOOK)
def load_case_rows() -> dict[str, dict[str, str]]:
wb = load_workbook(CASE_BOOK, data_only=True)
ws = wb["订单状态流转"]
h = header_map(ws)
rows: dict[str, dict[str, str]] = {}
for row in range(2, ws.max_row + 1):
case_id = str(ws.cell(row, h["用例编号"]).value or "").strip()
if case_id:
rows[case_id] = {key: str(ws.cell(row, col).value or "") for key, col in h.items()}
return rows
def enrich_flow(values: dict[str, str], case_rows: dict[str, dict[str, str]]) -> dict[str, str]:
case_ids = [item.strip() for item in values["用例编号序列"].split("->") if item.strip()]
refs = []
modules = []
for case_id in case_ids:
case = case_rows.get(case_id, {})
refs.append(f"{case_id}({case.get('用例标题', '')})")
module = case.get("所属模块", "")
if module and module not in modules:
modules.append(module)
enriched = dict(values)
enriched["用例链路(编号+标题)"] = " -> ".join(refs)
enriched["涉及模块"] = " -> ".join(modules)
return enriched
def delete_detail_rows(ws, flow_id: str) -> int:
insert_at = ws.max_row + 1
for row in range(ws.max_row, 1, -1):
if str(ws.cell(row, 1).value or "").strip() == flow_id:
insert_at = min(insert_at, row)
ws.delete_rows(row)
return insert_at if insert_at <= ws.max_row + 1 else ws.max_row + 1
def update_flow_book() -> None:
backup = FLOW_BOOK.with_name(f"{FLOW_BOOK.stem}.s_flow_083前备份_{datetime.now():%Y%m%d%H%M%S}{FLOW_BOOK.suffix}")
shutil.copyfile(FLOW_BOOK, backup)
case_rows = load_case_rows()
wb = load_workbook(FLOW_BOOK)
summary = wb["链路流程线"]
hs = header_map(summary)
flow_rows = row_by_value(summary, hs["链路编号"])
set_row_values(summary, flow_rows["FLOW-030"], enrich_flow(FLOW_030_UPDATE, case_rows))
details = wb["链路步骤明细"]
insert_at = delete_detail_rows(details, "FLOW-030")
style_row = max(2, min(insert_at - 1, details.max_row))
for offset, case_id in enumerate([item.strip() for item in FLOW_030_SEQ.split("->") if item.strip()]):
row = insert_at + offset
details.insert_rows(row)
copy_row_style(details, style_row, row)
case = case_rows.get(case_id, {})
values = [
"FLOW-030",
str(offset + 1),
case_id,
case.get("所属模块", ""),
case.get("用例标题", ""),
case.get("前置条件", ""),
case.get("预期结果", ""),
]
for col, value in enumerate(values, start=1):
details.cell(row, col).value = value
coverage = wb["用例覆盖索引"]
hc = header_map(coverage)
coverage_rows = row_by_value(coverage, hc["用例编号"])
if "S-FLOW-083" not in coverage_rows:
template = coverage_rows.get("S-FLOW-082") or max(coverage_rows.values())
row = coverage.max_row + 1
coverage.insert_rows(row)
copy_row_style(coverage, template, row)
else:
row = coverage_rows["S-FLOW-083"]
set_row_values(
coverage,
row,
{
"用例编号": "S-FLOW-083",
"所属模块": CASE_083["所属模块"],
"用例标题": CASE_083["用例标题"],
"覆盖次数": "1",
"出现链路": "FLOW-030",
"是否已覆盖": "是",
"备注": "新增丸掌柜侧取消异常中子单售后分支",
},
)
if "说明" in wb.sheetnames:
wb["说明"].append(["更新记录", f"{datetime.now():%Y-%m-%d %H:%M:%S} 新增 S-FLOW-083,并将 FLOW-030 接到 S-FLOW-034 -> S-FLOW-083。"])
wb.save(FLOW_BOOK)
def main() -> None:
update_case_book()
update_flow_book()
print("Synced S-FLOW-083 and FLOW-030.")
if __name__ == "__main__":
main()
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