diff --git a/app/tasks/material_tasks.py b/app/tasks/material_tasks.py index 9e9b0e9..f72b682 100644 --- a/app/tasks/material_tasks.py +++ b/app/tasks/material_tasks.py @@ -127,32 +127,41 @@ def batch_disable_projects(data: Dict[str, Any], cookies: Dict[str, str], df: pd print('表单已自动提交至下一步') -def batch_modify_materials(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str, - ) -> None: +def batch_modify_materials(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str) -> None: """ - 材料批量修改后台任务 + 材料批量修改后台任务 - 在后台线程中批量修改材料,从 Excel 文件中读取材料编码。 - 执行完成后会更新简道云表单并自动提交工作流。 + 在后台线程中批量修改材料,从 Excel 文件中读取材料编码。 + 执行完成后会更新简道云表单并自动提交工作流。 - Args: - data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典 - cookies: 用户登录 F6 系统的 cookies 信息 - df: Excel 文件读取的内容,DataFrame 格式,第一列为材料编码 - save_path: Excel 文件保存的地址,执行完成后会删除此文件 + Args: + data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典 + cookies: 用户登录 F6 系统的 cookies 信息 + df: Excel 文件读取的内容,DataFrame 格式,列顺序为: + [原材料编码, 新材料编码, 品牌, 名称, 规格] + save_path: Excel 文件保存的地址,执行完成后会删除此文件 - Returns: - None + 注意: + - 无效的材料编码(None、空字符串)会被跳过 + - Excel 中某字段为空(NaN 或空字符串)时,保留原材料中的对应字段 + - 执行完成后会自动删除上传的文件 + - 执行结果会更新到简道云表单 + """ - 注意: - - 无效的材料编码(None、空字符串)会被跳过 - - 执行完成后会自动删除上传的文件 - - 执行结果会更新到简道云表单 - """ + def safe_str(val): + """将值转为字符串,NaN 返回空字符串""" + if pd.isna(val): + return "" + return str(val).strip() + + def should_update(new_val): + """判断是否应该用 new_val 更新:非 NaN 且非空字符串""" + return not (pd.isna(new_val) or (isinstance(new_val, str) and new_val.strip() == "")) # 获取门店id org_id = get_operate_org_id(data) - # 获取材料信息 + + # 第一步:获取所有材料列表(分页) json_data = { 'keyWord': '', 'idOwnOrg': org_id, @@ -168,12 +177,7 @@ def batch_modify_materials(data: Dict[str, Any], cookies: Dict[str, str], df: pd 'labelName': '', 'spec': '', 'applyModel': '', - 'sellPurchaseStatuses': [ - 2, - 3, - 4, - 5, - ], + 'sellPurchaseStatuses': [2, 3, 4, 5], 'customInvoiceCategory': 0, 'getThirdPlatformCode': 0, } @@ -183,41 +187,47 @@ def batch_modify_materials(data: Dict[str, Any], cookies: Dict[str, str], df: pd cookies=cookies, json=json_data, ) + response.raise_for_status() + total_pages = response.json().get("data", {}).get("totalPages", 0) + all_materials_list = [] - total_pages = response.json().get("data", {}).get("totalPages", "") - for page in tqdm(range(1, total_pages + 1)): - json_data['currentPage'] = str(page) - response = requests.post( + for page in tqdm(range(1, total_pages + 1), desc="获取材料列表"): + json_data['currentPage'] = page + resp = requests.post( 'https://ids-goods.f6car.com/f6-ids-goods/part/getExactPartStockInfo', cookies=cookies, json=json_data, ) - materials_list = response.json().get("data", {}).get("records", []) - all_materials_list.extend(materials_list) + resp.raise_for_status() + records = resp.json().get("data", {}).get("records", []) + all_materials_list.extend(records) - # 构建更新映射:{原customCode: {new_customCode, brand, name, spec}} + # 第二步:构建 update_map(只存原始值,不预处理) update_map = {} for _, row in df.iterrows(): - orig_code = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else None - if not orig_code: + orig_code = row.iloc[0] # 原材料编码 + if pd.isna(orig_code) or str(orig_code).strip() == "": continue + orig_code = str(orig_code).strip() update_map[orig_code] = { - "customCode": str(row.iloc[1]).strip() if pd.notna(row.iloc[1]) else "", - "brand": str(row.iloc[2]).strip() if pd.notna(row.iloc[2]) else "", - "name": str(row.iloc[3]).strip() if pd.notna(row.iloc[3]) else "", - "spec": str(row.iloc[4]).strip() if pd.notna(row.iloc[4]) else "", + "new_customCode": row.iloc[1], + "new_brand": row.iloc[2], + "new_name": row.iloc[3], + "new_spec": row.iloc[4], } - # 遍历获取到的材料信息修改材料属性 - code_list = df.iloc[:, 0].dropna().astype(str).tolist() - res_data_list = [] + # 第三步:遍历材料,按需更新 results = [] - for item in tqdm(all_materials_list): + for item in tqdm(all_materials_list, desc="处理材料更新"): custom_code = item.get("customCode") - # 判断当前材料编码是否在文件中 if not custom_code or str(custom_code) not in update_map: continue + part_id = item.get("partId") + if not part_id: + results.append({'材料编码': custom_code, '状态': '缺少 partId'}) + continue + try: # 获取材料明细 params = { @@ -225,14 +235,13 @@ def batch_modify_materials(data: Dict[str, Any], cookies: Dict[str, str], df: pd 'customInvoiceCategory': '0', 'getThirdPlatformCode': '0', } - materials_response = requests.get( 'https://ids-goods.f6car.com/f6-ids-goods/part/getPartInfo', params=params, cookies=cookies, ) if materials_response.status_code != 200: - results.append({'材料编码': custom_code, '状态': '获取明细失败'}) + results.append({'材料编码': custom_code, '状态': f'获取明细失败: {materials_response.status_code}'}) continue detail = materials_response.json().get("data") @@ -240,42 +249,277 @@ def batch_modify_materials(data: Dict[str, Any], cookies: Dict[str, str], df: pd results.append({'材料编码': custom_code, '状态': '明细为空'}) continue - updates = update_map[custom_code] - # === 关键:强制覆盖,空值也写入 === - detail["customCode"] = updates["customCode"] # 可能是 "" - detail["brand"] = updates["brand"] # 可能是 "" - detail["name"] = updates["name"] # 可能是 "" - detail["showName"] = updates["name"] # 同步 showName(重要!) - detail["spec"] = updates["spec"] # 可能是 "" + updates = update_map[str(custom_code)] - # 修复价格和数组(必须!) + # 安全更新字段:仅当 Excel 提供有效值时才覆盖 + if should_update(updates["new_customCode"]): + detail["customCode"] = safe_str(updates["new_customCode"]) + if should_update(updates["new_brand"]): + detail["brand"] = safe_str(updates["new_brand"]) + if should_update(updates["new_name"]): + name_val = safe_str(updates["new_name"]) + detail["name"] = name_val + detail["showName"] = name_val # 同步 showName + if should_update(updates["new_spec"]): + detail["spec"] = safe_str(updates["new_spec"]) + + # 修复价格格式和数组(必须!) for f in ["purchasePrice", "sellPrice"]: - if isinstance(detail.get(f), (int, float)): - detail[f] = f"{detail[f]:.2f}" + val = detail.get(f) + if isinstance(val, (int, float)): + detail[f] = f"{val:.2f}" if detail.get("partBarCodeVos") is None: detail["partBarCodeVos"] = [] + # 提交更新 update_resp = requests.post( 'https://ids-goods.f6car.com/f6-ids-goods/part/updateAreaPartBasicInfo', cookies=cookies, json=detail ) + if update_resp.status_code == 200 and update_resp.json().get("code") == 200: results.append({'材料编码': custom_code, '状态': '修改成功'}) else: msg = update_resp.json().get("message", "未知错误") results.append({'材料编码': custom_code, '状态': f'修改失败: {msg}'}) + except requests.exceptions.RequestException as e: - results.append({'材料编码': custom_code, '状态': f'修改属性失败: {str(e)}'}) - pass + results.append({'材料编码': custom_code, '状态': f'请求异常: {str(e)}'}) + except Exception as e: + results.append({'材料编码': custom_code, '状态': f'内部错误: {str(e)}'}) - print({'msg': '已执行', 'msg_details': f'{results}'}) - logger.info(f"批量修改结果: {results}") - os.remove(save_path) - print(f'{save_path}已删除') - # 调用api回写改掉 执行明细与执行状态 - msg = update_jiandaoyun(data, f'{results}') + # 第四步:清理与回写 + print({'msg': '已执行', 'msg_details': results}) + logger.info(f"材料批量修改结果: {results}") + try: + os.remove(save_path) + print(f'{save_path} 已删除') + except Exception as e: + logger.error(f"删除文件失败: {e}") + + # 回写简道云 + msg = update_jiandaoyun(data, str(results)) + if msg.get('msg'): + approve_workflow(data) + print('表单已自动提交至下一步') + + +def batch_modify_projects(data: Dict[str, Any], cookies: Dict[str, str], df: pd.DataFrame, save_path: str) -> None: + """ + 项目批量修改后台任务 + + 在后台线程中批量修改项目,从 Excel 文件中读取项目编码。 + 执行完成后会更新简道云表单并自动提交工作流。 + + Args: + data: 包含表单ID(api_key)、表单ID(entry_id)、数据ID(data_id)的字典 + cookies: 用户登录 F6 系统的 cookies 信息 + df: Excel 文件读取的内容,DataFrame 格式,列顺序为: + [0:原项目编码, 1:新项目编码, 2:新项目名称, 3:业务分类, + 4:销项税率, 5:项目说明, 6:车辆分类, 7:工时单价, 8:工时] + save_path: Excel 文件保存的地址,执行完成后会删除此文件 + + 注意: + - 无效的项目编码(None、空字符串)会被跳过 + - Excel 中某字段为空(NaN 或空字符串)时,保留原始项目中的对应字段 + - 执行完成后会自动删除上传的文件 + - 执行结果会更新到简道云表单 + """ + + def safe_str(val): + """将值转为字符串,NaN 返回空字符串""" + if pd.isna(val): + return "" + return str(val).strip() + + def safe_float(val): + """安全转换为 float,失败返回 None""" + if pd.isna(val): + return None + try: + return float(val) + except (ValueError, TypeError): + return None + + def should_update(new_val): + """判断是否应该用 new_val 更新:非 NaN 且非空字符串""" + return not (pd.isna(new_val) or (isinstance(new_val, str) and new_val.strip() == "")) + + # 获取门店id + org_id = get_operate_org_id(data) + + # 获取服务分类(用于映射业务分类名称 → pkId) + init_add_resp = requests.post( + 'https://ids-goods.f6car.cn/f6-ids-goods/service/initAdd', + cookies=cookies, + data={'idOwnOrg': org_id} + ) + init_add_resp.raise_for_status() + service_category_list = init_add_resp.json().get("data", {}).get("serviceCategory", []) + category_name_to_pk = {item["name"]: item["pkId"] for item in service_category_list} + + # 第一步:获取所有项目列表(分页) + json_data = { + 'param': '', + 'name': '', + 'customCode': '', + 'currentPage': 1, + 'pageSize': 100, + 'isDel': 0, # 只查启用的 + 'customInvoiceCategory': 0, + 'idOwnOrg': org_id, + } + + response = requests.post( + 'https://ids-goods.f6car.cn/f6-ids-goods/service/getServiceList', + cookies=cookies, + json=json_data, + ) + response.raise_for_status() + total_pages = response.json().get("data", {}).get("totalPages", 0) + + all_project_list = [] + for page in tqdm(range(1, total_pages + 1), desc="获取项目列表"): + json_data['currentPage'] = page + resp = requests.post( + 'https://ids-goods.f6car.cn/f6-ids-goods/service/getServiceList', + cookies=cookies, + json=json_data, + ) + resp.raise_for_status() + records = resp.json().get("data", {}).get("records", []) + all_project_list.extend(records) + + # 第二步:构建 update_map + update_map = {} + for _, row in df.iterrows(): + orig_code = row.iloc[0] + if pd.isna(orig_code) or str(orig_code).strip() == "": + continue + orig_code = str(orig_code).strip() + + # 计算工时费 = 单价 * 工时 + price = safe_float(row.iloc[7]) + work_hour = safe_float(row.iloc[8]) + amount = None + if price is not None and work_hour is not None: + amount = round(price * work_hour, 2) + + # 业务分类映射 + category_name = row.iloc[3] + category_pk = None + if should_update(category_name): + cat_name_clean = safe_str(category_name) + category_pk = category_name_to_pk.get(cat_name_clean) + if category_pk is None: + logger.warning(f"业务分类 '{cat_name_clean}' 未在系统中找到") + + update_map[orig_code] = { + "new_customCode": row.iloc[1], + "new_name": row.iloc[2], + "new_serviceCategoryId": category_pk, + "new_taxRate": row.iloc[4], + "new_memo": row.iloc[5], + "new_carCategoryName": row.iloc[6], + "new_price": price, + "new_workHour": work_hour, + "new_amount": amount, + } + + # 第三步:遍历项目,按需更新 + results = [] + for item in tqdm(all_project_list, desc="处理项目更新"): + custom_code = item.get("customCode") + if not custom_code or str(custom_code) not in update_map: + continue + + service_id = item.get("pkId") # 项目主键 + if not service_id: + results.append({'项目编码': custom_code, '状态': '缺少 pkId'}) + continue + + try: + # 获取项目明细 + params = { + 'serviceId': service_id, + 'customInvoiceCategory': '0', + } + detail_resp = requests.get( + 'https://ids-goods.f6car.cn/f6-ids-goods/service/viewService', + params=params, + cookies=cookies, + ) + if detail_resp.status_code != 200: + results.append({'项目编码': custom_code, '状态': f'获取明细失败: {detail_resp.status_code}'}) + continue + + detail = detail_resp.json().get("data") + if not detail: + results.append({'项目编码': custom_code, '状态': '明细为空'}) + continue + + updates = update_map[str(custom_code)] + + # === 安全更新字段(仅非空时覆盖)=== + if should_update(updates["new_customCode"]): + detail["customCode"] = safe_str(updates["new_customCode"]) + if should_update(updates["new_name"]): + name_val = safe_str(updates["new_name"]) + detail["name"] = name_val + if "showName" in detail: + detail["showName"] = name_val + + if updates["new_serviceCategoryId"] is not None: + detail["serviceCategoryId"] = updates["new_serviceCategoryId"] + + if should_update(updates["new_taxRate"]): + detail["taxRate"] = safe_str(updates["new_taxRate"]) + + if should_update(updates["new_memo"]): + detail["memo"] = safe_str(updates["new_memo"]) + + if should_update(updates["new_carCategoryName"]): + detail["carCategoryName"] = safe_str(updates["new_carCategoryName"]) + + if updates["new_price"] is not None: + detail["price"] = f"{updates['new_price']:.2f}" + if updates["new_workHour"] is not None: + detail["workHour"] = f"{updates['new_workHour']:.2f}" + if updates["new_amount"] is not None: + detail["amount"] = f"{updates['new_amount']:.2f}" + + # === 提交更新 === + update_resp = requests.post( + 'https://ids-goods.f6car.cn/f6-ids-goods/service/editService', + cookies=cookies, + json=detail + ) + + if update_resp.status_code == 200 and update_resp.json().get("code") == 200: + results.append({'项目编码': custom_code, '状态': '修改成功'}) + else: + msg = update_resp.json().get("message", "未知错误") + results.append({'项目编码': custom_code, '状态': f'修改失败: {msg}'}) + + except Exception as e: + error_msg = f"异常: {str(e)}" + logger.error(f"项目 {custom_code} 更新出错: {traceback.format_exc()}") + results.append({'项目编码': custom_code, '状态': error_msg}) + + # 第四步:清理与回写 + print({'msg': '已执行', 'msg_details': results}) + logger.info(f"项目批量修改结果: {results}") + + try: + os.remove(save_path) + print(f'{save_path} 已删除') + except Exception as e: + logger.error(f"删除文件失败: {e}") + + # 回写简道云 + msg = update_jiandaoyun(data, str(results)) if msg.get('msg'): approve_workflow(data) print('表单已自动提交至下一步') diff --git a/模板文件/客户信息修改.xlsx b/模板文件/客户信息修改.xlsx index 9bce7ef..f68e790 100644 Binary files a/模板文件/客户信息修改.xlsx and b/模板文件/客户信息修改.xlsx differ