From 283f7849f8d0da148ee7b062e73c94e235bb887a Mon Sep 17 00:00:00 2001 From: panda <1415243231@qq.com> Date: Thu, 22 Jan 2026 10:40:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A1=B9=E7=9B=AE=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tasks/material_tasks.py | 368 ++++++++++++++++++++++++++++++------ 模板文件/客户信息修改.xlsx | Bin 9392 -> 9426 bytes 2 files changed, 306 insertions(+), 62 deletions(-) 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 9bce7ef267a3c236edb3e4a84ba0391dad344d01..f68e790bac666b434de63d9097f0812124b411d3 100644 GIT binary patch delta 4566 zcmZ9QXE@y7zQt$sHd>G(rEh2~oAO0(O{6Fcj!iri(p|enx^d zaOKX^c)>}ZS|vbYPWP;{-;-I7+tHIhUdS3GX=k6Csr>X%qH*NHmKf3{%S#v(Ys1q%rxifCZZ8O*Hj`j@+kp%``8wRp&-bnLcGH>d|;}c;1urp!R@N2{vFWCDnL`nQPiHXKz3dDXze^?conqAovqT?b@DKLAAyL@1Sj1HN~;O+Nu zk-gqDw4KMLAp_q*2CeEt0zSJ(A``oGhOYHly(C8OGemlzi2gwJU@SMq+#b>_Xvskuj6&Oo@G zwE5xTs3qS(Pad_6`9-fut$m)1>W6`<(Y6KB7>|8f2R{^_@uP z9VCXkA~JPxhTmW2x`)5FoT1p_N%1X`_2tUQCF1N7%^cm1VmjGL1;S3B^5;+X7C^gn zi>F}U;O6Mca?^0MO_QBuMZH6-vxO&&O|f+^?<`KD%nnX;x#*x!^-p1V%sRl$lLYAs zq@bD_clwpA!F?FJJ>)dPp720&D+9tW%lEwwZv4(n7X|a(2*rv}JOt{Fh+1W3BGT>d zJ@$O>&;!FD)lS^rpCy0h?hN-K4sr0Q-aw7(8&F5G{_fWb^y=6uV#O>C&1-P8kc%<0 zdLiT^!$XPV7n@u1wXNkeut2j^=STTHE#%)M>?XOY&dmZBWBk>vyAvuOd1**N*Gv58 zU>`MZhuS3^=^*cH=i|R7YW3#-OY?U6RC}|DCXu*?tv3oLtysm29@j^$1l{>iCG`yl zuFi_rTR*u~ZB6sW*Yv2McXr6MqrAv|Xh+K!K3t6#K07Aoq=iGdq+87LyAe6dVnF9Z z8*26oBOmX1nr@P0ZNe`KmI9vQ&HQT7O_>iIMi*B0eHs z#Q#e3dX$Y0E=*NeMM#02LVwh9d%JRg0+*r4^ z`&YG05=x;f5ZT+;Z6Ho z#Ec?$Bt|+dP}TUWbQN9f`?%PbFoq6Of*HD|Ds@gy)2tJ-elfFhdp2j5bnD%|Ys^Z7jdm(V#R?rs8NGXcfHQznS}3 zCL<9@YOvM8&|OvBS^Y^WY@8y_8h??Alzmz|vTO6@m@JeOam!PjqJ__ye64-e@$K!m@ur1YP;7ft7mu2o1SRvYokOlF3C3@+Rwfvgl zrP4ASyG|;>`KLU#3)^?*H=i6TYSaGVBn!>s?<4E}?X3UzwSsKrQY~vlUPm za`5g7+l>SLeQ@bOAP@=Yh5oYgldU=287T!r5Oa$O|!MyOT7l z$$0&`>)Jskn(AfSL)`XE;4?MvT>&!>qe22wQo}moc?EJDOM_GkK0C5#Gc_NY2Z}1A zEi@xx>j4uEl}KzVVdiGTmjV05H;gmlSh@M*hdvYS*(t|;^1Ha;oc;dE%B>f#1?NOB zI}T=|L75Gt`NI3kJ(5E-mut=C`Q-71-2#yr)ul^6Bjs&Nx$%xuJRdr5 zaQS;FCM$e@6}3Y1lL0w~1AUWUb+d7gr5&M=);Q({XN6`?lw38{QaqCVvAzkZ zExs7ro_8^l)#{vqZ=5#i0KbNOpR=i1XpzsR3)$eHX48x^r=G>$L@3qjtO zDW?pj>cfAR1{AxhNf{Z&5asNg8I!oZ;9Y@x4~ps}Z1z^$DFVPto1N1OkSrmEAzZuC~#W2fdoqK@IoRtrI^4 zNvBbuDGFvZ{Qe9<`tD}F;7%O$3?eaiq5D>5TUO!SI5nFrFcyg$=xH(10J2bHl{<`c zZ38`cPNPyho<7CR-Yl95cUMAQ`5JU-=OJ?<;5NrD*eOGgSDFQ-o?qA3EL0*)JS9B& zON^|fOm71_6Ut$2)?TKmMP=|UeK6_*^2RFl5F*Z@lrAEE;7=;fdQ^dvn((9ikK-Rc za8Fp5={UQyzh{uEKk>r$QzB2)_(GQBd&PpQ4sRRawLbudrb za)z4cGUpebPPb*T)$aN@?UuOLIi^P~cBNZp^_pQhW37{vMkJf7K*%_?7~Pa=5495o zyV?^*$>F%A3dtJ>;Y1=f-D8+N@g+|x>UfDDiF{-M2Dca%m~KX^J8bG^Ry0&?pdZP^ zg15Ybrdf=qG|mfaihVD`>j!KPojuey8PKO4bvs$S0}()qlkeTay-pfz1f?AF_i29X ze61HEMvQmS0S^Fu*~%QRR}Z@b96ygAsd+|@hZ6gGpf#pDhgfXxR==!Xh96T>Zgb$T zKHglVFwJ$9Yr-2kWUggB8n$V_o%x)4ceErj_=h>YaFso&S>C5=N^{Zd_{LCM(&C>% z#J#D8shbtr?oV8~_OE|+ZDzGcH8KvhKSH$li!v1?`qoPTF0`x>YY;&Z=8_1?kA)G3~YW%#?00k_;1Ap3ZP*kod{CRbOi5JE{_ z#%UtASd*b!rdO|=x5$9;~}S@ zj4easUQLq#Vr|LSYI3;K-D+~WbD7rN_M(Dv_c1+SdS7zc!_kwf*!f0Y^nDs<)&DC( z%g9QF?6e!P?IO1cl@`2_$(Nr0VAwTQ_P zErFOhW^w|mUu|Jm4(Ib9T-h!pxz!xrIjxRO2Eo-ie;h3s6b(ku+=AK~;$5_VY0b6( z!oaF7fI&`S9%@S1SAd zh)e0T=lo2C?A11d&5pqNRBSq`%$zwmqOA7oo)Pa#)co zl95@ZRsZdk^Lf^(D^4jV{C8spCD)_a*R8p-#hcp&E7+uKYLmq&Xsr6i)d+LcTLxKbHx)Nzo6?0^G$jRW!)ZF!#<-NmO5YgZ%Ki0jG?m^5+*tj7w zIV0jJy6(qNEQ`36z7 zElKs!=5ps_W`&Dz)2JKS55#sSad#Pb-D74oF&rbDCh6BXFZ_y6K0i(`qJ6CUiHK@| z*P+LiZyk*JN&hu=P%Bd;y)Xp%K8k~AKl9G(1WO%SDxfLqF|^CI%z~Gh6`M(oEw#%- z1g(0gt`^+{mr&*8D0qi1+Ug!Fc8{ju(BZ}tC71g~fG}=To!aX`Dx~2_mfGn|Mlw$t zZhhM3-6QLY8D%~atd=)DN4jEn$9F~bxAdKV5;3(3i=J+;LvG2nrp_Yzj($J5_=&Hs zXSS#X$9G(AcP|4}h4Al2JA=@5#|_aT-HgA61kzDmLA zRM$JN$CM=12|u4+1rX{o+U+zGXG+OTm^~-~Hkw6_0Pijv1_xCF$6~Ib;@~ojI6LOw z(~>@RQ!oz(3R4DWVH{y1;9(3JCJ8>n^un~jg%}zR9-jYSIBz;92n>SW=p)+yQ$I1^ zp->FKK?yd)yylPuhhaW*aR2)^v&TUX7Q_&7%7a@mYMkuM|2s1Pf$sdp-K>8RH!uJv KoTwi9*ZD8yqKZNQ delta 4550 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