diff --git a/test/resources/test.json b/test/resources/test.json new file mode 100644 index 00000000..9fa92372 --- /dev/null +++ b/test/resources/test.json @@ -0,0 +1,191 @@ +{ + "data": [ + { + "orderCode": "BSO2025103100006", + "orderName": "DP20250927001", + "errorMessage": null, + "usedMaterials": [ + { + "id": "3a1d4b13-25a6-cfb2-7315-159f14b32425", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b13-2467-e64d-d8bc-3957fb6e3240", + "materialName": "适配器块", + "materialCode": "0018-00065", + "quantity": "1块", + "materialTypeId": "efc3bb32-d504-4890-91c0-b64ed3ac80cf", + "materialTypeCode": "0018", + "materialTypeMode": "Consumables", + "materialTypeName": "适配器块", + "locationId": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c", + "locationCode": "0014-0001", + "locationShowName": "0014-0001" + }, + { + "id": "3a1d4b13-2420-8cfe-17f1-5f77a6ff6dc3", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b11-e448-bf90-d0bd-b20758425370", + "materialName": "test1", + "materialCode": "0001-00063", + "quantity": "1块", + "materialTypeId": "3a190c8b-3284-af78-d29f-9a69463ad047", + "materialTypeCode": "0001", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)板", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4", + "locationShowName": "4" + }, + { + "id": "3a1d4b13-2420-73a1-2b4d-7bf6dd993c36", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b11-e448-fea3-8291-0b66ecd06d72", + "materialName": "test1", + "materialCode": "0002-00282", + "quantity": "1块", + "materialTypeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "materialTypeCode": "0002", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4-1/1", + "locationShowName": "4-1/1" + }, + { + "id": "3a1d4b13-2420-e45f-192d-639887ad73b7", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b12-67fc-5f91-13ed-c223d0155399", + "materialName": "test2", + "materialCode": "0010-00059", + "quantity": "1块", + "materialTypeId": "3a192fa4-007d-ec7b-456e-2a8be7a13f23", + "materialTypeCode": "0010", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶板", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5", + "locationShowName": "5" + }, + { + "id": "3a1d4b13-2420-c052-93cc-002f0aae79fc", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-23cb-63e5-10df-6a1d38335163", + "materialId": "3a1d4b12-67fc-60f7-1129-3d1ef2a2d1f8", + "materialName": "test2", + "materialCode": "0007-00211", + "quantity": "1块", + "materialTypeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4", + "materialTypeCode": "0007", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5-1/1", + "locationShowName": "5-1/1" + } + ] + }, + { + "orderCode": "BSO2025103100007", + "orderName": "DP20250927002", + "errorMessage": null, + "usedMaterials": [ + { + "id": "3a1d4b13-264b-aca7-9e97-ab4df186d5c2", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b13-2467-e64d-d8bc-3957fb6e3240", + "materialName": "适配器块", + "materialCode": "0018-00065", + "quantity": "1块", + "materialTypeId": "efc3bb32-d504-4890-91c0-b64ed3ac80cf", + "materialTypeCode": "0018", + "materialTypeMode": "Consumables", + "materialTypeName": "适配器块", + "locationId": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c", + "locationCode": "0014-0001", + "locationShowName": "0014-0001" + }, + { + "id": "3a1d4b13-263e-873e-1331-7e668b411e98", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b11-e448-bf90-d0bd-b20758425370", + "materialName": "test1", + "materialCode": "0001-00063", + "quantity": "1块", + "materialTypeId": "3a190c8b-3284-af78-d29f-9a69463ad047", + "materialTypeCode": "0001", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)板", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4", + "locationShowName": "4" + }, + { + "id": "3a1d4b13-263e-7884-d9e0-b010478b7448", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b11-e448-82e0-6a64-6230ee1bf0a9", + "materialName": "test1", + "materialCode": "0002-00283", + "quantity": "1块", + "materialTypeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb", + "materialTypeCode": "0002", + "materialTypeMode": "Sample", + "materialTypeName": "配液瓶(小)", + "locationId": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "locationCode": "4-1/2", + "locationShowName": "4-1/2" + }, + { + "id": "3a1d4b13-263e-6e99-b513-66047191643f", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b12-67fc-5f91-13ed-c223d0155399", + "materialName": "test2", + "materialCode": "0010-00059", + "quantity": "1块", + "materialTypeId": "3a192fa4-007d-ec7b-456e-2a8be7a13f23", + "materialTypeCode": "0010", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶板", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5", + "locationShowName": "5" + }, + { + "id": "3a1d4b13-263e-5b21-2c41-53e4ea7fe947", + "destinationType": "TempOrder", + "destinationId": "3a1d4b13-260c-9239-5c8a-ecb6fd96dc86", + "materialId": "3a1d4b12-67fc-131a-82ff-87e9e7708f9f", + "materialName": "test2", + "materialCode": "0007-00212", + "quantity": "1块", + "materialTypeId": "3a192c2a-ebb7-58a1-480d-8b3863bf74f4", + "materialTypeCode": "0007", + "materialTypeMode": "Sample", + "materialTypeName": "5ml分液瓶", + "locationId": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "locationCode": "5-1/2", + "locationShowName": "5-1/2" + } + ] + } + ], + "code": 1, + "message": "", + "timestamp": 1761891208109 +} + +25-10-31 [14:27:52,203] [ERROR] 从Bioyond同步物料数据失败: 'BottleCarrier' object has no attribute 'tracker' [sync_from_external:83] [unilabos.utils.log.station] +Traceback (most recent call last): + File "C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_studio\station.py", line 73, in sync_from_external + unilab_resources = resource_bioyond_to_plr( + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\ML\GitHub\Uni-Lab-OS\unilabos\resources\graphio.py", line 661, in resource_bioyond_to_plr + bottle.tracker.liquids = [ + ^^^^^^^^^^^^^^ +AttributeError: 'BottleCarrier' object has no attribute 'tracker' \ No newline at end of file diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py index 5ce49e00..c40945e7 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py @@ -253,7 +253,7 @@ class BioyondCellWorkstation(BioyondWorkstation): def auto_feeding4to3( self, # ★ 修改点:默认模板路径 - xlsx_path: Optional[str] = "/Users/calvincao/Desktop/work/uni-lab-all/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx", + xlsx_path: Optional[str] = "unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\material_template.xlsx", # ---------------- WH4 - 加样头面 (Z=1, 12个点位) ---------------- WH4_x1_y1_z1_1_materialName: str = "", WH4_x1_y1_z1_1_quantity: float = 0.0, WH4_x2_y1_z1_2_materialName: str = "", WH4_x2_y1_z1_2_quantity: float = 0.0, @@ -630,7 +630,12 @@ class BioyondCellWorkstation(BioyondWorkstation): response = self._post_lims("/api/lims/order/orders", orders) print(response) # 等待任务报送成功 - order_code = response.get("data", {}).get("orderCode") + data_list = response.get("data", []) + if data_list: + order_code = data_list[0].get("orderCode") + else: + order_code = None + if not order_code: logger.error("上料任务未返回有效 orderCode!") return response @@ -963,6 +968,119 @@ class BioyondCellWorkstation(BioyondWorkstation): logger.error(f"✗ 执行失败: {e}") return {"success": False, "error": str(e)} +def create_material( + self, + material_name: str, + type_id: str, + warehouse_name: str, + location_name_or_id: Optional[str] = None + ) -> Dict[str, Any]: + """创建单个物料并可选入库。 + Args: + material_name: 物料名称(会优先匹配配置模板)。 + type_id: 物料类型 ID(若为空则尝试从配置推断)。 + warehouse_name: 需要入库的仓库名称;若为空则仅创建不入库。 + location_name_or_id: 具体库位名称(如 A01)或库位 UUID,由用户指定。 + Returns: + 包含创建结果、物料ID以及入库结果的字典。 + """ + material_name = (material_name or "").strip() + if not material_name: + raise ValueError("material_name 不能为空") + resolved_type_id = (type_id or "").strip() + # 优先从 SOLID_LIQUID_MAPPINGS 中获取模板数据 + template = SOLID_LIQUID_MAPPINGS.get(material_name) + if not template: + raise ValueError(f"在配置中未找到物料 {material_name} 的模板,请检查 SOLID_LIQUID_MAPPINGS。") + material_data: Dict[str, Any] + material_data = deepcopy(template) + # 最终确保 typeId 为调用方传入的值 + if resolved_type_id: + material_data["typeId"] = resolved_type_id + material_data["name"] = material_name + # 生成唯一编码 + def _generate_code(prefix: str) -> str: + normalized = re.sub(r"\W+", "_", prefix) + normalized = normalized.strip("_") or "material" + return f"{normalized}_{datetime.now().strftime('%Y%m%d%H%M%S')}" + if not material_data.get("code"): + material_data["code"] = _generate_code(material_name) + if not material_data.get("barCode"): + material_data["barCode"] = "" + # 处理数量字段类型 + def _to_number(value: Any, default: float = 0.0) -> float: + try: + if value is None: + return default + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str) and value.strip() == "": + return default + return float(value) + except (TypeError, ValueError): + return default + material_data["quantity"] = _to_number(material_data.get("quantity"), 1.0) + material_data["warningQuantity"] = _to_number(material_data.get("warningQuantity"), 0.0) + unit = material_data.get("unit") or "个" + material_data["unit"] = unit + if not material_data.get("parameters"): + material_data["parameters"] = json.dumps({"unit": unit}, ensure_ascii=False) + # 补充子物料信息 + details = material_data.get("details") or [] + if not isinstance(details, list): + logger.warning("details 字段不是列表,已忽略。") + details = [] + else: + for idx, detail in enumerate(details, start=1): + if not isinstance(detail, dict): + continue + if not detail.get("code"): + detail["code"] = f"{material_data['code']}_{idx:02d}" + if not detail.get("name"): + detail["name"] = f"{material_name}_detail_{idx:02d}" + if not detail.get("unit"): + detail["unit"] = unit + if not detail.get("parameters"): + detail["parameters"] = json.dumps({"unit": detail.get("unit", unit)}, ensure_ascii=False) + if "quantity" in detail: + detail["quantity"] = _to_number(detail.get("quantity"), 1.0) + material_data["details"] = details + create_result = self._post_lims("/api/lims/storage/material", material_data) + # 解析创建结果中的物料 ID + material_id: Optional[str] = None + if isinstance(create_result, dict): + data_field = create_result.get("data") + if isinstance(data_field, str): + material_id = data_field + elif isinstance(data_field, dict): + material_id = data_field.get("id") or data_field.get("materialId") + inbound_result: Optional[Dict[str, Any]] = None + location_id: Optional[str] = None + # 按用户指定位置入库 + if warehouse_name and material_id and location_name_or_id: + try: + location_ids, position_names = self._load_warehouse_locations(warehouse_name) + position_to_id = {name: loc_id for name, loc_id in zip(position_names, location_ids)} + target_location_id = position_to_id.get(location_name_or_id, location_name_or_id) + if target_location_id: + location_id = target_location_id + inbound_result = self.storage_inbound(material_id, target_location_id) + else: + inbound_result = {"error": f"未找到匹配的库位: {location_name_or_id}"} + except Exception as exc: + logger.error(f"获取仓库 {warehouse_name} 位置失败: {exc}") + inbound_result = {"error": str(exc)} + return { + "success": bool(isinstance(create_result, dict) and create_result.get("code") == 1 and material_id), + "material_name": material_name, + "material_id": material_id, + "warehouse": warehouse_name, + "location_id": location_id, + "location_name_or_id": location_name_or_id, + "create_result": create_result, + "inbound_result": inbound_result, + } + # -------------------------------- @@ -971,7 +1089,7 @@ if __name__ == "__main__": lab_registry.setup() ws = BioyondCellWorkstation() # logger.info(ws.scheduler_stop()) - # logger.info(ws.scheduler_start()) + logger.info(ws.scheduler_start()) # results = ws.create_materials(SOLID_LIQUID_MAPPINGS) # for r in results: @@ -980,11 +1098,11 @@ if __name__ == "__main__": # result = ws.create_and_inbound_materials() # 继续后续流程 - # logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱 + logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱 # # 使用正斜杠或 Path 对象来指定文件路径 - # excel_path = Path("unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025092701.xlsx") - # logger.info(ws.create_orders(excel_path)) - # logger.info(ws.transfer_3_to_2_to_1()) + excel_path = Path("unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\2025092701.xlsx") + logger.info(ws.create_orders(excel_path)) + logger.info(ws.transfer_3_to_2_to_1()) # logger.info(ws.transfer_1_to_2()) # logger.info(ws.scheduler_start()) diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 504cf459..519e6869 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -16,7 +16,7 @@ API_CONFIG = { "report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"), # HTTP 服务配置 - "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.210"), # HTTP服务监听地址,监听计算机飞连ip地址 + "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.33.174"), # HTTP服务监听地址,监听计算机飞连ip地址 "HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")), "debug_mode": False,# 调试模式 } diff --git a/unilabos/registry/devices/laiyu_liquid.yaml b/unilabos/registry/devices/laiyu_liquid.yaml index 98201a7d..64c0c182 100644 --- a/unilabos/registry/devices/laiyu_liquid.yaml +++ b/unilabos/registry/devices/laiyu_liquid.yaml @@ -1361,8 +1361,7 @@ laiyu_liquid: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -1492,11 +1491,9 @@ laiyu_liquid: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index b21ccd7e..99c92333 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -3994,8 +3994,7 @@ liquid_handler: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -4151,11 +4150,9 @@ liquid_handler: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -5015,8 +5012,7 @@ liquid_handler.biomek: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -5159,11 +5155,9 @@ liquid_handler.biomek: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -7807,8 +7801,7 @@ liquid_handler.prcxi: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: - - 0 + mix_times: 0 mix_vol: 0 none_keys: - '' @@ -7937,11 +7930,9 @@ liquid_handler.prcxi: mix_stage: type: string mix_times: - items: - maximum: 2147483647 - minimum: -2147483648 - type: integer - type: array + maximum: 2147483647 + minimum: -2147483648 + type: integer mix_vol: maximum: 2147483647 minimum: -2147483648