PayrollManagementSystem/backend/app/services/authorization_metadata.py
liuwenxin 1e53792ad6 feat(T+): 完成数据贯通与自动接入闭环
打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。
新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。
补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
2026-11-05 18:47:13 +08:00

149 lines
8.7 KiB
Python

"""业务表单参照只返回必要字段,不借下拉权限开放整个资料接口。"""
from sqlalchemy import func, select
from app.core.exceptions import BadRequestError, ForbiddenError
from app.models import (
Business,
BusinessRole,
CalcTemplateDefinition,
Customer,
CustomerRatingGrade,
Personnel,
Product,
Shipment,
)
from app.services.authorization_service import calculate
OPTIONS = {
"managers": (Personnel, ("id", "personnel_name", "business_role_id", "status"), (
"base:personnel:view", "base:personnel:create", "base:personnel:update",
)),
"rating_grades": (CustomerRatingGrade, ("id", "grade_name", "min_score", "max_score", "sort_order", "remark"), (
"base:customers:view", "base:customers:rating", "business:ratings:view",
)),
"personnel": (Personnel, ("id", "personnel_name", "business_role_id", "status"), (
"base:personnel:view", "base:customers:view", "base:customers:create", "base:customers:update",
"base:customers:ownership", "business:businesses:view", "business:businesses:create",
"business:businesses:update", "business:businesses:ownership", "payroll:prepare", "adjustments:create",
"payroll:view", "reports:personal:view", "reports:department:view", "reports:business:view", "reports:payments:view",
)),
"customers": (Customer, ("id", "company_name", "customer_type"), (
"base:customers:view", "business:businesses:view", "business:businesses:create", "business:businesses:update",
"reports:business:view",
)),
"products": (Product, ("id", "product_name", "spec_model", "unit"), (
"base:products:view", "business:businesses:create", "business:businesses:update",
"business:shipments:create", "business:shipments:update",
)),
"businesses": (Business, ("id", "business_no", "status"), (
"business:businesses:view", "business:shipments:view", "business:shipments:create",
"business:shipments:update", "business:receipts:view", "business:receipts:create", "business:receipts:update",
)),
"shipments": (Shipment, ("id", "shipment_no", "business_id", "ship_date"), (
"business:shipments:view", "business:receipts:view", "business:receipts:create", "business:receipts:update",
)),
"business_roles": (BusinessRole, ("id", "role_code", "role_name", "status"), (
"base:personnel:view", "base:personnel:create", "base:personnel:update", "system:business_roles:view",
"system:params:view", "reports:personal:view", "reports:department:view", "reports:business:view", "reports:payments:view",
)),
}
def payroll_options(session, actor, business_id, *, page_num=1, page_size=200):
"""核算资料所需的人员及发货明细,不开放人员薪酬或联系方式。"""
from app.core.exceptions import NotFoundError
from app.models import ShipmentItem, TPlusBusinessDocument, TPlusSourceLink, TPlusSyncRecord
from app.services.authorization_service import require_permission
from app.services.tplus.guidance import latest_guidance_facts, resolve_guidance
if page_num < 1 or not 1 <= page_size <= 200:
raise ValueError("无效的核算参照分页")
require_permission(session, actor, "payroll:view")
business = session.get(Business, business_id)
if business is None:
raise NotFoundError("业务不存在")
source_customer = (business.integration_json or {}).get("customer_source_id")
offset = (page_num - 1) * page_size
people = session.execute(select(Personnel.id, Personnel.personnel_name).order_by(Personnel.id)
.offset(offset).limit(page_size)).mappings()
lines_query = select(ShipmentItem.id, Shipment.shipment_no, Shipment.ship_date,
ShipmentItem.product_name, ShipmentItem.amount, ShipmentItem.unit).join(
Shipment, Shipment.id == ShipmentItem.shipment_id).where(Shipment.business_id == business_id)
lines = list(session.execute(lines_query.order_by(Shipment.id, ShipmentItem.id).offset(offset).limit(page_size)).mappings())
source = (business.integration_json or {}).get("source_database")
candidates = {}
if source and lines:
documents = session.scalars(select(TPlusBusinessDocument).where(
TPlusBusinessDocument.business_id == business_id,
TPlusBusinessDocument.source_database == source,
TPlusBusinessDocument.entity_type == "shipment_items",
TPlusBusinessDocument.status == "APPLIED",
TPlusBusinessDocument.target_id.in_([row["id"] for row in lines]),
))
for document in documents:
candidates.setdefault(document.target_id, []).append(document)
person_sources = {str(data["source_personnel_id"]) for values in candidates.values() for doc in values
if (data := doc.data_json or {}).get("source_personnel_id") is not None}
links = dict(session.execute(select(TPlusSourceLink.source_id, Personnel.id).join(
Personnel, Personnel.id == TPlusSourceLink.target_id).where(
TPlusSourceLink.source_database == source, TPlusSourceLink.entity_type == "personnel",
TPlusSourceLink.source_id.in_(person_sources))).all()) if person_sources else {}
record_ids = {doc.record_id for values in candidates.values() for doc in values}
records = {r.id: r for r in session.scalars(select(TPlusSyncRecord).where(
TPlusSyncRecord.id.in_(record_ids)))} if record_ids else {}
guidance_facts = latest_guidance_facts(session, source) if record_ids else {}
def source_details(row):
matches = candidates.get(row["id"], [])
document = matches[0] if len(matches) == 1 else None
data = (document.data_json or {}) if document else {}
source_id = data.get("source_personnel_id")
record = records.get(document.record_id) if document else None
product_id = record.payload_json.get("idinventory") if record else None
status, guidance = resolve_guidance(guidance_facts, str(product_id), document.source_id,
row["ship_date"].strftime("%Y-%m"), row["unit"]) if product_id is not None else ("MISSING", None)
if guidance:
status = "FINALIZED_" + guidance["basis"]
return {"source_personnel_name": data.get("source_personnel_name"),
"matched_personnel_id": links.get(str(source_id)) if source_id is not None else None,
"unit": row["unit"],
"guidance_status": status,
"guidance_unit_cost": guidance["amount"] if status == "FINALIZED_UNIT" else None,
"guidance_line_total": guidance["amount"] if status == "FINALIZED_LINE_TOTAL" else None}
return {"business_key": f"customer:{source_customer}" if source_customer is not None else None,
"personnel": [dict(row) for row in people],
"personnel_total": session.scalar(select(func.count()).select_from(Personnel)),
"shipments_total": session.scalar(select(func.count()).select_from(ShipmentItem).join(
Shipment, Shipment.id == ShipmentItem.shipment_id).where(Shipment.business_id == business_id)),
"shipments": [{"id": row["id"], "shipment_no": row["shipment_no"],
"ship_date": str(row["ship_date"]), "product_name": row["product_name"],
"amount": str(row["amount"]), **source_details(row)} for row in lines]}
def options(session, actor, resource, *, business_id=None, search=None):
definition = OPTIONS.get(resource)
if definition is None:
raise BadRequestError("不支持的参照类型")
model, fields, callers = definition
if not set(calculate(session, actor)["permissions"]) & set(callers):
raise ForbiddenError("没有使用此参照数据的业务权限")
statement = select(*(getattr(model, f) for f in fields))
if resource == "managers":
statement = (
statement.join(BusinessRole, BusinessRole.id == Personnel.business_role_id)
.join(CalcTemplateDefinition, CalcTemplateDefinition.template_code == BusinessRole.calc_template)
.where(
Personnel.status == 1, BusinessRole.status == 1,
CalcTemplateDefinition.formula_type == "manager_share",
)
)
if business_id is not None and model == Shipment:
statement = statement.where(Shipment.business_id == business_id)
if search:
name = getattr(model, fields[1])
escaped = search.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
statement = statement.where(name.like(f"%{escaped}%", escape="\\"))
order = (model.sort_order, model.id) if model == CustomerRatingGrade else (model.id,)
rows = session.execute(statement.order_by(*order).limit(200)).mappings()
return [dict(row) for row in rows]