diff --git a/backend/app/api/pl/base_data.py b/backend/app/api/pl/base_data.py index 25ddd210e..d13ed5a2f 100644 --- a/backend/app/api/pl/base_data.py +++ b/backend/app/api/pl/base_data.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user +from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope from app.core.tenant_context import get_tenant_no from app.core.utils import generate_uuid from app.models.pl_models import ( @@ -77,6 +78,8 @@ def list_carriers( ) ) + query = apply_pl_scope(query, PlCarrier, user, db, tenant_no) + query = query.order_by( case((PlCarrier.order_num.is_(None), 1), else_=0).asc(), PlCarrier.create_time.desc(), @@ -116,7 +119,7 @@ def get_carrier( user: DbUserContext = Depends(get_db_current_user), ) -> CarrierRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - carrier = _get_carrier(db, tenant_no, id) + carrier = _get_carrier(db, tenant_no, id, user) return carrier @@ -128,7 +131,7 @@ def update_carrier( user: DbUserContext = Depends(get_db_current_user), ) -> CarrierRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - carrier = _get_carrier(db, tenant_no, id) + carrier = _get_carrier(db, tenant_no, id, user) for key, value in payload.model_dump(exclude_unset=True).items(): setattr(carrier, key, value) @@ -146,13 +149,13 @@ def delete_carrier( user: DbUserContext = Depends(get_db_current_user), ) -> None: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - carrier = _get_carrier(db, tenant_no, id) + carrier = _get_carrier(db, tenant_no, id, user) carrier.del_flag = 1 carrier.update_by = user.id db.commit() -def _get_carrier(db: Session, tenant_no: str, carrier_id: str) -> PlCarrier: +def _get_carrier(db: Session, tenant_no: str, carrier_id: str, user: DbUserContext) -> PlCarrier: carrier = ( db.query(PlCarrier) .filter( @@ -164,6 +167,7 @@ def _get_carrier(db: Session, tenant_no: str, carrier_id: str) -> PlCarrier: ) if not carrier: raise HTTPException(status_code=404, detail="承运商不存在") + assert_pl_row_in_scope(db, carrier, PlCarrier, user, tenant_no) return carrier @@ -229,6 +233,8 @@ def list_vehicles( if q: query = query.filter(PlVehicle.license_plate.contains(q)) + query = apply_pl_scope(query, PlVehicle, user, db, tenant_no) + query = query.order_by( case((PlVehicle.order_num.is_(None), 1), else_=0).asc(), PlVehicle.create_time.desc(), @@ -269,6 +275,7 @@ def create_vehicle( carrier_id=payload.carrier_id, carrier_name=payload.carrier_name, license_plate=payload.license_plate, + trailer_no=payload.trailer_no, load_capacity=payload.load_capacity, vehicle_status=payload.vehicle_status, remark=payload.remark, @@ -315,7 +322,7 @@ def get_vehicle( user: DbUserContext = Depends(get_db_current_user), ) -> VehicleDetailRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - vehicle = _get_vehicle(db, tenant_no, id) + vehicle = _get_vehicle(db, tenant_no, id, user) return _vehicle_to_detail(db, tenant_no, vehicle) @@ -327,7 +334,7 @@ def update_vehicle( user: DbUserContext = Depends(get_db_current_user), ) -> VehicleDetailRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - vehicle = _get_vehicle(db, tenant_no, id) + vehicle = _get_vehicle(db, tenant_no, id, user) personnel_list = payload.personnel_list cert_list = payload.cert_list @@ -379,13 +386,13 @@ def delete_vehicle( user: DbUserContext = Depends(get_db_current_user), ) -> None: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - vehicle = _get_vehicle(db, tenant_no, id) + vehicle = _get_vehicle(db, tenant_no, id, user) vehicle.del_flag = 1 vehicle.update_by = user.id db.commit() -def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str) -> PlVehicle: +def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str, user: DbUserContext) -> PlVehicle: vehicle = ( db.query(PlVehicle) .filter( @@ -397,6 +404,7 @@ def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str) -> PlVehicle: ) if not vehicle: raise HTTPException(status_code=404, detail="车辆不存在") + assert_pl_row_in_scope(db, vehicle, PlVehicle, user, tenant_no) return vehicle @@ -451,6 +459,8 @@ def list_personnel( ) ) + query = apply_pl_scope(query, PlPersonnel, user, db, tenant_no) + query = query.order_by( case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(), PlPersonnel.create_time.desc(), @@ -508,7 +518,7 @@ def get_personnel( user: DbUserContext = Depends(get_db_current_user), ) -> PersonnelDetailRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - personnel = _get_personnel(db, tenant_no, id) + personnel = _get_personnel(db, tenant_no, id, user) return _personnel_to_detail(db, tenant_no, personnel) @@ -520,7 +530,7 @@ def update_personnel( user: DbUserContext = Depends(get_db_current_user), ) -> PersonnelDetailRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - personnel = _get_personnel(db, tenant_no, id) + personnel = _get_personnel(db, tenant_no, id, user) cert_list = payload.cert_list data = payload.model_dump(exclude_unset=True, exclude={"cert_list"}) @@ -557,13 +567,13 @@ def delete_personnel( user: DbUserContext = Depends(get_db_current_user), ) -> None: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - personnel = _get_personnel(db, tenant_no, id) + personnel = _get_personnel(db, tenant_no, id, user) personnel.del_flag = 1 personnel.update_by = user.id db.commit() -def _get_personnel(db: Session, tenant_no: str, personnel_id: str) -> PlPersonnel: +def _get_personnel(db: Session, tenant_no: str, personnel_id: str, user: DbUserContext) -> PlPersonnel: personnel = ( db.query(PlPersonnel) .filter( @@ -575,6 +585,7 @@ def _get_personnel(db: Session, tenant_no: str, personnel_id: str) -> PlPersonne ) if not personnel: raise HTTPException(status_code=404, detail="人员不存在") + assert_pl_row_in_scope(db, personnel, PlPersonnel, user, tenant_no) return personnel @@ -621,6 +632,8 @@ def list_drivers( ) ) + query = apply_pl_scope(query, PlPersonnel, user, db, tenant_no) + query = query.order_by( case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(), PlPersonnel.create_time.desc(), @@ -660,7 +673,7 @@ def get_driver( user: DbUserContext = Depends(get_db_current_user), ) -> DriverRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - personnel = _get_personnel(db, tenant_no, id) + personnel = _get_personnel(db, tenant_no, id, user) return _personnel_to_driver_read(personnel) @@ -672,7 +685,7 @@ def update_driver( user: DbUserContext = Depends(get_db_current_user), ) -> DriverRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - personnel = _get_personnel(db, tenant_no, id) + personnel = _get_personnel(db, tenant_no, id, user) if payload.driver_name is not None: personnel.personnel_name = payload.driver_name @@ -696,7 +709,7 @@ def delete_driver( user: DbUserContext = Depends(get_db_current_user), ) -> None: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - personnel = _get_personnel(db, tenant_no, id) + personnel = _get_personnel(db, tenant_no, id, user) personnel.del_flag = 1 personnel.update_by = user.id db.commit() @@ -714,6 +727,8 @@ def bind_vehicle_driver( user: DbUserContext = Depends(get_db_current_user), ) -> dict: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) + _get_vehicle(db, tenant_no, payload.vehicle_id, user) + _get_personnel(db, tenant_no, payload.driver_id, user) if payload.is_default == 1: db.query(PlVehiclePersonnel).filter( @@ -764,5 +779,9 @@ def unbind_vehicle_driver( .first() ) if vd: + if isinstance(vd, PlVehiclePersonnel): + assert_pl_row_in_scope(db, vd, PlVehiclePersonnel, user, tenant_no) + else: + assert_pl_row_in_scope(db, vd, PlVehicleDriver, user, tenant_no) vd.del_flag = 1 db.commit() diff --git a/backend/app/api/pl/freight.py b/backend/app/api/pl/freight.py index f8ae75882..76b0d1667 100644 --- a/backend/app/api/pl/freight.py +++ b/backend/app/api/pl/freight.py @@ -47,6 +47,7 @@ def _generate_settlement_no(db: Session) -> str: @router.get("/unsettled-plans", response_model=UnsettledPlanListResponse) def list_unsettled_plans( q: Optional[str] = Query(default=None), + carrier_id: Optional[str] = Query(default=None, alias="carrierId"), plan_type: Optional[str] = Query(default=None, alias="planType"), license_plate: Optional[str] = Query(default=None, alias="licensePlate"), page: int = Query(default=1, ge=1), @@ -58,7 +59,9 @@ def list_unsettled_plans( total, items = freight_svc.list_unsettled_plans( db, tenant_no=tenant_no, + user=user, q=q, + carrier_id=carrier_id, plan_type=plan_type, license_plate=license_plate, page=page, @@ -70,8 +73,12 @@ def list_unsettled_plans( @router.get("/", response_model=FreightSettlementListResponse) def list_settlements( q: Optional[str] = Query(default=None), + carrier_id: Optional[str] = Query(default=None, alias="carrierId"), settlement_status: Optional[str] = Query(default=None, alias="settlementStatus"), - tab: Optional[str] = Query(default=None, description="bills|disburse|settled"), + tab: Optional[str] = Query( + default=None, + description="bills|approval_pending|approval_done|disburse|settled", + ), page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"), db: Session = Depends(get_db), @@ -82,7 +89,9 @@ def list_settlements( db, tenant_no=tenant_no, user_id=user.id, + user=user, q=q, + carrier_id=carrier_id, settlement_status=settlement_status, tab=tab, page=page, @@ -118,7 +127,7 @@ def get_settlement( ) -> FreightSettlementWithTransports: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) detail = freight_svc.get_settlement_detail( - db, tenant_no=tenant_no, settlement_id=id, user_id=user.id + db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user ) return FreightSettlementWithTransports.model_validate(detail) @@ -132,7 +141,12 @@ def update_settlement( ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) return freight_svc.update_settlement_draft( - db, tenant_no=tenant_no, settlement_id=id, payload=payload, user_id=user.id + db, + tenant_no=tenant_no, + settlement_id=id, + payload=payload, + user_id=user.id, + user=user, ) @@ -144,7 +158,7 @@ def delete_settlement( ) -> None: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) freight_svc.delete_draft_settlement( - db, tenant_no=tenant_no, settlement_id=id, user_id=user.id + db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user ) @@ -163,6 +177,7 @@ def submit_settlement_approval( body=payload, user_id=user.id, dept_id=user.dept_id, + user=user, ) @@ -194,7 +209,7 @@ def withdraw_settlement_approval( ) -> FreightSettlementRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) return freight_svc.withdraw_settlement_approval( - db, tenant_no=tenant_no, settlement_id=id, user_id=user.id + db, tenant_no=tenant_no, settlement_id=id, user_id=user.id, user=user ) @@ -205,7 +220,7 @@ def get_settlement_approval_flow( user: DbUserContext = Depends(get_db_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - freight_svc.get_settlement_row(db, tenant_no, id) + freight_svc.get_settlement_row(db, tenant_no, id, user=user) return freight_svc.get_approval_flow(db, id) @@ -224,6 +239,7 @@ def disburse_settlement( body=payload, user_id=user.id, dept_id=user.dept_id, + user=user, ) @@ -253,8 +269,8 @@ def list_vouchers( user: DbUserContext = Depends(get_db_current_user), ) -> list[RemittanceVoucherRead]: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - freight_svc.get_settlement_row(db, tenant_no, settlement_id) + freight_svc.get_settlement_row(db, tenant_no, settlement_id, user=user) detail = freight_svc.get_settlement_detail( - db, tenant_no=tenant_no, settlement_id=settlement_id, user_id=user.id + db, tenant_no=tenant_no, settlement_id=settlement_id, user_id=user.id, user=user ) return detail.get("vouchers") or [] diff --git a/backend/app/api/pl/inventory.py b/backend/app/api/pl/inventory.py index 596e5061a..6159f9b9e 100644 --- a/backend/app/api/pl/inventory.py +++ b/backend/app/api/pl/inventory.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user +from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope from app.core.tenant_context import get_tenant_no from app.core.utils import generate_uuid from app.models.pl_models import ( @@ -61,6 +62,8 @@ def get_inventory_ledger( if product_name: query = query.filter(PlInventory.product_name.contains(product_name)) + query = apply_pl_scope(query, PlInventory, user, db, tenant_no) + total = query.count() offset = (page - 1) * page_size items = query.order_by(PlInventory.product_name).offset(offset).limit(page_size).all() @@ -92,6 +95,7 @@ def set_warning_threshold( ) if inventory: + assert_pl_row_in_scope(db, inventory, PlInventory, user, tenant_no) inventory.low_stock_warning = payload.low_stock_warning inventory.high_stock_warning = payload.high_stock_warning else: @@ -136,6 +140,7 @@ def list_warnings( if status: query = query.filter(PlInventoryWarning.status == status) + query = apply_pl_scope(query, PlInventoryWarning, user, db, tenant_no) return query.order_by(PlInventoryWarning.create_time.desc()).all() @@ -160,6 +165,7 @@ def update_warning( if not warning: raise HTTPException(status_code=404, detail="预警不存在") + assert_pl_row_in_scope(db, warning, PlInventoryWarning, user, tenant_no) if payload.status == "resolved": warning.status = "resolved" @@ -207,6 +213,8 @@ def list_transactions( if date_to: query = query.filter(PlInventoryTransaction.create_time <= date_to) + query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no) + total = query.count() offset = (page - 1) * page_size items = query.order_by(PlInventoryTransaction.create_time.desc()).offset(offset).limit(page_size).all() @@ -236,6 +244,7 @@ def get_summary( PlInventoryTransaction.create_time >= date_from, PlInventoryTransaction.create_time <= date_to, ) + query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no) if product_id: query = query.filter(PlInventoryTransaction.product_id == product_id) @@ -295,6 +304,7 @@ def list_document_reminders( if status: query = query.filter(PlDocumentReminder.status == status) + query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no) return query.order_by(PlDocumentReminder.expire_date.asc()).all() @@ -319,6 +329,7 @@ def update_document_reminder( if not reminder: raise HTTPException(status_code=404, detail="提醒不存在") + assert_pl_row_in_scope(db, reminder, PlDocumentReminder, user, tenant_no) reminder.status = payload.status if payload.remark: diff --git a/backend/app/api/pl/plan.py b/backend/app/api/pl/plan.py index aedf23d76..f8cb8a4a5 100644 --- a/backend/app/api/pl/plan.py +++ b/backend/app/api/pl/plan.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import Session from app.core.config import settings from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user +from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope from app.core.tenant_context import get_tenant_no from app.core.utils import generate_uuid from app.models.crm_models import CrmContract, CrmContractProduct, CrmProduct @@ -171,6 +172,8 @@ def list_plans( if plan_date_to: query = query.filter(PlPlan.plan_date <= plan_date_to) + query = apply_pl_scope(query, PlPlan, user, db, tenant_no) + # 统计总数 total = query.count() @@ -234,9 +237,8 @@ def count_plans_by_contract( PlPlan.del_flag == 0, PlPlan.contract_id.in_(ids), ) - .group_by(PlPlan.contract_id) - .all() ) + rows = apply_pl_scope(rows, PlPlan, user, db, tenant_no).group_by(PlPlan.contract_id).all() return {"counts": {contract_id: count for contract_id, count in rows}} @@ -544,7 +546,7 @@ def get_plan( user: DbUserContext = Depends(get_db_current_user), ) -> PlanWithProducts: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - plan = _get_plan(db, tenant_no, id) + plan = _get_plan(db, tenant_no, id, user) products = ( db.query(PlPlanProduct) @@ -588,7 +590,7 @@ def update_plan( user: DbUserContext = Depends(get_db_current_user), ) -> PlanRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - plan = _get_plan(db, tenant_no, id) + plan = _get_plan(db, tenant_no, id, user) # 更新主表字段 update_data = payload.model_dump(exclude_unset=True, exclude={"products", "transport"}) @@ -698,6 +700,7 @@ def patch_plan_product_certs( ) -> PlanWithProducts: """更新计划危化品产品购买证/运输证(不触发安排运输)""" tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) + _get_plan(db, tenant_no, id, user) update_plan_product_certs( db, tenant_no=tenant_no, @@ -705,7 +708,7 @@ def patch_plan_product_certs( items=payload.products, user_id=user.id, ) - plan = _get_plan(db, tenant_no, id) + plan = _get_plan(db, tenant_no, id, user) products = ( db.query(PlPlanProduct) .filter( @@ -733,7 +736,7 @@ def get_plan_arrange_info( ) -> PlanArrangeInfoRead: """获取计划已安排运输后的司机链接与地磅出入库关联(供查看弹窗复制简述/二维码)。""" tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - plan = _get_plan(db, tenant_no, id) + plan = _get_plan(db, tenant_no, id, user) empty = PlanArrangeInfoRead(driver_url=None, license_plate=None, site_records=[], inventory_records=[]) if plan.follow_status == "cancelled" or plan.transport_status != "arranged" or not plan.transport_id: @@ -847,7 +850,7 @@ def cancel_plan( ) -> PlanRead: """作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。""" tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - plan = _get_plan(db, tenant_no, id) + plan = _get_plan(db, tenant_no, id, user) if plan.follow_status == "cancelled": raise HTTPException(status_code=400, detail="计划已作废") @@ -905,7 +908,7 @@ def archive_plan( user: DbUserContext = Depends(get_db_current_user), ) -> PlanWithProducts: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - plan = _get_plan(db, tenant_no, id) + plan = _get_plan(db, tenant_no, id, user) if plan.follow_status == "cancelled": raise HTTPException(status_code=400, detail="已作废的计划不能归档") @@ -965,7 +968,7 @@ def delete_plan( user: DbUserContext = Depends(get_db_current_user), ) -> None: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - plan = _get_plan(db, tenant_no, id) + plan = _get_plan(db, tenant_no, id, user) plan.del_flag = 1 plan.update_by = user.id @@ -990,7 +993,7 @@ def delete_plan( # ============================================================================= -def _get_plan(db: Session, tenant_no: str, plan_id: str) -> PlPlan: +def _get_plan(db: Session, tenant_no: str, plan_id: str, user: DbUserContext) -> PlPlan: plan = ( db.query(PlPlan) .filter( @@ -1002,4 +1005,5 @@ def _get_plan(db: Session, tenant_no: str, plan_id: str) -> PlPlan: ) if not plan: raise HTTPException(status_code=404, detail="计划不存在") + assert_pl_row_in_scope(db, plan, PlPlan, user, tenant_no) return plan diff --git a/backend/app/api/pl/transport.py b/backend/app/api/pl/transport.py index 823e3e530..3d5792919 100644 --- a/backend/app/api/pl/transport.py +++ b/backend/app/api/pl/transport.py @@ -11,6 +11,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user +from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope from app.core.tenant_context import get_tenant_no from app.core.utils import generate_uuid from app.core.config import settings @@ -334,6 +335,8 @@ def list_transports( if driver_id: query = query.filter(PlTransport.driver_id == driver_id) + query = apply_pl_scope(query, PlTransport, user, db, tenant_no) + total = query.count() offset = (page - 1) * page_size transports = query.order_by(PlTransport.create_time.desc()).offset(offset).limit(page_size).all() @@ -401,6 +404,7 @@ def create_transport( # 更新计划关联的运输信息 plan = db.query(PlPlan).filter(PlPlan.id == pid, PlPlan.del_flag == 0).first() if plan: + assert_pl_row_in_scope(db, plan, PlPlan, user, tenant_no) plan.transport_id = transport.id plan.transport_no = transport_no plan.vehicle_id = transport.vehicle_id @@ -435,6 +439,7 @@ def arrange_transport( ).first() if not plan: raise HTTPException(status_code=404, detail="计划不存在") + assert_pl_row_in_scope(db, plan, PlPlan, user, tenant_no) # 获取车辆信息 vehicle_info = _get_vehicle_info(db, payload.vehicle_id, tenant_no) @@ -655,7 +660,7 @@ def get_transport( user: DbUserContext = Depends(get_db_current_user), ) -> TransportRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - transport = _get_transport(db, tenant_no, id) + transport = _get_transport(db, tenant_no, id, user) return transport @@ -681,7 +686,7 @@ def generate_qrcode( user: DbUserContext = Depends(get_db_current_user), ) -> QrCodeResponse: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - transport = _get_transport(db, tenant_no, id) + transport = _get_transport(db, tenant_no, id, user) from datetime import timedelta expire_date = date.today() + timedelta(days=expire_days) @@ -755,7 +760,7 @@ def submit_approval( user: DbUserContext = Depends(get_db_current_user), ) -> TransportRead: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - transport = _get_transport(db, tenant_no, id) + transport = _get_transport(db, tenant_no, id, user) if transport.approval_status not in ("none", "rejected"): raise HTTPException(status_code=400, detail="当前状态不允许提交审批") @@ -880,7 +885,7 @@ def get_transport_approvals( # ============================================================================= -def _get_transport(db: Session, tenant_no: str, transport_id: str) -> PlTransport: +def _get_transport(db: Session, tenant_no: str, transport_id: str, user: DbUserContext) -> PlTransport: transport = ( db.query(PlTransport) .filter( @@ -892,4 +897,5 @@ def _get_transport(db: Session, tenant_no: str, transport_id: str) -> PlTranspor ) if not transport: raise HTTPException(status_code=404, detail="运输不存在") + assert_pl_row_in_scope(db, transport, PlTransport, user, tenant_no) return transport diff --git a/backend/app/services/pl_data_scope.py b/backend/app/services/pl_data_scope.py new file mode 100644 index 000000000..d65277c91 --- /dev/null +++ b/backend/app/services/pl_data_scope.py @@ -0,0 +1,27 @@ +"""PL 模块数据权限辅助(create_by / create_dept_id,无 owner_field)。""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sqlalchemy.orm import Session + +from app.core.data_scope import apply_data_scope, assert_row_in_data_scope + +if TYPE_CHECKING: + from app.core.db_auth import DbUserContext + + +def apply_pl_scope(query, model, user: "DbUserContext", db: Session, tenant_no: str): + """列表查询应用数据权限。""" + return apply_data_scope(query, model, user.to_user_info(), db, tenant_no) + + +def assert_pl_row_in_scope( + db: Session, + row, + model, + user: "DbUserContext", + tenant_no: str, +) -> None: + """详情/改/删前校验行级数据权限。""" + assert_row_in_data_scope(db, row, model, user.to_user_info(), tenant_no) diff --git a/backend/app/services/pl_freight_settlement_service.py b/backend/app/services/pl_freight_settlement_service.py index 5f98481a0..7c92d2b31 100644 --- a/backend/app/services/pl_freight_settlement_service.py +++ b/backend/app/services/pl_freight_settlement_service.py @@ -12,6 +12,7 @@ from sqlalchemy.orm import Session from app.core.utils import generate_uuid from app.models.models import SysUser +from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope from app.models.pl_models import ( PlFreightSettlement, PlFreightSettlementApproval, @@ -29,7 +30,9 @@ from app.schemas.pl.freight import ( _FMT = "%Y-%m-%d %H:%M:%S" TAB_STATUS_MAP = { - "bills": ("pending", "approving"), + "bills": ("pending", "approving", "approved", "confirmed"), + "approval_pending": ("approving",), + "approval_done": ("approved", "confirmed"), "disburse": ("approved",), "settled": ("confirmed",), } @@ -61,7 +64,14 @@ def _occupied_transport_ids_subquery(db: Session, tenant_no: str): ) -def get_settlement_row(db: Session, tenant_no: str, settlement_id: str) -> PlFreightSettlement: +def get_settlement_row( + db: Session, + tenant_no: str, + settlement_id: str, + *, + user=None, + skip_scope: bool = False, +) -> PlFreightSettlement: row = ( db.query(PlFreightSettlement) .filter( @@ -73,6 +83,8 @@ def get_settlement_row(db: Session, tenant_no: str, settlement_id: str) -> PlFre ) if not row: raise HTTPException(status_code=404, detail="结算单不存在") + if user and not skip_scope: + assert_pl_row_in_scope(db, row, PlFreightSettlement, user, tenant_no) return row @@ -80,7 +92,9 @@ def list_unsettled_plans( db: Session, *, tenant_no: str, + user=None, q: Optional[str] = None, + carrier_id: Optional[str] = None, plan_type: Optional[str] = None, license_plate: Optional[str] = None, page: int = 1, @@ -112,11 +126,16 @@ def list_unsettled_plans( PlPlan.supplier_name.contains(q), ) ) + if carrier_id: + query = query.filter(PlTransport.carrier_id == carrier_id) if plan_type: query = query.filter(PlPlan.plan_type == plan_type) if license_plate: query = query.filter(PlPlan.license_plate.contains(license_plate)) + if user: + query = apply_pl_scope(query, PlPlan, user, db, tenant_no) + total = query.count() rows = ( query.order_by(PlPlan.update_time.desc()) @@ -238,8 +257,10 @@ def create_settlement( return settlement -def delete_draft_settlement(db: Session, *, tenant_no: str, settlement_id: str, user_id: str) -> None: - settlement = get_settlement_row(db, tenant_no, settlement_id) +def delete_draft_settlement( + db: Session, *, tenant_no: str, settlement_id: str, user_id: str, user=None +) -> None: + settlement = get_settlement_row(db, tenant_no, settlement_id, user=user) if settlement.settlement_status != "pending": raise HTTPException(status_code=400, detail="仅草稿结算单可删除") @@ -363,6 +384,84 @@ def create_settlement_approval_records( db.flush() +def _clear_settlement_approval_records(db: Session, settlement_id: str) -> None: + """软删除结算单历史审批记录(重新提交审批前清理)""" + now = datetime.now() + rows = ( + db.query(PlFreightSettlementApproval) + .filter( + PlFreightSettlementApproval.settlement_id == settlement_id, + PlFreightSettlementApproval.del_flag == 0, + ) + .all() + ) + for row in rows: + row.del_flag = 1 + row.update_time = now + if rows: + db.flush() + + +def _sync_settlement_status_if_approval_complete( + db: Session, settlement: PlFreightSettlement +) -> bool: + """审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据)""" + if settlement.settlement_status != "approving": + return False + if not _settlement_all_approved(db, settlement.id): + return False + settlement.settlement_status = "approved" + settlement.update_time = datetime.now() + return True + + +def _sync_settlement_status_if_disbursed( + db: Session, settlement: PlFreightSettlement +) -> bool: + """已有汇款凭证但结算单仍停留在 approved 时自动纠正(兼容历史脏数据)""" + if settlement.settlement_status != "approved": + return False + voucher = ( + db.query(PlRemittanceVoucher) + .filter( + PlRemittanceVoucher.settlement_id == settlement.id, + PlRemittanceVoucher.del_flag == 0, + ) + .first() + ) + if not voucher: + return False + settlement.settlement_status = "confirmed" + settlement.update_time = datetime.now() + return True + + +def _sync_disbursed_settlements(db: Session, tenant_no: str) -> None: + """批量纠正已有汇款凭证但仍为 approved 的结算单""" + rows = ( + db.query(PlFreightSettlement) + .filter( + PlFreightSettlement.tenant_no == tenant_no, + PlFreightSettlement.del_flag == 0, + PlFreightSettlement.settlement_status == "approved", + db.query(PlRemittanceVoucher.id) + .filter( + PlRemittanceVoucher.settlement_id == PlFreightSettlement.id, + PlRemittanceVoucher.del_flag == 0, + ) + .exists(), + ) + .all() + ) + if not rows: + return + now = datetime.now() + for row in rows: + row.settlement_status = "confirmed" + row.update_time = now + db.commit() + + def can_user_approve_settlement(db: Session, settlement_id: str, user_id: str) -> bool: if not settlement_id or not user_id: return False @@ -413,6 +512,20 @@ def _advance_after_approval( if result == "rejected": settlement.settlement_status = "pending" settlement.update_time = now + others = ( + db.query(PlFreightSettlementApproval) + .filter( + PlFreightSettlementApproval.settlement_id == settlement.id, + PlFreightSettlementApproval.id != current.id, + PlFreightSettlementApproval.approval_result == "pending", + PlFreightSettlementApproval.del_flag == 0, + ) + .all() + ) + for row in others: + row.approval_result = "skipped" + row.is_current = 0 + row.update_time = now return approval_type = current.approval_type or "sequential" @@ -477,8 +590,9 @@ def submit_settlement_approval( body: FreightSettlementSubmitApprovalBody, user_id: str, dept_id: str, + user=None, ) -> PlFreightSettlement: - settlement = get_settlement_row(db, tenant_no, settlement_id) + settlement = get_settlement_row(db, tenant_no, settlement_id, user=user) if settlement.settlement_status != "pending": raise HTTPException(status_code=400, detail="仅草稿状态可提交审批") @@ -488,6 +602,8 @@ def submit_settlement_approval( settlement.update_by = user_id db.flush() + _clear_settlement_approval_records(db, settlement_id) + create_settlement_approval_records( db, tenant_no=tenant_no, @@ -523,7 +639,7 @@ def process_settlement_approval( if not approval: raise HTTPException(status_code=404, detail="审批记录不存在") - settlement = get_settlement_row(db, tenant_no, approval.settlement_id) + settlement = get_settlement_row(db, tenant_no, approval.settlement_id, skip_scope=True) if settlement.settlement_status != "approving": raise HTTPException(status_code=400, detail="结算单不在审批中") @@ -541,6 +657,8 @@ def process_settlement_approval( approval.update_by = user_id approval.update_time = now + # SessionLocal 关闭了 autoflush,须先 flush 再查询 pending 计数,否则当前审批仍会被算作 pending + db.flush() _advance_after_approval(db, settlement, approval, result) settlement.update_by = user_id db.commit() @@ -549,9 +667,9 @@ def process_settlement_approval( def withdraw_settlement_approval( - db: Session, *, tenant_no: str, settlement_id: str, user_id: str + db: Session, *, tenant_no: str, settlement_id: str, user_id: str, user=None ) -> PlFreightSettlement: - settlement = get_settlement_row(db, tenant_no, settlement_id) + settlement = get_settlement_row(db, tenant_no, settlement_id, user=user) if settlement.settlement_status != "approving": raise HTTPException(status_code=400, detail="仅审批中可撤回") if settlement.create_by != user_id: @@ -586,8 +704,9 @@ def disburse_settlement( body: FreightSettlementDisburseBody, user_id: str, dept_id: str, + user=None, ) -> PlFreightSettlement: - settlement = get_settlement_row(db, tenant_no, settlement_id) + settlement = get_settlement_row(db, tenant_no, settlement_id, user=user) if settlement.settlement_status != "approved": raise HTTPException(status_code=400, detail="仅待发放状态可登记") @@ -710,6 +829,7 @@ def enrich_settlement_list_item( "invoiceAmount": settlement.invoice_amount, "invoiceNo": settlement.invoice_no, "invoiceDate": settlement.invoice_date, + "invoiceAttachmentUrls": settlement.invoice_attachment_urls, "settlementStatus": settlement.settlement_status, "remark": settlement.remark, "tenantNo": settlement.tenant_no, @@ -739,25 +859,49 @@ def list_settlements_enriched( *, tenant_no: str, user_id: str, + user=None, q: Optional[str] = None, + carrier_id: Optional[str] = None, settlement_status: Optional[str] = None, tab: Optional[str] = None, page: int = 1, page_size: int = 20, ) -> Tuple[int, List[Dict[str, Any]]]: + _sync_disbursed_settlements(db, tenant_no) query = db.query(PlFreightSettlement).filter( PlFreightSettlement.tenant_no == tenant_no, PlFreightSettlement.del_flag == 0, ) if tab and tab in TAB_STATUS_MAP: - query = query.filter(PlFreightSettlement.settlement_status.in_(TAB_STATUS_MAP[tab])) + tab_statuses = TAB_STATUS_MAP[tab] + if settlement_status and settlement_status in tab_statuses: + query = query.filter(PlFreightSettlement.settlement_status == settlement_status) + else: + query = query.filter(PlFreightSettlement.settlement_status.in_(tab_statuses)) elif settlement_status: query = query.filter(PlFreightSettlement.settlement_status == settlement_status) if q: query = query.filter(PlFreightSettlement.settlement_no.contains(q)) + if carrier_id: + carrier_settlement_ids = ( + db.query(PlFreightSettlementTransport.settlement_id) + .join(PlTransport, PlTransport.id == PlFreightSettlementTransport.transport_id) + .filter( + PlFreightSettlementTransport.tenant_no == tenant_no, + PlFreightSettlementTransport.del_flag == 0, + PlTransport.del_flag == 0, + PlTransport.carrier_id == carrier_id, + ) + .distinct() + ) + query = query.filter(PlFreightSettlement.id.in_(carrier_settlement_ids)) + + if user and tab != "approval_pending": + query = apply_pl_scope(query, PlFreightSettlement, user, db, tenant_no) + total = query.count() rows = ( query.order_by(PlFreightSettlement.create_time.desc()) @@ -765,13 +909,34 @@ def list_settlements_enriched( .limit(page_size) .all() ) + status_fixed = False + for row in rows: + if _sync_settlement_status_if_approval_complete(db, row): + status_fixed = True + if _sync_settlement_status_if_disbursed(db, row): + status_fixed = True + if status_fixed: + db.commit() + for row in rows: + db.refresh(row) + + if tab == "bills": + bills_statuses = set(TAB_STATUS_MAP["bills"]) + rows = [r for r in rows if r.settlement_status in bills_statuses] + return total, [enrich_settlement_list_item(db, r, user_id) for r in rows] def get_settlement_detail( - db: Session, *, tenant_no: str, settlement_id: str, user_id: str + db: Session, *, tenant_no: str, settlement_id: str, user_id: str, user=None ) -> Dict[str, Any]: - settlement = get_settlement_row(db, tenant_no, settlement_id) + settlement = get_settlement_row(db, tenant_no, settlement_id, user=user) + if _sync_settlement_status_if_approval_complete(db, settlement): + db.commit() + db.refresh(settlement) + if _sync_settlement_status_if_disbursed(db, settlement): + db.commit() + db.refresh(settlement) base = enrich_settlement_list_item(db, settlement, user_id) transport_ids = _get_transport_ids(db, settlement_id) @@ -812,9 +977,12 @@ def get_settlement_detail( "transportNo": t.transport_no, "licensePlate": t.license_plate, "driverName": t.driver_name, + "carrierName": t.carrier_name, + "transportMedium": t.transport_medium, + "startPoint": t.start_point, + "endPoint": t.end_point, "freightAmount": t.freight_amount, "transportStatus": t.transport_status, - "carrierName": t.carrier_name, } ) @@ -847,12 +1015,19 @@ def update_settlement_draft( settlement_id: str, payload: FreightSettlementUpdate, user_id: str, + user=None, ) -> PlFreightSettlement: - settlement = get_settlement_row(db, tenant_no, settlement_id) - if settlement.settlement_status != "pending": - raise HTTPException(status_code=400, detail="仅草稿可编辑") + settlement = get_settlement_row(db, tenant_no, settlement_id, user=user) + status = settlement.settlement_status + if status not in ("pending", "approved"): + raise HTTPException(status_code=400, detail="当前状态不可编辑") update_data = payload.model_dump(exclude_unset=True, by_alias=False) + if status == "approved": + allowed = {"invoice_amount", "invoice_no", "invoice_date", "invoice_attachment_urls", "remark"} + update_data = {k: v for k, v in update_data.items() if k in allowed} + if not update_data: + raise HTTPException(status_code=400, detail="无有效更新字段") for key, value in update_data.items(): setattr(settlement, key, value) settlement.update_by = user_id diff --git a/backend/scripts/apply_pl_data_scope.py b/backend/scripts/apply_pl_data_scope.py new file mode 100644 index 000000000..85e580270 --- /dev/null +++ b/backend/scripts/apply_pl_data_scope.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""为 PL API 批量接入数据权限(PR4 Phase 3)。""" +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +IMPORT_LINE = "from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope\n" + + +def ensure_import(content: str) -> str: + if "from app.services.pl_data_scope import" in content: + return content + marker = "from app.core.db_auth import DbUserContext, get_db_current_user\n" + if marker in content: + return content.replace(marker, marker + IMPORT_LINE) + return content + + +def patch_get_helper(content: str, fn_name: str, model_name: str) -> str: + """_get_xxx(db, tenant_no, id) -> _get_xxx(db, tenant_no, id, user) + assert.""" + old_sig = f"def {fn_name}(db: Session, tenant_no: str, " + if f"def {fn_name}(db: Session, tenant_no: str, " not in content: + return content + if f"def {fn_name}(db: Session, tenant_no: str, " in content and "user: DbUserContext" in content.split(f"def {fn_name}")[1].split("\n\n")[0]: + return content + + pattern = rf"(def {fn_name}\(\n db: Session,\n tenant_no: str,\n \w+_id: str,\n\) -> \w+:\n)( \w+ = \([\s\S]*?\n if not \w+:\n raise HTTPException\(status_code=404[^\n]+\n return \w+\n)" + repl = rf"\1\2".replace("\\2", "") # noqa - use explicit replacement below + + def replacer(m): + body = m.group(0) + if "assert_pl_row_in_scope" in body: + return body + var = re.search(r"if not (\w+):", body).group(1) + return ( + body.replace( + f"def {fn_name}(db: Session, tenant_no: str, ", + f"def {fn_name}(db: Session, tenant_no: str, ", + ) + .replace( + f") -> ", + f",\n user: DbUserContext,\n) -> ", + 1, + ) + if ",\n user: DbUserContext," not in body + else body + ) + + # simpler manual replace per function + old = f"""def {fn_name}(db: Session, tenant_no: str, """ + # find entity id param name + m = re.search(rf"def {fn_name}\(db: Session, tenant_no: str, (\w+): str\)", content) + if not m: + return content + id_param = m.group(1) + old_block = f"""def {fn_name}(db: Session, tenant_no: str, {id_param}: str) ->""" + if f"user: DbUserContext" in content[content.find(old_block) : content.find(old_block) + 200]: + return content + new_block = f"""def {fn_name}(db: Session, tenant_no: str, {id_param}: str, user: DbUserContext) ->""" + content = content.replace(old_block, new_block, 1) + # add assert before return + needle = f"if not {id_param.split('_')[0] if False else ''}" + # find variable name from query + fn_body_start = content.find(new_block) + fn_chunk = content[fn_body_start : fn_body_start + 600] + var_m = re.search(r"(\w+) = \(\n db\.query", fn_chunk) + if not var_m: + var_m = re.search(r"(\w+) = \(\n db\.query", content[fn_body_start : fn_body_start + 800]) + var = var_m.group(1) if var_m else id_param.replace("_id", "").replace("carrier", "carrier") + if var_m: + var = var_m.group(1) + else: + # guess from id_param + var = {"carrier_id": "carrier", "vehicle_id": "vehicle", "personnel_id": "personnel", "plan_id": "plan", "transport_id": "transport"}.get(id_param, "row") + + insert = f""" assert_pl_row_in_scope(db, {var}, {model_name}, user, tenant_no) + return {var} +""" + old_return = f" return {var}\n" + if f"assert_pl_row_in_scope(db, {var}," not in content: + content = content.replace(old_return, insert, 1) + # fix _get calls: _get_xxx(db, tenant_no, id) -> _get_xxx(db, tenant_no, id, user) + content = re.sub( + rf"{fn_name}\(db, tenant_no, ([^,\)]+)\)", + rf"{fn_name}(db, tenant_no, \1, user)", + content, + ) + return content + + +def add_scope_before_order(content: str, model: str, anchor: str) -> str: + """在 order_by 前插入 apply_pl_scope。""" + if f"apply_pl_scope(query, {model}," in content: + return content + idx = content.find(anchor) + if idx == -1: + return content + insert = f" query = apply_pl_scope(query, {model}, user, db, tenant_no)\n\n " + # insert before anchor's line + line_start = content.rfind("\n", 0, idx) + 1 + return content[:line_start] + insert + content[line_start:] + + +def patch_base_data(): + path = ROOT / "app/api/pl/base_data.py" + c = ensure_import(path.read_text()) + c = add_scope_before_order(c, "PlCarrier", " query = query.order_by(\n case((PlCarrier.order_num") + c = add_scope_before_order(c, "PlVehicle", " vehicles = query.order_by(") + if "vehicles = query.order_by(" not in c: + c = add_scope_before_order(c, "PlVehicle", " query = query.order_by(") + c = add_scope_before_order(c, "PlPersonnel", " query = query.order_by(\n case((PlPersonnel.order_num") + # driver list - second PlPersonnel order_by + if c.count("apply_pl_scope(query, PlPersonnel,") < 2: + c = c.replace( + " query = query.order_by(\n case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),\n PlPersonnel.create_time.desc(),\n )\n return [_personnel_to_driver_read(p) for p in query.all()]", + " query = apply_pl_scope(query, PlPersonnel, user, db, tenant_no)\n\n query = query.order_by(\n case((PlPersonnel.order_num.is_(None), 1), else_=0).asc(),\n PlPersonnel.create_time.desc(),\n )\n return [_personnel_to_driver_read(p) for p in query.all()]", + 1, + ) + for fn, model in ( + ("_get_carrier", "PlCarrier"), + ("_get_vehicle", "PlVehicle"), + ("_get_personnel", "PlPersonnel"), + ): + c = patch_get_helper(c, fn, model) + path.write_text(c) + print("patched base_data.py") + + +def patch_plan(): + path = ROOT / "app/api/pl/plan.py" + c = ensure_import(path.read_text()) + if "apply_pl_scope(query, PlPlan," not in c: + c = c.replace( + " total = query.count()\n offset = (page - 1) * page_size\n plans = query.order_by(PlPlan.create_time.desc())", + " query = apply_pl_scope(query, PlPlan, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n plans = query.order_by(PlPlan.create_time.desc())", + 1, + ) + c = patch_get_helper(c, "_get_plan", "PlPlan") + path.write_text(c) + print("patched plan.py") + + +def patch_transport(): + path = ROOT / "app/api/pl/transport.py" + c = ensure_import(path.read_text()) + if "apply_pl_scope(query, PlTransport," not in c: + c = c.replace( + " total = query.count()\n offset = (page - 1) * page_size\n transports = query.order_by(PlTransport.create_time.desc())", + " query = apply_pl_scope(query, PlTransport, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n transports = query.order_by(PlTransport.create_time.desc())", + 1, + ) + c = patch_get_helper(c, "_get_transport", "PlTransport") + path.write_text(c) + print("patched transport.py") + + +def patch_inventory(): + path = ROOT / "app/api/pl/inventory.py" + c = ensure_import(path.read_text()) + replacements = [ + ( + " total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventory.product_name)", + " query = apply_pl_scope(query, PlInventory, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventory.product_name)", + ), + ( + " return query.order_by(PlInventoryWarning.create_time.desc()).all()", + " query = apply_pl_scope(query, PlInventoryWarning, user, db, tenant_no)\n return query.order_by(PlInventoryWarning.create_time.desc()).all()", + ), + ( + " total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventoryTransaction.create_time.desc())", + " query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)\n\n total = query.count()\n offset = (page - 1) * page_size\n items = query.order_by(PlInventoryTransaction.create_time.desc())", + ), + ( + " query = db.query(PlInventoryTransaction).filter(\n PlInventoryTransaction.tenant_no == tenant_no,\n PlInventoryTransaction.del_flag == 0,\n PlInventoryTransaction.create_time >= date_from,\n PlInventoryTransaction.create_time <= date_to,\n )\n\n if product_id:", + " query = db.query(PlInventoryTransaction).filter(\n PlInventoryTransaction.tenant_no == tenant_no,\n PlInventoryTransaction.del_flag == 0,\n PlInventoryTransaction.create_time >= date_from,\n PlInventoryTransaction.create_time <= date_to,\n )\n query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)\n\n if product_id:", + ), + ( + " return query.order_by(PlDocumentReminder.create_time.desc()).all()", + " query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no)\n return query.order_by(PlDocumentReminder.create_time.desc()).all()", + ), + ] + for old, new in replacements: + if old in c and new not in c: + c = c.replace(old, new, 1) + # warning update assert + if "assert_pl_row_in_scope(db, warning," not in c: + c = c.replace( + " if not warning:\n raise HTTPException(status_code=404, detail=\"预警不存在\")\n\n if payload.status == \"resolved\":", + " if not warning:\n raise HTTPException(status_code=404, detail=\"预警不存在\")\n assert_pl_row_in_scope(db, warning, PlInventoryWarning, user, tenant_no)\n\n if payload.status == \"resolved\":", + 1, + ) + if "assert_pl_row_in_scope(db, reminder," not in c: + c = c.replace( + " if not reminder:\n raise HTTPException(status_code=404, detail=\"提醒不存在\")", + " if not reminder:\n raise HTTPException(status_code=404, detail=\"提醒不存在\")\n assert_pl_row_in_scope(db, reminder, PlDocumentReminder, user, tenant_no)", + 1, + ) + if "assert_pl_row_in_scope(db, inventory," not in c: + c = c.replace( + " if inventory:\n inventory.low_stock_warning = payload.low_stock_warning", + " if inventory:\n assert_pl_row_in_scope(db, inventory, PlInventory, user, tenant_no)\n inventory.low_stock_warning = payload.low_stock_warning", + 1, + ) + path.write_text(c) + print("patched inventory.py") + + +if __name__ == "__main__": + patch_base_data() + patch_plan() + patch_transport() + patch_inventory()