feat(T+): 完成数据贯通与自动接入闭环
打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
This commit is contained in:
1 parent
3efe72c344
commit
1e53792ad6
151 files changed
+8842
-401
No files matched your search
@@ -1,5 +1,5 @@
|
||||
"""业务表单参照只返回必要字段,不借下拉权限开放整个资料接口。"""
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.core.exceptions import BadRequestError, ForbiddenError
|
||||
from app.models import (
|
||||
@@ -49,28 +49,75 @@ OPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
def payroll_options(session, actor, business_id):
|
||||
def payroll_options(session, actor, business_id, *, page_num=1, page_size=200):
|
||||
"""核算资料所需的人员及发货明细,不开放人员薪酬或联系方式。"""
|
||||
from app.core.exceptions import NotFoundError
|
||||
from app.models import ShipmentItem
|
||||
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")
|
||||
if session.get(Business, business_id) is None:
|
||||
business = session.get(Business, business_id)
|
||||
if business is None:
|
||||
raise NotFoundError("业务不存在")
|
||||
heads = list(session.scalars(select(Shipment).where(Shipment.business_id == business_id).order_by(Shipment.id).limit(201)))
|
||||
if len(heads) > 200:
|
||||
raise BadRequestError("单笔业务超过 200 张发货单,请分批核对资料")
|
||||
people = [dict(row) for row in session.execute(select(Personnel.id, Personnel.personnel_name).order_by(Personnel.id).limit(201)).mappings()]
|
||||
if len(people) > 200:
|
||||
raise BadRequestError("核算人员超过单次范围,请按实际归属分批处理")
|
||||
lines = session.execute(select(ShipmentItem.id, ShipmentItem.shipment_id, ShipmentItem.product_name, ShipmentItem.amount).where(ShipmentItem.shipment_id.in_([h.id for h in heads]))).mappings()
|
||||
by_head = {head.id: head for head in heads}
|
||||
return {"personnel": people, "shipments": [
|
||||
{"id": row["id"], "shipment_no": by_head[row["shipment_id"]].shipment_no,
|
||||
"ship_date": str(by_head[row["shipment_id"]].ship_date), "product_name": row["product_name"], "amount": str(row["amount"])}
|
||||
for row in lines
|
||||
]}
|
||||
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):
|
||||
|
||||
Reference in new issue
Block a user