2026-11-02 22:20:55 -05:00
|
|
|
"""业务表单参照只返回必要字段,不借下拉权限开放整个资料接口。"""
|
2026-11-05 05:47:13 -05:00
|
|
|
from sqlalchemy import func, select
|
2026-11-02 22:20:55 -05:00
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-11-05 05:47:13 -05:00
|
|
|
def payroll_options(session, actor, business_id, *, page_num=1, page_size=200):
|
2026-11-02 22:20:55 -05:00
|
|
|
"""核算资料所需的人员及发货明细,不开放人员薪酬或联系方式。"""
|
|
|
|
|
from app.core.exceptions import NotFoundError
|
2026-11-05 05:47:13 -05:00
|
|
|
from app.models import ShipmentItem, TPlusBusinessDocument, TPlusSourceLink, TPlusSyncRecord
|
2026-11-02 22:20:55 -05:00
|
|
|
from app.services.authorization_service import require_permission
|
2026-11-05 05:47:13 -05:00
|
|
|
from app.services.tplus.guidance import latest_guidance_facts, resolve_guidance
|
2026-11-02 22:20:55 -05:00
|
|
|
|
2026-11-05 05:47:13 -05:00
|
|
|
if page_num < 1 or not 1 <= page_size <= 200:
|
|
|
|
|
raise ValueError("无效的核算参照分页")
|
2026-11-02 22:20:55 -05:00
|
|
|
require_permission(session, actor, "payroll:view")
|
2026-11-05 05:47:13 -05:00
|
|
|
business = session.get(Business, business_id)
|
|
|
|
|
if business is None:
|
2026-11-02 22:20:55 -05:00
|
|
|
raise NotFoundError("业务不存在")
|
2026-11-05 05:47:13 -05:00
|
|
|
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]}
|
2026-11-02 22:20:55 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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]
|