refactor(后端): 将工资与工作区业务逻辑下沉服务层
将月份状态、资料输出、调整单编排及工作区查询、 CSV 生成移至现有服务层,路由仅绑定参数并返回响应。 保持接口路径、参数、响应模型和权限依赖一致, 补充九项内存库行为回归并同步测试记录。
This commit is contained in:
1 parent
019817d6dc
commit
71b7846a79
9 files changed
+426
-192
No files matched your search
@@ -22,6 +22,20 @@ from app.services.payroll_calculation import latest_input
|
||||
from app.services.tplus.workbench import digest
|
||||
|
||||
|
||||
def detail(session, business_id):
|
||||
"""返回最新核算资料;未准备资料的业务保持空结果。"""
|
||||
record = latest_input(session, business_id)
|
||||
if record is None:
|
||||
return None
|
||||
return {
|
||||
"id": record.id,
|
||||
"version": record.version,
|
||||
"source": record.source_database,
|
||||
"fingerprint": record.fingerprint,
|
||||
"data": record.data_json,
|
||||
}
|
||||
|
||||
|
||||
def setup(session, business_id, payload, operator, meta=None):
|
||||
with audit_service.audit(
|
||||
session,
|
||||
|
||||
@@ -15,6 +15,20 @@ def current_month() -> str:
|
||||
return datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m")
|
||||
|
||||
|
||||
def month_state(session, month):
|
||||
"""查询生成锁及制作人,兼容升级前已存在的月结记录。"""
|
||||
validate_month(month)
|
||||
control = session.scalar(select(PayrollMonth).where(PayrollMonth.month == month))
|
||||
existing = session.scalar(
|
||||
select(MonthlySettlement.id).where(MonthlySettlement.settlement_month == month)
|
||||
)
|
||||
return {
|
||||
"month": month,
|
||||
"allocation_locked": bool(existing or (control and control.generated_at)),
|
||||
"maker_name": control.maker_name if control else None,
|
||||
}
|
||||
|
||||
|
||||
def lock_month(session, month, operator):
|
||||
"""INSERT 的唯一键串行化首次建行,避免两个请求同时认为月份不存在。"""
|
||||
ensure_payroll_writer(operator)
|
||||
|
||||
@@ -534,6 +534,26 @@ def _detail_of(session: Session, settlement: MonthlySettlement) -> SettlementDet
|
||||
return detail
|
||||
|
||||
|
||||
def create_allocation_adjustment(session, payload, *, operator, meta=None):
|
||||
"""校验已生成工资的原台账,并将分配差额转换为当月调整单。"""
|
||||
from app.models import PerformanceLedger
|
||||
from app.schemas.settlement import AdjustmentCreateIn
|
||||
from app.services import payroll_months
|
||||
|
||||
old = session.get(PerformanceLedger, payload.ledger_id)
|
||||
if old is None or old.settlement_id is None:
|
||||
raise BadRequestError("请选择已生成工资的原台账")
|
||||
data = AdjustmentCreateIn(
|
||||
settlement_month=payroll_months.current_month(),
|
||||
personnel_id=payload.personnel_id,
|
||||
direction=payload.direction,
|
||||
amount=payload.amount,
|
||||
reason=payload.reason,
|
||||
source_ledger_id=old.id,
|
||||
)
|
||||
return create_adjustment(session, data, operator=operator, meta=meta)
|
||||
|
||||
|
||||
def create_adjustment(
|
||||
session: Session,
|
||||
payload,
|
||||
|
||||
@@ -12,8 +12,9 @@ from sqlalchemy.orm import defer
|
||||
from app.core.exceptions import BadRequestError, NotFoundError
|
||||
from app.core.response import PageResult
|
||||
from app.core.tplus_lock import TPlusBusyError, sync_lock
|
||||
from app.models import TPlusFact, TPlusWorkLine, TPlusWorkRun
|
||||
from app.models import TPlusFact, TPlusSyncRecord, TPlusWorkLine, TPlusWorkRun
|
||||
from app.schemas.tplus_workbench import WorkCreateIn
|
||||
from app.services.tplus.report_labels import ACTION_LABELS, reason_label, spreadsheet_value
|
||||
from app.services.tplus.task_service import _source_database, new_batch_no
|
||||
|
||||
RULE_VERSION = "2026-10-30.2"
|
||||
@@ -244,6 +245,152 @@ def list_lines(session, number, query):
|
||||
)
|
||||
|
||||
|
||||
def fact_history(session, fact_id, query):
|
||||
"""按事实身份分页查看修改历史,不允许跨账套读取。"""
|
||||
fact = session.get(TPlusFact, fact_id)
|
||||
if fact is None or fact.source_database != _source_database():
|
||||
raise NotFoundError("补充资料不存在")
|
||||
conditions = [
|
||||
TPlusFact.source_database == fact.source_database,
|
||||
TPlusFact.kind == fact.kind,
|
||||
TPlusFact.fact_key == fact.fact_key,
|
||||
]
|
||||
total = session.scalar(select(func.count()).select_from(TPlusFact).where(*conditions))
|
||||
rows = session.scalars(
|
||||
select(TPlusFact)
|
||||
.where(*conditions)
|
||||
.order_by(TPlusFact.version.desc())
|
||||
.offset(query.offset)
|
||||
.limit(query.page_size)
|
||||
)
|
||||
return PageResult(
|
||||
records=[fact_out(f) for f in rows],
|
||||
total=total or 0,
|
||||
page_num=query.page_num,
|
||||
page_size=query.page_size,
|
||||
)
|
||||
|
||||
|
||||
def source_choices(session, query):
|
||||
"""仅提供挑选资料所需的源编号和名称,不返回电话、地址和原始载荷。"""
|
||||
from app.services.tplus.snapshots import source_statement
|
||||
|
||||
try:
|
||||
statement = source_statement(session, _source_database(), query.entity)
|
||||
except WorkbenchError:
|
||||
raise BadRequestError("请先完整读取相关源数据") from None
|
||||
if query.search:
|
||||
escaped = query.search.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
statement = statement.where(TPlusSyncRecord.source_no.like(f"%{escaped}%", escape="\\"))
|
||||
total = session.scalar(select(func.count()).select_from(statement.subquery()))
|
||||
rows = session.scalars(
|
||||
statement.order_by(TPlusSyncRecord.source_id).offset(query.offset).limit(query.page_size)
|
||||
)
|
||||
records = [
|
||||
{
|
||||
"source_id": r.source_id,
|
||||
"source_no": r.source_no,
|
||||
"name": str(r.payload_json.get("name") or ""),
|
||||
"parent_id": str(r.payload_json.get("source_parent_id") or ""),
|
||||
"date": str(
|
||||
r.payload_json.get("voucherdate") or r.payload_json.get("voucherDate") or ""
|
||||
)[:10],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return PageResult(
|
||||
records=records, total=total or 0, page_num=query.page_num, page_size=query.page_size
|
||||
)
|
||||
|
||||
|
||||
def report_csv(session, run_no):
|
||||
"""导出全部预演明细,防止源单号被电子表格当作公式执行。"""
|
||||
import csv
|
||||
import io
|
||||
|
||||
get_run(session, run_no)
|
||||
|
||||
def rows():
|
||||
buffer = io.StringIO()
|
||||
writer = csv.writer(buffer)
|
||||
yield "\ufeff"
|
||||
writer.writerow(
|
||||
[
|
||||
"运行编号",
|
||||
"行号",
|
||||
"类型",
|
||||
"源ID或平账组",
|
||||
"处理结果",
|
||||
"源编号",
|
||||
"名称",
|
||||
"回款单号",
|
||||
"关联凭证示例",
|
||||
"发货单号",
|
||||
"匹配金额",
|
||||
"分配后该明细剩余应收",
|
||||
"回款天数",
|
||||
"指导成本分摊",
|
||||
"折算利润",
|
||||
"利润门槛(百分数)",
|
||||
"计提比例",
|
||||
"计提金额",
|
||||
"归属工号",
|
||||
"原因",
|
||||
"说明",
|
||||
]
|
||||
)
|
||||
yield buffer.getvalue()
|
||||
buffer.seek(0)
|
||||
buffer.truncate(0)
|
||||
statement = (
|
||||
select(TPlusWorkLine)
|
||||
.where(TPlusWorkLine.run_no == run_no)
|
||||
.order_by(TPlusWorkLine.line_no)
|
||||
)
|
||||
for row in session.scalars(statement).yield_per(200):
|
||||
data = row.data_json
|
||||
values = data.get("values") or {}
|
||||
fields = [
|
||||
run_no,
|
||||
row.line_no,
|
||||
row.entity_type,
|
||||
row.source_id,
|
||||
ACTION_LABELS.get(row.action, row.action),
|
||||
data.get("source_no", ""),
|
||||
values.get("personnel_name")
|
||||
or values.get("product_name")
|
||||
or values.get("company_name")
|
||||
or data.get("source_name")
|
||||
or "",
|
||||
*[
|
||||
data.get(k, "")
|
||||
for k in (
|
||||
"receipt_no",
|
||||
"accounting_voucher_code",
|
||||
"shipment_no",
|
||||
"amount",
|
||||
"shipment_remaining_after",
|
||||
"days",
|
||||
"allocated_cost",
|
||||
"converted_profit",
|
||||
"profit_threshold",
|
||||
"rate_percent",
|
||||
"performance",
|
||||
"personnel_id",
|
||||
)
|
||||
],
|
||||
reason_label(row.reason)
|
||||
or ("经理本人业务,已排除绩效汇总" if data.get("excluded_manager_own") else ""),
|
||||
"仅供核对,未写入正式工资",
|
||||
]
|
||||
writer.writerow([spreadsheet_value(v) for v in fields])
|
||||
yield buffer.getvalue()
|
||||
buffer.seek(0)
|
||||
buffer.truncate(0)
|
||||
|
||||
return rows()
|
||||
|
||||
|
||||
def execute_next(session):
|
||||
"""调用方持有原有全局锁;中断任务保留明细,禁止不明状态自动应用。"""
|
||||
for old in session.scalars(select(TPlusWorkRun).where(TPlusWorkRun.status == "RUNNING")):
|
||||
|
||||
Reference in new issue
Block a user