Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8020e667f1 | ||
|
|
50d56ce0bb | ||
|
|
4efd423d2d | ||
|
|
5e563a6669 | ||
|
|
d66e55af65 | ||
|
|
045ee8d0e3 | ||
|
|
4583c54568 | ||
|
|
5dad1bb0e3 | ||
|
|
19bc66ba45 | ||
|
|
076e892df9 | ||
|
|
07508e677b | ||
|
|
bab304abd3 | ||
|
|
6a9a8bddd6 | ||
|
|
c2c5078ee6 | ||
|
|
ef7ea3c190 | ||
|
|
61fdb6ebcd | ||
|
|
5651bcd85e | ||
|
|
17f3948dc7 | ||
|
|
85f99ed757 | ||
|
|
b8b1631d46 | ||
|
|
97d73d835b | ||
|
|
a51d4e10aa | ||
|
|
33c24129b4 | ||
|
|
063727a7d8 | ||
|
|
6e2656f999 | ||
|
|
a81f177cb6 | ||
|
|
77e8c78cb6 | ||
|
|
9b34f9e1ba | ||
|
|
0fb3ee50e3 | ||
|
|
d259004f9d | ||
|
|
9561f94074 | ||
|
|
afcb4e9ebe | ||
|
|
0506fe7e8a | ||
|
|
545a4c5fc1 | ||
|
|
d37bb41948 | ||
|
|
f76ef8a6e5 | ||
|
|
41e8d6f7c4 | ||
|
|
7aa685840f | ||
|
|
bd669ea77e | ||
|
|
96468f3c48 | ||
|
|
11a46bf1eb | ||
|
|
d67965aae2 | ||
|
|
dd00dba39c | ||
|
|
92c59d67b4 | ||
|
|
9325f8cf61 | ||
|
|
ac8e471aca | ||
|
|
03fcc01d6d | ||
|
|
3ceee48c00 | ||
|
|
19dffbaa49 | ||
|
|
be2226e4ca | ||
|
|
48dcf991dc | ||
|
|
d355cd0640 | ||
|
|
c825c170e7 | ||
|
|
9de7f5456e | ||
|
|
e2fee1d9c5 | ||
|
|
80d238d31d | ||
|
|
526c9029c8 | ||
|
|
b545e5987c | ||
|
|
d85c70a860 | ||
|
|
b76a7d65be | ||
|
|
d998378e12 | ||
|
|
90c1b83a54 | ||
|
|
75f00a6472 | ||
|
|
241dd2861c | ||
|
|
d29a18db86 | ||
|
|
3a148b2985 | ||
|
|
6c8b8025d7 | ||
|
|
209fb7bff3 | ||
|
|
e5ae28f0c7 | ||
|
|
0402f797c0 | ||
|
|
b042266c5b | ||
|
|
5e38dc1eca | ||
|
|
00a315b61c | ||
|
|
35532eceee | ||
|
|
a148961b55 | ||
|
|
6cd22b218d | ||
|
|
3ecd8f4a74 | ||
|
|
06d021807b | ||
|
|
ecde2ba07d | ||
|
|
b9d981d46b | ||
|
|
2b5f2f939c | ||
|
|
7ba336169a | ||
|
|
79f9c8785b | ||
|
|
87f1e9a4eb | ||
|
|
a7a5a914f1 | ||
|
|
920b432491 | ||
|
|
b592aaec2e | ||
|
|
012527ed35 | ||
|
|
9d7a72673d | ||
|
|
5ce18e56a0 | ||
|
|
f045841f1a | ||
|
|
82b8a9c1b2 | ||
|
|
6eafec3fa9 | ||
|
|
321dd3ca43 | ||
|
|
4e586e905b | ||
|
|
bbc24b7c73 | ||
|
|
42dde4b0bc | ||
|
|
3950b5c334 | ||
|
|
a2125374a8 | ||
|
|
6e190665c0 | ||
|
|
f040f579d1 | ||
|
|
e16afdee95 | ||
|
|
e56d8d7f51 | ||
|
|
0398cb3897 | ||
|
|
ee95036666 | ||
|
|
b217ffcdca | ||
|
|
e4ec917a03 | ||
|
|
63f1d7c2f9 | ||
|
|
c767373a8f | ||
|
|
8dd816ee1d | ||
|
|
1c53bcd226 | ||
|
|
f5c514c520 | ||
|
|
f5daab0d44 | ||
|
|
a7b8560f5f | ||
|
|
3354e66d82 | ||
|
|
20a8bf538a | ||
|
|
66e1c8334c | ||
|
|
be22c37196 | ||
|
|
9c61f36db8 | ||
|
|
cc843535f5 | ||
|
|
d08f03ab03 | ||
|
|
6d56572f8b | ||
|
|
618d8fe21c | ||
|
|
6ec4ec6730 | ||
|
|
5fd5550e0d | ||
|
|
13ac45d10d | ||
|
|
0efcd6a6e5 | ||
|
|
08150167a8 | ||
|
|
6c66eb164e | ||
|
|
99e71096ac | ||
|
|
0ab0774e8a | ||
|
|
aa231ea5f8 | ||
|
|
bee2c0d193 | ||
|
|
60efc5d5d3 | ||
|
|
e0ef78925a | ||
|
|
56f379dd0c | ||
|
|
c22c9467f5 | ||
|
|
22a6e1f4e4 | ||
|
|
efbf8f6ac9 | ||
|
|
8cebb8e0b8 | ||
|
|
1c225ec89d | ||
|
|
ad18204eb9 | ||
|
|
9c264d7942 | ||
|
|
aeadaa9919 | ||
|
|
50400ad19c | ||
|
|
edabf70287 | ||
|
|
d676bfadd7 | ||
|
|
eee70e417c | ||
|
|
da35921f58 | ||
|
|
8618141c2f | ||
|
|
af992b5680 | ||
|
|
cc0b217c89 | ||
|
|
727002761d | ||
|
|
a5bf4252b9 | ||
|
|
a6f56f3453 | ||
|
|
c71bb5a11c | ||
|
|
d2bb1e0fa1 | ||
|
|
b8b20dcebe | ||
|
|
a1be31c694 | ||
|
|
06a30ff658 | ||
|
|
97bf67c962 | ||
|
|
f659ec602e | ||
|
|
8c0cbb8ab8 | ||
|
|
75bae974e4 | ||
|
|
d72e28959d | ||
|
|
ce9d836194 | ||
|
|
55e321f266 | ||
|
|
54ec8e1cd1 | ||
|
|
e0dc7f4cee | ||
|
|
f761c7021e | ||
|
|
06255d1e41 | ||
|
|
0927a514d2 | ||
|
|
0c4a2408ee | ||
|
|
a3a2ffcce0 | ||
|
|
7a750e7e81 | ||
|
|
b28ed3e280 | ||
|
|
702ff0e074 | ||
|
|
81bda0cb79 | ||
|
|
02d1e16036 | ||
|
|
3f40caa1cd | ||
|
|
337d31bbe8 | ||
|
|
7e930c77fd | ||
|
|
34bf9aa0dc | ||
|
|
c5c7e42feb | ||
|
|
8453c9c805 | ||
|
|
e8e278be86 | ||
|
|
99bd4749ea | ||
|
|
d20b9f5a20 | ||
|
|
3018d05bc4 | ||
|
|
7a6d804cfa | ||
|
|
a47a7db7dc | ||
|
|
f5c999f633 | ||
|
|
8c82f6a6dd | ||
|
|
28990e66be | ||
|
|
5a24f3b433 | ||
|
|
b2ea79710c | ||
|
|
daace3ff74 | ||
|
|
8ef8981d71 | ||
|
|
b0a4b94d7d | ||
|
|
db31990ebe | ||
|
|
2d3472f5c4 | ||
|
|
f5fdfc6770 | ||
|
|
416b1cefa2 | ||
|
|
842aa0847f | ||
|
|
ea5fba9dc2 | ||
|
|
628a20dbbd | ||
|
|
5025aa09c2 | ||
|
|
34c8a1a62d | ||
|
|
da63c228f5 | ||
|
|
de132a7c04 | ||
|
|
674084c511 | ||
|
|
8b42cf1b90 | ||
|
|
60ad9de191 | ||
|
|
32c742c1ed | ||
|
|
3edfb9dd86 | ||
|
|
8cbbd3548b | ||
|
|
c860e39363 | ||
|
|
eb8a76d8d2 | ||
|
|
d4477f357d | ||
|
|
70fb2219de | ||
|
|
9444945add | ||
|
|
bce0a15772 | ||
|
|
df5206454f | ||
|
|
5d0e3d904a | ||
|
|
500a85465f | ||
|
|
f1dcafd33d | ||
|
|
df827e4344 | ||
|
|
28b10663ee | ||
|
|
5546b7aa73 | ||
|
|
e194e1ad04 | ||
|
|
e6763241d1 | ||
|
|
e521d781e3 | ||
|
|
fac7b87728 | ||
|
|
55ed0d8929 | ||
|
|
776031cb4c | ||
|
|
884d2f4a41 | ||
|
|
1e12783a41 | ||
|
|
38f9822d49 | ||
|
|
0da75fb855 | ||
|
|
4904508d9f | ||
|
|
8f00db9400 | ||
|
|
702c8a2f12 | ||
|
|
e69ddd0d32 | ||
|
|
85b315a0cf | ||
|
|
26fd37e6f9 | ||
|
|
86ba0c2cd0 | ||
|
|
f092b64a33 | ||
|
|
971af93132 | ||
|
|
04eb1e81bd | ||
|
|
68f653eb51 | ||
|
|
57d6241804 | ||
|
|
5dd545ba5c | ||
|
|
468a7c5e12 | ||
|
|
6d542a0acf | ||
|
|
66fc507b1a | ||
|
|
dbdabdce24 | ||
|
|
719bf20c5b | ||
|
|
95a28d5f0f | ||
|
|
a6138d60bc | ||
|
|
818b809614 | ||
|
|
af5c6e725d | ||
|
|
e7f2dfa4d4 | ||
|
|
f005370a1e | ||
|
|
910f61ed33 | ||
|
|
fc14c51348 | ||
|
|
69e76b85ab | ||
|
|
e027b5abc4 | ||
|
|
cf86c585b7 | ||
|
|
98ad5370dc | ||
|
|
be98dbe7ac | ||
|
|
ed37a31654 | ||
|
|
56fd44388b | ||
|
|
c1f9a56404 | ||
|
|
70c92fb33f | ||
|
|
db75ca6deb | ||
|
|
f447db7681 | ||
|
|
f8b0e2de11 | ||
|
|
cf4b8c0e56 | ||
|
|
d5bfb895a7 | ||
|
|
d5ad755695 | ||
|
|
edaddc9da1 | ||
|
|
32c01a4c69 | ||
|
|
1bc44cab13 | ||
|
|
c35e4ca3d2 | ||
|
|
47f873a3e6 | ||
|
|
4849b40594 | ||
|
|
33004fe9b9 | ||
|
|
f73428dcfd | ||
|
|
e7677d1661 | ||
|
|
a9112414c9 | ||
|
|
fdd2c1fbc3 | ||
|
|
c404c168fe | ||
|
|
4e71a13165 | ||
|
|
a328b9ff2d | ||
|
|
2ff5bc09f9 | ||
|
|
aed7a29c1f | ||
|
|
8827ec3804 | ||
|
|
ca3d464761 | ||
|
|
4928e00423 |
No files matched your search
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# 生产 Docker 交付包:仅 tag 触发
|
||||||
|
# 镜像与包名固定为 v1.1.0(与现网 .env / 部署习惯一致),便于生产升级时不必改镜像标签
|
||||||
|
# 产物:/root/zkxw-releases/zkxw-docker-deploy-v1.1.0.tar.gz
|
||||||
|
# 脚本:./scripts/package-docker-deploy.sh
|
||||||
|
#
|
||||||
|
# docker.sock 与 /releases 由 runner 环境变量 DRONE_RUNNER_VOLUMES 注入,
|
||||||
|
# 勿在本文件再声明同路径 host volume,否则会 Duplicate mount point。
|
||||||
|
#
|
||||||
|
# 注意:Drone 会对 commands 里的 ${VAR} 做预处理替换;自定义环境变量请用 $${VAR}
|
||||||
|
# 或直接写死版本号,避免变成空字符串。
|
||||||
|
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: package-prod
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
limit: 1
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
|
||||||
|
clone:
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: package-docker
|
||||||
|
image: docker:27-cli
|
||||||
|
environment:
|
||||||
|
DOCKER_BUILDKIT: '1'
|
||||||
|
NODE_MAX_OLD_SPACE_SIZE: '3072'
|
||||||
|
NPM_REGISTRY: https://registry.npmmirror.com
|
||||||
|
PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple/
|
||||||
|
PIP_TRUSTED_HOST: mirrors.aliyun.com
|
||||||
|
ZKXW_DEPLOY_VERSION: v1.1.0
|
||||||
|
commands:
|
||||||
|
- set -eu
|
||||||
|
# 国内机访问 dl-cdn.alpinelinux.org 偶发 Permission denied,改用阿里云镜像
|
||||||
|
- sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories
|
||||||
|
- apk add --no-cache bash git python3
|
||||||
|
# 固定 v1.1.0;$${ } 防止 Drone 预处理把自定义变量掏空
|
||||||
|
- export ZKXW_DEPLOY_VERSION="$${ZKXW_DEPLOY_VERSION:-v1.1.0}"
|
||||||
|
- echo "==> git tag=${DRONE_TAG:-none} deploy_version=$${ZKXW_DEPLOY_VERSION} commit=${DRONE_COMMIT}"
|
||||||
|
- chmod +x scripts/package-docker-deploy.sh scripts/build-docker-images.sh
|
||||||
|
- |
|
||||||
|
if [ -f /releases/.deploy-docker.env ]; then
|
||||||
|
mkdir -p deploy/docker
|
||||||
|
cp /releases/.deploy-docker.env deploy/docker/.env
|
||||||
|
echo "using /releases/.deploy-docker.env for secret inject"
|
||||||
|
fi
|
||||||
|
- ./scripts/package-docker-deploy.sh "$${ZKXW_DEPLOY_VERSION}"
|
||||||
|
- mkdir -p /releases
|
||||||
|
- cp -v dist/zkxw-docker-deploy-"$${ZKXW_DEPLOY_VERSION}".tar.gz /releases/
|
||||||
|
- ls -lh /releases/zkxw-docker-deploy-"$${ZKXW_DEPLOY_VERSION}".tar.gz
|
||||||
|
- sha256sum /releases/zkxw-docker-deploy-"$${ZKXW_DEPLOY_VERSION}".tar.gz
|
||||||
@@ -70,6 +70,7 @@ Full specs: `openspec/changes/zkxw-engineering-standards/specs/*/spec.md`. Below
|
|||||||
- **DB changes**: SHALL maintain SQL in `docs/sql/`. Schema changes SHALL align with `database-conventions` public field rules.
|
- **DB changes**: SHALL maintain SQL in `docs/sql/`. Schema changes SHALL align with `database-conventions` public field rules.
|
||||||
- **Commit messages**: SHALL be complete sentences describing "what changed and why". Follow Conventional Commits (`feat|fix|docs|…`).
|
- **Commit messages**: SHALL be complete sentences describing "what changed and why". Follow Conventional Commits (`feat|fix|docs|…`).
|
||||||
- **Field consistency (CRUD ↔ Schema ↔ Frontend)**: When implementing CRUD for any entity, the `_row_to_dict` function is the **single source of truth** for all readable fields. The CreateBody/UpdateBody Pydantic schemas and the frontend `formData` initialization SHALL include every writable field present in `_row_to_dict` (excluding computed/read-only fields like `id`, `createTime`, `updateTime`, `stockQty`). Run `python scripts/check_field_consistency.py` after implementation to verify. See `docs/dev-field-consistency.md` for details.
|
- **Field consistency (CRUD ↔ Schema ↔ Frontend)**: When implementing CRUD for any entity, the `_row_to_dict` function is the **single source of truth** for all readable fields. The CreateBody/UpdateBody Pydantic schemas and the frontend `formData` initialization SHALL include every writable field present in `_row_to_dict` (excluding computed/read-only fields like `id`, `createTime`, `updateTime`, `stockQty`). Run `python scripts/check_field_consistency.py` after implementation to verify. See `docs/dev-field-consistency.md` for details.
|
||||||
|
- **Child-row save(禁止整表删除再新增)**: 保存子表时 SHALL 按行处理:已有行就地更新,只插入新增行,只软删除用户去掉的行。客户端可以提交稳定 `id`,字段变化时该行必须保留原 `id`。请求未带 `id` 时,先用稳定业务键(样本编码、物料编码、条码、组分 id 等)匹配一条尚未占用的有效行,匹配不到再插入新 UUID。属于其他父记录或其他租户的 `id` SHALL 拒绝。SHALL NOT 在保存时把子表全部软删除再整批插入新行——其他表会拿这些 id 做外键(`production_step_output_id`、`protocol_sample_id`、`donation_sample_id`),整批换 id 会把关联打断。删除父记录时可以级联软删除其子行。复制或克隆父记录时 SHALL 为子行分配新 id。实现入口:`backend/app/crud/child_row_sync.py` 的 `sync_child_rows`。
|
||||||
|
|
||||||
### Permission Conventions
|
### Permission Conventions
|
||||||
|
|
||||||
@@ -220,3 +221,5 @@ pnpm clean:dev # strip demo data for fresh start
|
|||||||
- **Table column widths**: All `el-table-column` SHALL use `min-width` (not `width`) for proportional scaling when viewport resizes.
|
- **Table column widths**: All `el-table-column` SHALL use `min-width` (not `width`) for proportional scaling when viewport resizes.
|
||||||
- **Toolbar**: "已选 N 项" first, then primary action button (only one `type="primary"` per toolbar row), no `space-between` push.
|
- **Toolbar**: "已选 N 项" first, then primary action button (only one `type="primary"` per toolbar row), no `space-between` push.
|
||||||
- **Database changes**: always pair with schema SQL in `docs/sql/` and evaluate API/TypeScript type impact.
|
- **Database changes**: always pair with schema SQL in `docs/sql/` and evaluate API/TypeScript type impact.
|
||||||
|
- **Child-row save**: SHALL NOT soft-delete every child row and insert new rows on save. Update in place, insert new rows, soft-delete only removed rows. See Coding Conventions.
|
||||||
|
- **进程重启**:修改 bug 时,如果前端或后端需要重启,Agent 只告知用户,由用户自行重启。Agent 不得自行停止、启动或重启 `web-admin` 开发服务与 `backend` 服务。
|
||||||
+19
-2
@@ -7,8 +7,10 @@ DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw
|
|||||||
# 已废弃:无角色用户不可登录,JWT role 固定为 guest(勿设为 super_admin)
|
# 已废弃:无角色用户不可登录,JWT role 固定为 guest(勿设为 super_admin)
|
||||||
# WEB_AUTH_DEFAULT_ROLE=guest
|
# WEB_AUTH_DEFAULT_ROLE=guest
|
||||||
|
|
||||||
# JWT配置示例
|
# JWT配置(必填;启动时会拒绝空值与弱占位密钥,长度至少 32)
|
||||||
# SECRET_KEY=your-secret-key-change-in-production
|
# 生成:python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||||
|
# 注意:更换 SECRET_KEY 会使已签发的 JWT 全部失效,用户需重新登录;生产勿随意轮换。
|
||||||
|
SECRET_KEY=
|
||||||
# ALGORITHM=HS256
|
# ALGORITHM=HS256
|
||||||
# ACCESS_TOKEN_EXPIRE_MINUTES=30
|
# ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
|
||||||
@@ -84,3 +86,18 @@ DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw
|
|||||||
# WX_MP_TPL_PROGRESS_FIELD_APPROVER=thing6
|
# WX_MP_TPL_PROGRESS_FIELD_APPROVER=thing6
|
||||||
# WX_MP_TPL_PROGRESS_FIELD_RESULT=const4
|
# WX_MP_TPL_PROGRESS_FIELD_RESULT=const4
|
||||||
# WX_MP_TPL_PROGRESS_FIELD_TIME=time11
|
# WX_MP_TPL_PROGRESS_FIELD_TIME=time11
|
||||||
|
# 基础实验交付日前一天提醒(出库提醒模板)
|
||||||
|
# WX_MP_TEMPLATE_BASIC_EXPERIMENT_DELIVERY=oQNU3t0Q5kwc4KPJ8Z6yPA-k6qlsH9pikSCIb4eAwgM
|
||||||
|
# WX_MP_TPL_BE_DELIVERY_FIELD_CUSTOMER=thing6
|
||||||
|
# WX_MP_TPL_BE_DELIVERY_FIELD_PRODUCT=thing10
|
||||||
|
# WX_MP_TPL_BE_DELIVERY_FIELD_QTY=character_string8
|
||||||
|
# WX_MP_TPL_BE_DELIVERY_FIELD_CONTACT=thing7
|
||||||
|
# WX_MP_TPL_BE_DELIVERY_FIELD_TIME=time14
|
||||||
|
# 协议新增样本采集通知(工单类模板)
|
||||||
|
# WX_MP_TEMPLATE_SAMPLE_COLLECT=iOnQ9k2mavAOwFbp_6yH4t1SHqpEkaiwNWKueY0kpwE
|
||||||
|
# WX_MP_SAMPLE_COLLECT_ROLE_NAME=样本采集通知人员
|
||||||
|
# WX_MP_TPL_SC_FIELD_TITLE=thing2
|
||||||
|
# WX_MP_TPL_SC_FIELD_APPLICANT=thing8
|
||||||
|
# WX_MP_TPL_SC_FIELD_TIME=time3
|
||||||
|
# WX_MP_TPL_SC_FIELD_TYPE=phrase10
|
||||||
|
# WX_MP_TPL_SC_FIELD_SAMPLE_TYPE=thing17
|
||||||
@@ -5,6 +5,10 @@ from app.api.admin import router as admin_router
|
|||||||
from app.api.workflow import router as workflow_router
|
from app.api.workflow import router as workflow_router
|
||||||
from app.api.basic_info import router as basic_info_router
|
from app.api.basic_info import router as basic_info_router
|
||||||
from app.api.collection_protocol import router as collection_protocol_router
|
from app.api.collection_protocol import router as collection_protocol_router
|
||||||
|
from app.api.protocol_sample_payment import router as protocol_sample_payment_router
|
||||||
|
from app.api.protocol_refund_record import router as protocol_refund_record_router
|
||||||
|
from app.api.presign_order import router as presign_order_router
|
||||||
|
from app.api.sample_freight import router as sample_freight_router
|
||||||
from app.api.leave import router as leave_router
|
from app.api.leave import router as leave_router
|
||||||
from app.api.operations import router as operations_router
|
from app.api.operations import router as operations_router
|
||||||
from app.api.ethnic import router as ethnic_router
|
from app.api.ethnic import router as ethnic_router
|
||||||
@@ -36,6 +40,11 @@ from app.api.pre_transit_sample import router as pre_transit_sample_router
|
|||||||
from app.api.oa_approval import router as oa_approval_router
|
from app.api.oa_approval import router as oa_approval_router
|
||||||
from app.api.mini_auth import router as mini_auth_router
|
from app.api.mini_auth import router as mini_auth_router
|
||||||
from app.api.mini_menu import router as mini_menu_router
|
from app.api.mini_menu import router as mini_menu_router
|
||||||
|
from app.api.market_mobile import router as market_mobile_router
|
||||||
|
from app.api.sample_storage_cert_qc import router as sample_storage_cert_qc_router
|
||||||
|
from app.api.customer_service_dict import router as customer_service_dict_router
|
||||||
|
from app.api.sample_cert_insurance import router as sample_cert_insurance_router
|
||||||
|
from app.api.sample_code_correction import router as sample_code_correction_router
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"public_router",
|
"public_router",
|
||||||
@@ -44,6 +53,10 @@ __all__ = [
|
|||||||
"workflow_router",
|
"workflow_router",
|
||||||
"basic_info_router",
|
"basic_info_router",
|
||||||
"collection_protocol_router",
|
"collection_protocol_router",
|
||||||
|
"protocol_sample_payment_router",
|
||||||
|
"protocol_refund_record_router",
|
||||||
|
"presign_order_router",
|
||||||
|
"sample_freight_router",
|
||||||
"leave_router",
|
"leave_router",
|
||||||
"operations_router",
|
"operations_router",
|
||||||
"ethnic_router",
|
"ethnic_router",
|
||||||
@@ -75,4 +88,9 @@ __all__ = [
|
|||||||
"oa_approval_router",
|
"oa_approval_router",
|
||||||
"mini_auth_router",
|
"mini_auth_router",
|
||||||
"mini_menu_router",
|
"mini_menu_router",
|
||||||
|
"market_mobile_router",
|
||||||
|
"sample_storage_cert_qc_router",
|
||||||
|
"customer_service_dict_router",
|
||||||
|
"sample_cert_insurance_router",
|
||||||
|
"sample_code_correction_router",
|
||||||
]
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
"""运营端管理接口(租户、角色下拉等)"""
|
"""运营端管理接口(租户、角色下拉等)"""
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
@@ -236,6 +237,25 @@ def _get_tenant_no(db: Session, user: dict) -> str:
|
|||||||
return (sys_user.tenant_no if sys_user else None) or "zkxw"
|
return (sys_user.tenant_no if sys_user else None) or "zkxw"
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_user_tenant_access(db: Session, user: dict, target_user_id: str):
|
||||||
|
"""同租户或 super_admin 才可写操作用户;返回目标用户行。"""
|
||||||
|
row = user_svc.get_user(db, target_user_id)
|
||||||
|
if not row:
|
||||||
|
raise ValueError("用户不存在")
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
if row.tenant_no != tenant_no and user.get("role") != "super_admin":
|
||||||
|
raise ValueError("无权操作其他租户的用户")
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_temp_password(length: int = 12) -> str:
|
||||||
|
"""生成可读的临时密码(字母数字,避免易混淆字符)。"""
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
|
||||||
|
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 用户管理
|
# 用户管理
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -405,6 +425,7 @@ async def update_user(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
_assert_user_tenant_access(db, user, user_id)
|
||||||
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
||||||
update_kwargs: dict[str, Any] = {
|
update_kwargs: dict[str, Any] = {
|
||||||
"nick_name": payload.get("nick_name"),
|
"nick_name": payload.get("nick_name"),
|
||||||
@@ -436,10 +457,11 @@ async def update_user(
|
|||||||
@router.delete("/users/{user_id}")
|
@router.delete("/users/{user_id}")
|
||||||
async def delete_user(
|
async def delete_user(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
_user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
_assert_user_tenant_access(db, user, user_id)
|
||||||
user_svc.soft_delete_user(db, user_id)
|
user_svc.soft_delete_user(db, user_id)
|
||||||
return success_response(None, message="删除成功")
|
return success_response(None, message="删除成功")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -449,12 +471,17 @@ async def delete_user(
|
|||||||
@router.post("/users/{user_id}/reset-password")
|
@router.post("/users/{user_id}/reset-password")
|
||||||
async def reset_user_password(
|
async def reset_user_password(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
_user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
user_svc.reset_password(db, user_id, "111111")
|
_assert_user_tenant_access(db, user, user_id)
|
||||||
return success_response(None, message="密码已重置为 111111")
|
temp_password = _generate_temp_password(12)
|
||||||
|
user_svc.reset_password(db, user_id, temp_password)
|
||||||
|
return success_response(
|
||||||
|
{"tempPassword": temp_password},
|
||||||
|
message="密码已重置,请将临时密码告知用户并督促尽快修改",
|
||||||
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
@@ -1590,7 +1617,10 @@ async def upload_attachment(
|
|||||||
return error_response(400, "请选择文件")
|
return error_response(400, "请选择文件")
|
||||||
try:
|
try:
|
||||||
file_content = await file.read()
|
file_content = await file.read()
|
||||||
full_url = storage.upload_file(file_content, file.filename, folder or "")
|
# MinIO/本地写入是同步的。多张大图并行时若占住事件循环,其它上传会一直等不到响应,前端表现为超时后文件消失。
|
||||||
|
full_url = await asyncio.to_thread(
|
||||||
|
storage.upload_file, file_content, file.filename, folder or ""
|
||||||
|
)
|
||||||
base_url = settings.MINIO_BASE_URL.rstrip("/") if settings.MINIO_BASE_URL else ""
|
base_url = settings.MINIO_BASE_URL.rstrip("/") if settings.MINIO_BASE_URL else ""
|
||||||
if base_url and full_url.startswith(base_url + "/"):
|
if base_url and full_url.startswith(base_url + "/"):
|
||||||
file_url = full_url[len(base_url) + 1:]
|
file_url = full_url[len(base_url) + 1:]
|
||||||
@@ -1644,8 +1674,10 @@ async def attachment_download_url(
|
|||||||
return error_response(400, "文件地址不能为空")
|
return error_response(400, "文件地址不能为空")
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
att = _lookup_attachment_by_url(db, file_url, tenant_no)
|
att = _lookup_attachment_by_url(db, file_url, tenant_no)
|
||||||
|
if not att:
|
||||||
|
return error_response(403, "无权下载该附件或附件未登记")
|
||||||
download_name = (body.file_name or "").strip()
|
download_name = (body.file_name or "").strip()
|
||||||
if not download_name and att:
|
if not download_name:
|
||||||
download_name = (att.original_name or "").strip()
|
download_name = (att.original_name or "").strip()
|
||||||
if not download_name:
|
if not download_name:
|
||||||
download_name = file_url.rsplit("/", 1)[-1] or "download"
|
download_name = file_url.rsplit("/", 1)[-1] or "download"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from app.models.models import (
|
|||||||
BizSampleCode,
|
BizSampleCode,
|
||||||
SysUser,
|
SysUser,
|
||||||
)
|
)
|
||||||
|
from app.services.customer_relations import build_customer_relations
|
||||||
|
|
||||||
router = APIRouter(prefix="/web/admin", tags=["基础信息管理"])
|
router = APIRouter(prefix="/web/admin", tags=["基础信息管理"])
|
||||||
|
|
||||||
@@ -50,6 +51,10 @@ class CustomerCreateBody(BaseModel):
|
|||||||
sex: Optional[str] = Field("2", alias="sex")
|
sex: Optional[str] = Field("2", alias="sex")
|
||||||
id_card_no: Optional[str] = Field(None, alias="idCardNo")
|
id_card_no: Optional[str] = Field(None, alias="idCardNo")
|
||||||
age: Optional[int] = None
|
age: Optional[int] = None
|
||||||
|
birth_ym: Optional[str] = Field(None, alias="birthYm")
|
||||||
|
ethnic: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
phone_no: Optional[str] = Field(None, alias="phoneNo")
|
phone_no: Optional[str] = Field(None, alias="phoneNo")
|
||||||
customer_tags: Optional[str] = Field(None, alias="customerTags")
|
customer_tags: Optional[str] = Field(None, alias="customerTags")
|
||||||
order_num: Optional[int] = Field(0, alias="orderNum")
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
||||||
@@ -63,6 +68,10 @@ class CustomerUpdateBody(BaseModel):
|
|||||||
sex: Optional[str] = None
|
sex: Optional[str] = None
|
||||||
id_card_no: Optional[str] = Field(None, alias="idCardNo")
|
id_card_no: Optional[str] = Field(None, alias="idCardNo")
|
||||||
age: Optional[int] = None
|
age: Optional[int] = None
|
||||||
|
birth_ym: Optional[str] = Field(None, alias="birthYm")
|
||||||
|
ethnic: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
phone_no: Optional[str] = Field(None, alias="phoneNo")
|
phone_no: Optional[str] = Field(None, alias="phoneNo")
|
||||||
customer_tags: Optional[str] = Field(None, alias="customerTags")
|
customer_tags: Optional[str] = Field(None, alias="customerTags")
|
||||||
order_num: Optional[int] = Field(None, alias="orderNum")
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
||||||
@@ -119,6 +128,19 @@ async def list_customers(
|
|||||||
return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/customers/{customer_id}/relations")
|
||||||
|
async def get_customer_relations(
|
||||||
|
customer_id: str,
|
||||||
|
user: dict = Depends(require_menu_code("biz:customer:list")),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
data = build_customer_relations(db, customer_id, tenant_no)
|
||||||
|
if not data:
|
||||||
|
return error_response(404, "客户不存在")
|
||||||
|
return success_response(data)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/customers")
|
@router.post("/customers")
|
||||||
async def create_customer(
|
async def create_customer(
|
||||||
body: CustomerCreateBody,
|
body: CustomerCreateBody,
|
||||||
@@ -133,6 +155,10 @@ async def create_customer(
|
|||||||
sex=body.sex or "2",
|
sex=body.sex or "2",
|
||||||
id_card_no=body.id_card_no,
|
id_card_no=body.id_card_no,
|
||||||
age=body.age,
|
age=body.age,
|
||||||
|
birth_ym=body.birth_ym,
|
||||||
|
ethnic=body.ethnic,
|
||||||
|
email=body.email,
|
||||||
|
address=body.address,
|
||||||
phone_no=body.phone_no,
|
phone_no=body.phone_no,
|
||||||
customer_tags=body.customer_tags,
|
customer_tags=body.customer_tags,
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
@@ -162,6 +188,10 @@ async def update_customer(
|
|||||||
sex=payload.get("sex"),
|
sex=payload.get("sex"),
|
||||||
id_card_no=payload.get("id_card_no"),
|
id_card_no=payload.get("id_card_no"),
|
||||||
age=payload.get("age"),
|
age=payload.get("age"),
|
||||||
|
birth_ym=payload.get("birth_ym"),
|
||||||
|
ethnic=payload.get("ethnic"),
|
||||||
|
email=payload.get("email"),
|
||||||
|
address=payload.get("address"),
|
||||||
phone_no=payload.get("phone_no"),
|
phone_no=payload.get("phone_no"),
|
||||||
customer_tags=payload.get("customer_tags"),
|
customer_tags=payload.get("customer_tags"),
|
||||||
order_num=payload.get("order_num"),
|
order_num=payload.get("order_num"),
|
||||||
@@ -433,6 +463,17 @@ async def delete_collection_region(
|
|||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/collection-regions/by-staff-user/{user_id}")
|
||||||
|
async def api_list_regions_by_staff_user(
|
||||||
|
user_id: str,
|
||||||
|
user: dict = Depends(require_menu_code("biz:collectionRegion:list")),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items = region_svc.list_regions_by_staff_user(db, user_id=user_id, tenant_no=tenant_no)
|
||||||
|
return success_response({"list": items})
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 采集地区 — 下属医院
|
# 采集地区 — 下属医院
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,29 +2,60 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from sqlalchemy import and_ as sa_and, func as sa_func
|
from sqlalchemy import and_ as sa_and, func as sa_func
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.core.data_scope import get_user_dept_id
|
||||||
from app.core.menu_guard import require_admin_or_super_admin
|
from app.core.menu_guard import require_admin_or_super_admin
|
||||||
from app.core.response import error_response, success_response
|
from app.core.response import error_response, success_response
|
||||||
from app.core.utils import parse_datetime
|
from app.core.utils import parse_datetime
|
||||||
from app.crud import collection_protocol as cp_svc
|
from app.crud import collection_protocol as cp_svc
|
||||||
from app.crud import customer as customer_crud
|
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizCollectionProtocol,
|
BizCollectionProtocol,
|
||||||
BizCustomer,
|
|
||||||
BizProtocolPartyCustomer,
|
BizProtocolPartyCustomer,
|
||||||
BizProtocolSample,
|
BizProtocolSample,
|
||||||
BizProtocolStorageCert,
|
BizProtocolStorageCert,
|
||||||
SysUser,
|
SysUser,
|
||||||
)
|
)
|
||||||
|
from app.services.party_customer_sync import sync_party_customers
|
||||||
|
from app.services.sample_collect_wx_notify import (
|
||||||
|
PROTOCOL_KIND_CELL,
|
||||||
|
sample_codes_from_orm_rows,
|
||||||
|
sample_codes_from_payload,
|
||||||
|
schedule_sample_collect_notify,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/web/admin", tags=["细胞存储"])
|
router = APIRouter(prefix="/web/admin", tags=["细胞存储"])
|
||||||
|
|
||||||
|
_PAYMENT_DETAIL_SENSITIVE_FIELDS = {
|
||||||
|
"pay_status",
|
||||||
|
"payStatus",
|
||||||
|
"finance_confirm_attachments",
|
||||||
|
"financeConfirmAttachments",
|
||||||
|
"finance_confirm_attachment_files",
|
||||||
|
"financeConfirmAttachmentFiles",
|
||||||
|
"finance_remark",
|
||||||
|
"financeRemark",
|
||||||
|
"refund_amount",
|
||||||
|
"refundAmount",
|
||||||
|
"refund_remark",
|
||||||
|
"refundRemark",
|
||||||
|
"refund_attachments",
|
||||||
|
"refundAttachments",
|
||||||
|
"refund_attachment_files",
|
||||||
|
"refundAttachmentFiles",
|
||||||
|
"refund_finance_confirm_attachments",
|
||||||
|
"refundFinanceConfirmAttachments",
|
||||||
|
"refund_finance_confirm_attachment_files",
|
||||||
|
"refundFinanceConfirmAttachmentFiles",
|
||||||
|
"refund_finance_remark",
|
||||||
|
"refundFinanceRemark",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_tenant_no(db: Session, user: dict) -> str:
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
user_id = user.get("user_id")
|
user_id = user.get("user_id")
|
||||||
@@ -79,49 +110,6 @@ def _parse_datetime_fields(d: dict, camel_keys: list) -> None:
|
|||||||
d[snake] = None
|
d[snake] = None
|
||||||
|
|
||||||
|
|
||||||
def _ensure_customer_for_mother(db: Session, party_customer: dict, tenant_no: str, user_id: str) -> dict:
|
|
||||||
"""If mother info exists but no customerId, create a customer record and link it."""
|
|
||||||
if not party_customer:
|
|
||||||
return party_customer
|
|
||||||
customer_id = party_customer.get("customer_id") or party_customer.get("customerId")
|
|
||||||
mother_name = (party_customer.get("mother_name") or party_customer.get("motherName") or "").strip()
|
|
||||||
if customer_id or not mother_name:
|
|
||||||
return party_customer
|
|
||||||
# Build customer fields from mother info
|
|
||||||
mother_phone = (party_customer.get("mother_phone") or party_customer.get("motherPhone") or "").strip() or None
|
|
||||||
mother_id_card = (party_customer.get("mother_id_card") or party_customer.get("motherIdCard") or "").strip() or None
|
|
||||||
|
|
||||||
# Check if customer with this id_card already exists
|
|
||||||
if mother_id_card and customer_crud.id_card_taken(db, mother_id_card, tenant_no):
|
|
||||||
existing = db.query(BizCustomer).filter(
|
|
||||||
sa_and(
|
|
||||||
BizCustomer.id_card_no == mother_id_card,
|
|
||||||
BizCustomer.tenant_no == tenant_no,
|
|
||||||
BizCustomer.del_flag == 0,
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
if existing:
|
|
||||||
party_customer["customer_id"] = existing.id
|
|
||||||
return party_customer
|
|
||||||
|
|
||||||
# Create new customer
|
|
||||||
try:
|
|
||||||
obj = customer_crud.create_customer(
|
|
||||||
db,
|
|
||||||
customer_name=mother_name,
|
|
||||||
sex="1", # female (mother)
|
|
||||||
id_card_no=mother_id_card,
|
|
||||||
phone_no=mother_phone,
|
|
||||||
tenant_no=tenant_no,
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
party_customer["customer_id"] = obj.id
|
|
||||||
except ValueError:
|
|
||||||
# If creation fails (e.g. duplicate), try to find existing
|
|
||||||
pass
|
|
||||||
return party_customer
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 采集协议单
|
# 采集协议单
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -184,12 +172,16 @@ class ProtocolCreateBody(BaseModel):
|
|||||||
health_survey_form_urls: Optional[str] = Field(None, alias="healthSurveyFormUrls")
|
health_survey_form_urls: Optional[str] = Field(None, alias="healthSurveyFormUrls")
|
||||||
register_date: Optional[str] = Field(None, alias="registerDate")
|
register_date: Optional[str] = Field(None, alias="registerDate")
|
||||||
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
|
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
|
||||||
|
protocol_total_amount: Optional[float] = Field(None, alias="protocolTotalAmount")
|
||||||
|
source_presign_order_id: Optional[str] = Field(None, alias="sourcePresignOrderId")
|
||||||
|
payment_model_version: Optional[int] = Field(2, alias="paymentModelVersion")
|
||||||
remark: Optional[str] = None
|
remark: Optional[str] = None
|
||||||
party_customer: Optional[dict] = Field(None, alias="partyCustomer")
|
party_customer: Optional[dict] = Field(None, alias="partyCustomer")
|
||||||
collection_info: Optional[dict] = Field(None, alias="collectionInfo")
|
collection_info: Optional[dict] = Field(None, alias="collectionInfo")
|
||||||
samples: Optional[list] = Field(None, alias="samples")
|
samples: Optional[list] = Field(None, alias="samples")
|
||||||
payment: Optional[dict] = Field(None, alias="payment")
|
payment: Optional[dict] = Field(None, alias="payment")
|
||||||
payment_details: Optional[list] = Field(None, alias="paymentDetails")
|
payment_details: Optional[list] = Field(None, alias="paymentDetails")
|
||||||
|
sample_payments: Optional[list] = Field(None, alias="samplePayments")
|
||||||
attachments: Optional[list] = Field(None, alias="attachments")
|
attachments: Optional[list] = Field(None, alias="attachments")
|
||||||
consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
|
consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
|
||||||
health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
|
health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
|
||||||
@@ -215,18 +207,40 @@ class ProtocolUpdateBody(BaseModel):
|
|||||||
to_research_time: Optional[str] = Field(None, alias="toResearchTime")
|
to_research_time: Optional[str] = Field(None, alias="toResearchTime")
|
||||||
to_research_desc: Optional[str] = Field(None, alias="toResearchDesc")
|
to_research_desc: Optional[str] = Field(None, alias="toResearchDesc")
|
||||||
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
|
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
|
||||||
|
protocol_total_amount: Optional[float] = Field(None, alias="protocolTotalAmount")
|
||||||
|
source_presign_order_id: Optional[str] = Field(None, alias="sourcePresignOrderId")
|
||||||
remark: Optional[str] = None
|
remark: Optional[str] = None
|
||||||
party_customer: Optional[dict] = Field(None, alias="partyCustomer")
|
party_customer: Optional[dict] = Field(None, alias="partyCustomer")
|
||||||
collection_info: Optional[dict] = Field(None, alias="collectionInfo")
|
collection_info: Optional[dict] = Field(None, alias="collectionInfo")
|
||||||
samples: Optional[list] = Field(None, alias="samples")
|
samples: Optional[list] = Field(None, alias="samples")
|
||||||
payment: Optional[dict] = Field(None, alias="payment")
|
payment: Optional[dict] = Field(None, alias="payment")
|
||||||
payment_details: Optional[list] = Field(None, alias="paymentDetails")
|
payment_details: Optional[list] = Field(None, alias="paymentDetails")
|
||||||
|
sample_payments: Optional[list] = Field(None, alias="samplePayments")
|
||||||
attachments: Optional[list] = Field(None, alias="attachments")
|
attachments: Optional[list] = Field(None, alias="attachments")
|
||||||
consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
|
consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
|
||||||
health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
|
health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
|
||||||
storage_cert: Optional[dict] = Field(None, alias="storageCert")
|
storage_cert: Optional[dict] = Field(None, alias="storageCert")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_protocol_sensitive_fields(
|
||||||
|
db: Session,
|
||||||
|
user: dict,
|
||||||
|
body: BaseModel,
|
||||||
|
*,
|
||||||
|
creating: bool,
|
||||||
|
) -> None:
|
||||||
|
"""防止通过普通协议保存接口注入财务确认或退费状态字段(不做菜单权限校验)。"""
|
||||||
|
for detail in getattr(body, "payment_details", None) or []:
|
||||||
|
normalized_detail = (
|
||||||
|
cp_svc._normalize_fields(detail) if isinstance(detail, dict) else {}
|
||||||
|
)
|
||||||
|
if _PAYMENT_DETAIL_SENSITIVE_FIELDS.intersection(normalized_detail.keys()):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="普通协议保存不允许提交财务确认或退费状态字段",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_protocol_attachments(
|
def _resolve_protocol_attachments(
|
||||||
protocol_attachments: Optional[str],
|
protocol_attachments: Optional[str],
|
||||||
attachments: Optional[list],
|
attachments: Optional[list],
|
||||||
@@ -268,36 +282,78 @@ def _persist_protocol_sub_resources(
|
|||||||
samples: Optional[list],
|
samples: Optional[list],
|
||||||
payment: Optional[dict],
|
payment: Optional[dict],
|
||||||
payment_details: Optional[list],
|
payment_details: Optional[list],
|
||||||
|
sample_payments: Optional[list] = None,
|
||||||
storage_cert: Optional[dict],
|
storage_cert: Optional[dict],
|
||||||
|
payment_model_version: int = 2,
|
||||||
|
commit: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""持久化协议子资源。
|
"""持久化协议子资源。
|
||||||
|
|
||||||
缴费(含明细校验)须在样本之前:明细校验失败时不得已占用样本编码。
|
version=1:缴费(含明细校验)须在样本之前,避免校验失败后已占用样本编码。
|
||||||
各子表 CRUD 内部仍可能分段 commit,创建接口须在失败时做补偿软删。
|
version=2:样本先落库占码,再按 sample_code 挂样本缴费方案。
|
||||||
|
|
||||||
|
默认整段子资源共用一次事务:内部一律 commit=False,成功后再统一 commit,
|
||||||
|
避免样本已提交、缴费校验失败导致「提示失败但部分已保存」。
|
||||||
"""
|
"""
|
||||||
if party_customer:
|
if party_customer:
|
||||||
_parse_datetime_fields(party_customer, ["newbornDeliveryTime"])
|
_parse_datetime_fields(party_customer, ["newbornDeliveryTime"])
|
||||||
party_customer = _ensure_customer_for_mother(db, party_customer, tenant_no, user_id)
|
party_customer = sync_party_customers(
|
||||||
cp_svc.upsert_party_customer(db, protocol_id, tenant_no, user_id, **party_customer)
|
db,
|
||||||
|
party_customer,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
commit=False,
|
||||||
|
)
|
||||||
|
cp_svc.upsert_party_customer(
|
||||||
|
db, protocol_id, tenant_no, user_id, commit=False, **party_customer
|
||||||
|
)
|
||||||
if collection_info:
|
if collection_info:
|
||||||
_parse_datetime_fields(collection_info, ["sampleCollectTime", "maternalBloodCollectTime"])
|
_parse_datetime_fields(collection_info, ["sampleCollectTime", "maternalBloodCollectTime"])
|
||||||
cp_svc.upsert_collection_info(db, protocol_id, tenant_no, user_id, **collection_info)
|
cp_svc.upsert_collection_info(
|
||||||
# 缴费先于样本:避免「应缴合计不符」等校验失败后样本编码已被占用
|
db, protocol_id, tenant_no, user_id, commit=False, **collection_info
|
||||||
pay = None
|
)
|
||||||
if payment:
|
|
||||||
pay = cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
|
version = int(payment_model_version or 2)
|
||||||
if payment_details is not None:
|
if version >= 2:
|
||||||
if not pay and payment:
|
if samples is not None:
|
||||||
pay = cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
|
cp_svc.save_samples(
|
||||||
if not pay:
|
db, protocol_id, tenant_no, user_id, samples, commit=False
|
||||||
pay = cp_svc.get_payment_by_protocol_id(db, protocol_id, tenant_no)
|
)
|
||||||
if pay:
|
if sample_payments is not None:
|
||||||
aligned_details = cp_svc.align_fq_payment_details_payload(pay, payment_details)
|
from app.crud import protocol_sample_payment as sp_pay_svc
|
||||||
cp_svc.save_payment_details(db, pay.id, tenant_no, user_id, aligned_details)
|
|
||||||
if samples is not None:
|
sp_pay_svc.save_sample_payments_for_protocol(
|
||||||
cp_svc.save_samples(db, protocol_id, tenant_no, user_id, samples)
|
db,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
sample_payments=sample_payments,
|
||||||
|
commit=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 旧模型:缴费先于样本
|
||||||
|
if payment or payment_details is not None:
|
||||||
|
cp_svc.save_payment_with_details(
|
||||||
|
db,
|
||||||
|
protocol_id,
|
||||||
|
tenant_no,
|
||||||
|
user_id,
|
||||||
|
payment=payment,
|
||||||
|
payment_details=payment_details,
|
||||||
|
commit=False,
|
||||||
|
)
|
||||||
|
if samples is not None:
|
||||||
|
cp_svc.save_samples(
|
||||||
|
db, protocol_id, tenant_no, user_id, samples, commit=False
|
||||||
|
)
|
||||||
|
|
||||||
if storage_cert:
|
if storage_cert:
|
||||||
cp_svc.upsert_storage_cert(db, protocol_id, tenant_no, user_id, **storage_cert)
|
cp_svc.upsert_storage_cert(
|
||||||
|
db, protocol_id, tenant_no, user_id, commit=False, **storage_cert
|
||||||
|
)
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/collection-protocols")
|
@router.post("/collection-protocols")
|
||||||
@@ -306,6 +362,7 @@ async def create_collection_protocol(
|
|||||||
user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
assert_protocol_sensitive_fields(db, user, body, creating=True)
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
user_id = _get_current_user_id(user)
|
user_id = _get_current_user_id(user)
|
||||||
protocol_id: Optional[str] = None
|
protocol_id: Optional[str] = None
|
||||||
@@ -327,6 +384,7 @@ async def create_collection_protocol(
|
|||||||
collector = user_id
|
collector = user_id
|
||||||
obj = cp_svc.create_collection_protocol(
|
obj = cp_svc.create_collection_protocol(
|
||||||
db,
|
db,
|
||||||
|
create_dept_id=get_user_dept_id(db, user_id) or "",
|
||||||
protocol_no=body.protocol_no or "",
|
protocol_no=body.protocol_no or "",
|
||||||
protocol_no_id=body.protocol_no_id,
|
protocol_no_id=body.protocol_no_id,
|
||||||
protocol_status=body.protocol_status,
|
protocol_status=body.protocol_status,
|
||||||
@@ -335,9 +393,15 @@ async def create_collection_protocol(
|
|||||||
health_survey_form_urls=health_survey_form_urls,
|
health_survey_form_urls=health_survey_form_urls,
|
||||||
register_date=body.register_date,
|
register_date=body.register_date,
|
||||||
collector_user_id=collector,
|
collector_user_id=collector,
|
||||||
|
protocol_total_amount=body.protocol_total_amount,
|
||||||
|
source_presign_order_id=body.source_presign_order_id,
|
||||||
|
payment_model_version=body.payment_model_version
|
||||||
|
if body.payment_model_version is not None
|
||||||
|
else 2,
|
||||||
remark=body.remark,
|
remark=body.remark,
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
commit=False,
|
||||||
)
|
)
|
||||||
protocol_id = obj.id
|
protocol_id = obj.id
|
||||||
_persist_protocol_sub_resources(
|
_persist_protocol_sub_resources(
|
||||||
@@ -350,21 +414,28 @@ async def create_collection_protocol(
|
|||||||
samples=body.samples,
|
samples=body.samples,
|
||||||
payment=body.payment,
|
payment=body.payment,
|
||||||
payment_details=body.payment_details,
|
payment_details=body.payment_details,
|
||||||
|
sample_payments=body.sample_payments,
|
||||||
storage_cert=body.storage_cert,
|
storage_cert=body.storage_cert,
|
||||||
|
payment_model_version=int(
|
||||||
|
getattr(obj, "payment_model_version", None) or 2
|
||||||
|
),
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
db.refresh(obj)
|
||||||
|
schedule_sample_collect_notify(
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
protocol_id=protocol_id,
|
||||||
|
protocol_kind=PROTOCOL_KIND_CELL,
|
||||||
|
before_codes=[],
|
||||||
|
submitted_codes=(
|
||||||
|
sample_codes_from_payload(body.samples)
|
||||||
|
if body.samples is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return success_response(cp_svc._protocol_to_dict(obj), message="创建成功")
|
return success_response(cp_svc._protocol_to_dict(obj), message="创建成功")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# 子表分段 commit 后校验失败时,补偿软删主单及样本,释放样本编码占用
|
db.rollback()
|
||||||
if protocol_id:
|
|
||||||
try:
|
|
||||||
cp_svc.soft_delete_collection_protocol(
|
|
||||||
db,
|
|
||||||
protocol_id,
|
|
||||||
tenant_no=tenant_no,
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
@@ -375,6 +446,7 @@ async def update_collection_protocol(
|
|||||||
user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
assert_protocol_sensitive_fields(db, user, body, creating=False)
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
user_id = _get_current_user_id(user)
|
user_id = _get_current_user_id(user)
|
||||||
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
@@ -383,7 +455,7 @@ async def update_collection_protocol(
|
|||||||
payload = body.model_dump(exclude_unset=True, by_alias=False)
|
payload = body.model_dump(exclude_unset=True, by_alias=False)
|
||||||
sub_keys = {
|
sub_keys = {
|
||||||
"party_customer", "collection_info", "samples",
|
"party_customer", "collection_info", "samples",
|
||||||
"payment", "payment_details", "attachments",
|
"payment", "payment_details", "sample_payments", "attachments",
|
||||||
"consent_attachments", "health_survey_attachments", "storage_cert",
|
"consent_attachments", "health_survey_attachments", "storage_cert",
|
||||||
}
|
}
|
||||||
main_payload = {k: v for k, v in payload.items() if k not in sub_keys}
|
main_payload = {k: v for k, v in payload.items() if k not in sub_keys}
|
||||||
@@ -408,12 +480,20 @@ async def update_collection_protocol(
|
|||||||
)
|
)
|
||||||
if resolved is not None:
|
if resolved is not None:
|
||||||
main_payload["health_survey_form_urls"] = resolved
|
main_payload["health_survey_form_urls"] = resolved
|
||||||
|
before_codes = sample_codes_from_orm_rows(
|
||||||
|
cp_svc.list_samples(db, protocol_id, tenant_no)
|
||||||
|
)
|
||||||
obj = cp_svc.update_collection_protocol(
|
obj = cp_svc.update_collection_protocol(
|
||||||
db,
|
db,
|
||||||
protocol_id,
|
protocol_id,
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
**{k: v for k, v in main_payload.items() if v is not None},
|
commit=False,
|
||||||
|
**{
|
||||||
|
k: v
|
||||||
|
for k, v in main_payload.items()
|
||||||
|
if v is not None or k == "protocol_total_amount"
|
||||||
|
},
|
||||||
)
|
)
|
||||||
_persist_protocol_sub_resources(
|
_persist_protocol_sub_resources(
|
||||||
db,
|
db,
|
||||||
@@ -425,10 +505,28 @@ async def update_collection_protocol(
|
|||||||
samples=body.samples,
|
samples=body.samples,
|
||||||
payment=body.payment,
|
payment=body.payment,
|
||||||
payment_details=body.payment_details,
|
payment_details=body.payment_details,
|
||||||
|
sample_payments=body.sample_payments,
|
||||||
storage_cert=body.storage_cert,
|
storage_cert=body.storage_cert,
|
||||||
|
payment_model_version=int(
|
||||||
|
getattr(obj, "payment_model_version", None) or 1
|
||||||
|
),
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
db.refresh(obj)
|
||||||
|
schedule_sample_collect_notify(
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
protocol_id=protocol_id,
|
||||||
|
protocol_kind=PROTOCOL_KIND_CELL,
|
||||||
|
before_codes=before_codes,
|
||||||
|
submitted_codes=(
|
||||||
|
sample_codes_from_payload(body.samples)
|
||||||
|
if body.samples is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return success_response(cp_svc._protocol_to_dict(obj), message="更新成功")
|
return success_response(cp_svc._protocol_to_dict(obj), message="更新成功")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
db.rollback()
|
||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
@@ -499,6 +597,8 @@ async def submit_collection_protocol_audit(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(403, "无权访问该协议或协议不存在")
|
||||||
try:
|
try:
|
||||||
cp_svc.create_audit(
|
cp_svc.create_audit(
|
||||||
db,
|
db,
|
||||||
@@ -515,6 +615,23 @@ async def submit_collection_protocol_audit(
|
|||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/collection-protocols/{protocol_id}/status-changes")
|
||||||
|
async def list_collection_protocol_status_changes(
|
||||||
|
protocol_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(403, "无权访问该协议或协议不存在")
|
||||||
|
items = cp_svc.list_protocol_status_changes(
|
||||||
|
db,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
)
|
||||||
|
return success_response({"list": items})
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 收款确认
|
# 收款确认
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1089,6 +1206,94 @@ async def update_fullmoon_todo_insurance(
|
|||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class StorageCertHandleBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
handle_time: Optional[str] = Field(None, alias="handleTime")
|
||||||
|
handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
|
||||||
|
cert_image_urls: Optional[str] = Field(None, alias="certImageUrls")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FullmoonHandleBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
handle_time: Optional[str] = Field(None, alias="handleTime")
|
||||||
|
handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
|
||||||
|
insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
|
||||||
|
handle_remark: Optional[str] = Field(None, alias="handleRemark")
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_handler_name(db: Session, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
uid = (payload.get("handlerUserId") or "").strip()
|
||||||
|
name = ""
|
||||||
|
if uid:
|
||||||
|
u = db.query(SysUser).filter(SysUser.id == uid).first()
|
||||||
|
name = (u.nick_name or "") if u else ""
|
||||||
|
payload["handlerUserName"] = name
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/storage-certs/{protocol_id}")
|
||||||
|
async def update_storage_cert_handle(
|
||||||
|
protocol_id: str,
|
||||||
|
body: StorageCertHandleBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(404, "协议单不存在")
|
||||||
|
try:
|
||||||
|
ht = parse_datetime(body.handle_time) if body.handle_time else None
|
||||||
|
sc = cp_svc.upsert_storage_cert_handle(
|
||||||
|
db,
|
||||||
|
protocol_id,
|
||||||
|
tenant_no,
|
||||||
|
user_id,
|
||||||
|
handle_time=ht,
|
||||||
|
handler_user_id=body.handler_user_id,
|
||||||
|
cert_image_urls=body.cert_image_urls,
|
||||||
|
remark=body.remark,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
_attach_handler_name(db, cp_svc._storage_cert_to_dict(sc)), message="更新成功"
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/fullmoon-todos/{protocol_id}/handle")
|
||||||
|
async def update_fullmoon_todo_handle(
|
||||||
|
protocol_id: str,
|
||||||
|
body: FullmoonHandleBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(404, "协议单不存在")
|
||||||
|
try:
|
||||||
|
ht = parse_datetime(body.handle_time) if body.handle_time else None
|
||||||
|
ft = cp_svc.upsert_fullmoon_todo_handle(
|
||||||
|
db,
|
||||||
|
protocol_id,
|
||||||
|
tenant_no,
|
||||||
|
user_id,
|
||||||
|
handle_time=ht,
|
||||||
|
handler_user_id=body.handler_user_id,
|
||||||
|
insurance_attachments=body.insurance_attachments,
|
||||||
|
handle_remark=body.handle_remark,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
_attach_handler_name(db, cp_svc._fullmoon_todo_to_dict(ft)), message="更新成功"
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
class InvoiceRequestCreateBody(BaseModel):
|
class InvoiceRequestCreateBody(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""客户服务字典 API"""
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.menu_guard import require_any_menu_code
|
||||||
|
from app.core.response import success_response
|
||||||
|
from app.crud import customer_service_dict as svc
|
||||||
|
from app.models.models import SysUser
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["客户服务字典"])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/customer-service-dict")
|
||||||
|
async def get_customer_service_dict(
|
||||||
|
dictType: str = Query(...),
|
||||||
|
user: dict = Depends(require_any_menu_code("cell-storage:sample-info:list")),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
values = svc.list_dict_values(db, tenant_no, dictType)
|
||||||
|
return success_response({"list": values})
|
||||||
@@ -139,22 +139,12 @@ async def submit_leave(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/callback")
|
@router.post("/callback")
|
||||||
async def leave_workflow_callback(
|
async def leave_workflow_callback_blocked():
|
||||||
body: dict,
|
"""HTTP 回调已禁用:请假审批结果仅允许工作流本地回调(register_local_callback)。"""
|
||||||
db: Session = Depends(get_db),
|
return error_response(
|
||||||
):
|
403,
|
||||||
logger.info("请假工作流回调: %s", body)
|
"请假审批回调仅支持进程内本地回调,请勿通过 HTTP 调用本接口",
|
||||||
business_id = body.get("businessId", "")
|
)
|
||||||
event = body.get("event", "")
|
|
||||||
if not business_id:
|
|
||||||
return success_response({"received": True})
|
|
||||||
|
|
||||||
if event == "completed":
|
|
||||||
leave_crud.update_leave_status(db, business_id, "approved")
|
|
||||||
elif event == "rejected":
|
|
||||||
leave_crud.update_leave_status(db, business_id, "rejected")
|
|
||||||
db.commit()
|
|
||||||
return success_response({"received": True})
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{leave_id}")
|
@router.get("/{leave_id}")
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -171,6 +171,7 @@ class StockCheckCreateBody(BaseModel):
|
|||||||
|
|
||||||
class StockCheckUnusedLineItem(BaseModel):
|
class StockCheckUnusedLineItem(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
usage_id: Optional[str] = Field(None, alias="usageId")
|
usage_id: Optional[str] = Field(None, alias="usageId")
|
||||||
batch_id: Optional[str] = Field(None, alias="batchId")
|
batch_id: Optional[str] = Field(None, alias="batchId")
|
||||||
material_code: Optional[str] = Field(None, alias="materialCode")
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
||||||
|
|||||||
+67
-10
@@ -7,7 +7,7 @@ from app.core.response import success_response
|
|||||||
router = APIRouter(prefix="/web/menu", tags=["菜单管理"])
|
router = APIRouter(prefix="/web/menu", tags=["菜单管理"])
|
||||||
|
|
||||||
# 与 web-admin/src/router/modules/sqxp.ts 一致:5 个一级菜单分组
|
# 与 web-admin/src/router/modules/sqxp.ts 一致:5 个一级菜单分组
|
||||||
# 市场二级顺序:细胞存储、科研捐赠、市场基础信息、中试服务、基础实验
|
# 市场二级顺序:存储服务、科研捐赠、市场基础信息、中试服务、基础实验
|
||||||
STATIC_MENU_TREE = [
|
STATIC_MENU_TREE = [
|
||||||
# ────────────────────────────────────────────
|
# ────────────────────────────────────────────
|
||||||
# 1. 市场 (menu:market)
|
# 1. 市场 (menu:market)
|
||||||
@@ -26,14 +26,14 @@ STATIC_MENU_TREE = [
|
|||||||
"roles": ["admin"],
|
"roles": ["admin"],
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
# 细胞存储
|
# 存储服务(含细胞存储 / 科研捐赠统一业务入口)
|
||||||
{
|
{
|
||||||
"id": "CEL00000000000000000000000000001",
|
"id": "CEL00000000000000000000000000001",
|
||||||
"name": "CellStorage",
|
"name": "CellStorage",
|
||||||
"path": "cell-storage",
|
"path": "cell-storage",
|
||||||
"component": "",
|
"component": "",
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "细胞存储",
|
"title": "存储服务",
|
||||||
"icon": "ri:test-tube-line",
|
"icon": "ri:test-tube-line",
|
||||||
"keepAlive": False,
|
"keepAlive": False,
|
||||||
"roles": ["admin"],
|
"roles": ["admin"],
|
||||||
@@ -53,19 +53,70 @@ STATIC_MENU_TREE = [
|
|||||||
"component": "/cell-storage/protocol-audit/index",
|
"component": "/cell-storage/protocol-audit/index",
|
||||||
"meta": {"title": "采集协议单", "keepAlive": True, "menuCode": "biz:cellStorageProtocolAudit:list"},
|
"meta": {"title": "采集协议单", "keepAlive": True, "menuCode": "biz:cellStorageProtocolAudit:list"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "50000000000000000000000000000020",
|
||||||
|
"name": "CellStoragePresignOrder",
|
||||||
|
"path": "presign-order",
|
||||||
|
"component": "/cell-storage/presign-order/index",
|
||||||
|
"meta": {"title": "预签约订单", "keepAlive": True, "menuCode": "cell-storage:presign-order:list"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "904b5173519411f1944300163e3bdd96",
|
||||||
|
"name": "CellStoragePaymentInfo",
|
||||||
|
"path": "payment-info",
|
||||||
|
"component": "/cell-storage/payment-info/index",
|
||||||
|
"meta": {
|
||||||
|
"title": "缴费信息",
|
||||||
|
"keepAlive": True,
|
||||||
|
"menuCode": "cellStorage:paymentInfo:list",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "50000000000000000000000000000022",
|
||||||
|
"name": "CellStoragePaymentChase",
|
||||||
|
"path": "payment-chase",
|
||||||
|
"component": "/cell-storage/payment-chase/index",
|
||||||
|
"meta": {
|
||||||
|
"title": "催缴信息",
|
||||||
|
"keepAlive": True,
|
||||||
|
"menuCode": "cellStorage:paymentChase:list",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "50000000000000000000000000000013",
|
"id": "50000000000000000000000000000013",
|
||||||
"name": "CellStorageSampleInfo",
|
"name": "CellStorageSampleInfo",
|
||||||
"path": "sample-info",
|
"path": "sample-info",
|
||||||
"component": "/cell-storage/sample-info/index",
|
"component": "/cell-storage/sample-info/index",
|
||||||
"meta": {"title": "样本信息管理", "keepAlive": True, "menuCode": "cell-storage:sample-info:list"},
|
"meta": {"title": "客户服务管理", "keepAlive": True, "menuCode": "cell-storage:sample-info:list"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "50000000000000000000000000000021",
|
||||||
|
"name": "CellStorageSampleFreight",
|
||||||
|
"path": "sample-freight",
|
||||||
|
"component": "/cell-storage/sample-freight/index",
|
||||||
|
"meta": {
|
||||||
|
"title": "运费信息",
|
||||||
|
"keepAlive": True,
|
||||||
|
"menuCode": "cell-storage:sample-freight:list",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "50000000000000000000000000000023",
|
||||||
|
"name": "CellStorageSfExpressSettlement",
|
||||||
|
"path": "sf-express-settlement",
|
||||||
|
"component": "/cell-storage/sf-express-settlement/index",
|
||||||
|
"meta": {
|
||||||
|
"title": "顺丰月结",
|
||||||
|
"keepAlive": True,
|
||||||
|
"menuCode": "cell-storage:sf-express-settlement:list",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "50000000000000000000000000000004",
|
"id": "50000000000000000000000000000004",
|
||||||
"name": "CellStoragePaymentConfirm",
|
"name": "CellStoragePaymentConfirm",
|
||||||
"path": "payment-confirm",
|
"path": "payment-confirm",
|
||||||
"component": "/cell-storage/payment-confirm/index",
|
"component": "/cell-storage/payment-confirm/index",
|
||||||
"meta": {"title": "缴退费财务确认及开票", "keepAlive": True, "menuCode": "biz:cellStoragePaymentConfirm:list"},
|
"meta": {"title": "缴费财务确认", "keepAlive": True, "menuCode": "biz:cellStoragePaymentConfirm:list"},
|
||||||
},
|
},
|
||||||
# 市场侧入库放行审核(总览)暂下线:改由生产/质检入口审核
|
# 市场侧入库放行审核(总览)暂下线:改由生产/质检入口审核
|
||||||
# {
|
# {
|
||||||
@@ -88,6 +139,7 @@ STATIC_MENU_TREE = [
|
|||||||
"title": "科研捐赠",
|
"title": "科研捐赠",
|
||||||
"icon": "ri:hand-heart-line",
|
"icon": "ri:hand-heart-line",
|
||||||
"keepAlive": False,
|
"keepAlive": False,
|
||||||
|
"isHide": True,
|
||||||
"roles": ["admin"],
|
"roles": ["admin"],
|
||||||
},
|
},
|
||||||
"children": [
|
"children": [
|
||||||
@@ -103,14 +155,19 @@ STATIC_MENU_TREE = [
|
|||||||
"name": "ResearchDonationAudit",
|
"name": "ResearchDonationAudit",
|
||||||
"path": "audit",
|
"path": "audit",
|
||||||
"component": "/research-donation/audit/index",
|
"component": "/research-donation/audit/index",
|
||||||
"meta": {"title": "捐赠协议单", "keepAlive": True, "menuCode": "biz:researchDonationAudit:list"},
|
"meta": {"title": "捐赠协议单", "keepAlive": True, "isHide": True, "menuCode": "biz:researchDonationAudit:list"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "60000000000000000000000000000004",
|
"id": "60000000000000000000000000000004",
|
||||||
"name": "ResearchDonationSampleInfo",
|
"name": "ResearchDonationSampleInfo",
|
||||||
"path": "sample-info",
|
"path": "sample-info",
|
||||||
"component": "/research-donation/sample-info/index",
|
"component": "/research-donation/sample-info/index",
|
||||||
"meta": {"title": "样本信息管理", "keepAlive": True, "menuCode": "research-donation:sample-info:list"},
|
"meta": {
|
||||||
|
"title": "样本信息管理",
|
||||||
|
"keepAlive": True,
|
||||||
|
"isHide": True,
|
||||||
|
"menuCode": "research-donation:sample-info:list",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -149,7 +206,7 @@ STATIC_MENU_TREE = [
|
|||||||
"name": "BasicInfoInsuranceCompany",
|
"name": "BasicInfoInsuranceCompany",
|
||||||
"path": "insurance-company",
|
"path": "insurance-company",
|
||||||
"component": "/basic-info/insurance-company/index",
|
"component": "/basic-info/insurance-company/index",
|
||||||
"meta": {"title": "保险公司及险种价格", "keepAlive": True, "menuCode": "basic-info:insurance-company:list"},
|
"meta": {"title": "保险公司及险种价格", "keepAlive": True, "isHide": True, "menuCode": "basic-info:insurance-company:list"},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -177,7 +234,7 @@ STATIC_MENU_TREE = [
|
|||||||
"name": "BizCustomer",
|
"name": "BizCustomer",
|
||||||
"path": "customer",
|
"path": "customer",
|
||||||
"component": "/basic-info/customer/index",
|
"component": "/basic-info/customer/index",
|
||||||
"meta": {"title": "客户管理", "keepAlive": True, "menuCode": "biz:customer:list"},
|
"meta": {"title": "客户基础信息", "keepAlive": True, "menuCode": "biz:customer:list"},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -193,7 +250,7 @@ STATIC_MENU_TREE = [
|
|||||||
"name": "CheckupInstitution",
|
"name": "CheckupInstitution",
|
||||||
"path": "checkup-institution",
|
"path": "checkup-institution",
|
||||||
"component": "/pilot-service/checkup-institution/index",
|
"component": "/pilot-service/checkup-institution/index",
|
||||||
"meta": {"title": "体检机构及项目价格", "keepAlive": True, "menuCode": "pilot-service:checkup-institution:list"},
|
"meta": {"title": "体检机构及项目价格", "keepAlive": True, "isHide": True, "menuCode": "pilot-service:checkup-institution:list"},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from fastapi import APIRouter, Depends, Request
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.auth import get_current_user_by_token
|
from app.core.auth import get_current_user_by_token
|
||||||
from app.core.config import settings
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.response import error_response, success_response
|
from app.core.response import error_response, success_response
|
||||||
from app.models.models import SysMenu, SysRoleMenu
|
from app.models.models import SysMenu, SysRoleMenu
|
||||||
@@ -24,14 +23,13 @@ async def _current_user(request: Request, db: Session) -> dict:
|
|||||||
return await get_current_user_by_token(token=token, db=db)
|
return await get_current_user_by_token(token=token, db=db)
|
||||||
|
|
||||||
|
|
||||||
def _find_mobile_root(db: Session) -> SysMenu | None:
|
def _find_mobile_root(db: Session, tenant_no: str) -> SysMenu | None:
|
||||||
tenant = settings.DEFAULT_TENANT_NO
|
|
||||||
rows = (
|
rows = (
|
||||||
db.query(SysMenu)
|
db.query(SysMenu)
|
||||||
.filter(
|
.filter(
|
||||||
SysMenu.del_flag == 0,
|
SysMenu.del_flag == 0,
|
||||||
SysMenu.parent_id == "0",
|
SysMenu.parent_id == "0",
|
||||||
SysMenu.tenant_no == tenant,
|
SysMenu.tenant_no == tenant_no,
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@@ -42,13 +40,14 @@ def _find_mobile_root(db: Session) -> SysMenu | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _collect_descendants(db: Session, root_id: str) -> List[SysMenu]:
|
def _collect_descendants(
|
||||||
tenant = settings.DEFAULT_TENANT_NO
|
db: Session, root_id: str, tenant_no: str
|
||||||
|
) -> List[SysMenu]:
|
||||||
all_menus = (
|
all_menus = (
|
||||||
db.query(SysMenu)
|
db.query(SysMenu)
|
||||||
.filter(
|
.filter(
|
||||||
SysMenu.del_flag == 0,
|
SysMenu.del_flag == 0,
|
||||||
SysMenu.tenant_no == tenant,
|
SysMenu.tenant_no == tenant_no,
|
||||||
SysMenu.status == "1",
|
SysMenu.status == "1",
|
||||||
SysMenu.visible == "1",
|
SysMenu.visible == "1",
|
||||||
)
|
)
|
||||||
@@ -71,16 +70,15 @@ def _collect_descendants(db: Session, root_id: str) -> List[SysMenu]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _user_menu_ids(db: Session, user_id: str) -> Set[str]:
|
def _user_menu_ids(db: Session, user_id: str, tenant_no: str) -> Set[str]:
|
||||||
role_ids = get_effective_role_ids(db, user_id)
|
role_ids = get_effective_role_ids(db, user_id, tenant_no=tenant_no)
|
||||||
if not role_ids:
|
if not role_ids:
|
||||||
return set()
|
return set()
|
||||||
tenant = settings.DEFAULT_TENANT_NO
|
|
||||||
rows = (
|
rows = (
|
||||||
db.query(SysRoleMenu.menu_id)
|
db.query(SysRoleMenu.menu_id)
|
||||||
.filter(
|
.filter(
|
||||||
SysRoleMenu.del_flag == 0,
|
SysRoleMenu.del_flag == 0,
|
||||||
SysRoleMenu.tenant_no == tenant,
|
SysRoleMenu.tenant_no == tenant_no,
|
||||||
SysRoleMenu.role_id.in_(role_ids),
|
SysRoleMenu.role_id.in_(role_ids),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
@@ -91,6 +89,20 @@ def _user_menu_ids(db: Session, user_id: str) -> Set[str]:
|
|||||||
def _mobile_menu_icon(path: str) -> str:
|
def _mobile_menu_icon(path: str) -> str:
|
||||||
"""按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。"""
|
"""按路由匹配移动端宫格图标(与 mobile-h5 van-icon 字形名对齐)。"""
|
||||||
p = (path or "").lower()
|
p = (path or "").lower()
|
||||||
|
# 兼容有无前导斜杠、query 分隔写法
|
||||||
|
if "market/webview" in p and "entry=" in p:
|
||||||
|
# payment-chase 须先于 payment 匹配
|
||||||
|
if "payment-chase" in p:
|
||||||
|
return "market-payment-chase"
|
||||||
|
if "presign" in p:
|
||||||
|
return "market-presign"
|
||||||
|
if "protocol" in p:
|
||||||
|
return "market-protocol"
|
||||||
|
if "payment" in p:
|
||||||
|
return "market-payment"
|
||||||
|
if "freight" in p:
|
||||||
|
return "market-freight"
|
||||||
|
return ""
|
||||||
if "oa/start" in p:
|
if "oa/start" in p:
|
||||||
return "approve"
|
return "approve"
|
||||||
if "oa/drafts" in p:
|
if "oa/drafts" in p:
|
||||||
@@ -115,7 +127,7 @@ def _mobile_menu_icon(path: str) -> str:
|
|||||||
def _mobile_menu_icon_colors(path: str) -> dict:
|
def _mobile_menu_icon_colors(path: str) -> dict:
|
||||||
"""宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。"""
|
"""宫格图标底色/图形色(可按入口自定义,不强制品牌绿)。"""
|
||||||
p = (path or "").lower()
|
p = (path or "").lower()
|
||||||
# OA 菜单 PNG 自带颜色,宫格不套色底
|
# OA / 市场菜单 PNG 自带颜色,宫格不套色底
|
||||||
if any(
|
if any(
|
||||||
x in p
|
x in p
|
||||||
for x in (
|
for x in (
|
||||||
@@ -128,13 +140,61 @@ def _mobile_menu_icon_colors(path: str) -> dict:
|
|||||||
"oa/mentions",
|
"oa/mentions",
|
||||||
"oa/wechat-notify",
|
"oa/wechat-notify",
|
||||||
)
|
)
|
||||||
):
|
) or ("market/webview" in p and "entry=" in p):
|
||||||
return {"iconBg": "transparent", "iconFg": ""}
|
return {"iconBg": "transparent", "iconFg": ""}
|
||||||
if "form-sink" in p:
|
if "form-sink" in p:
|
||||||
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
||||||
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
return {"iconBg": "#eef2f1", "iconFg": "#3d524a"}
|
||||||
|
|
||||||
|
|
||||||
|
def list_mobile_menu_items(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
user_id: str,
|
||||||
|
tenant_no: str,
|
||||||
|
is_super_admin: bool = False,
|
||||||
|
) -> List[dict]:
|
||||||
|
"""列出当前租户下已授权的移动端叶子入口。"""
|
||||||
|
tenant = (tenant_no or "").strip()
|
||||||
|
uid = (user_id or "").strip()
|
||||||
|
if not tenant or not uid:
|
||||||
|
return []
|
||||||
|
|
||||||
|
root = _find_mobile_root(db, tenant)
|
||||||
|
if not root:
|
||||||
|
return []
|
||||||
|
|
||||||
|
descendants = _collect_descendants(db, root.id, tenant)
|
||||||
|
by_id = {menu.id: menu for menu in descendants}
|
||||||
|
allowed_ids = None if is_super_admin else _user_menu_ids(db, uid, tenant)
|
||||||
|
items: List[dict] = []
|
||||||
|
for menu in descendants:
|
||||||
|
if (menu.menu_type or "").upper() == "F":
|
||||||
|
continue
|
||||||
|
path = (menu.path or "").strip()
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
if "form-sink" in path.lower() or "水槽" in str(menu.menu_name or ""):
|
||||||
|
continue
|
||||||
|
if allowed_ids is not None and menu.id not in allowed_ids:
|
||||||
|
continue
|
||||||
|
colors = _mobile_menu_icon_colors(path)
|
||||||
|
parent = by_id.get((menu.parent_id or "").strip())
|
||||||
|
item = {
|
||||||
|
"id": menu.id,
|
||||||
|
"title": menu.menu_name,
|
||||||
|
"path": path if path.startswith("/") else f"/{path}",
|
||||||
|
"icon": _mobile_menu_icon(path),
|
||||||
|
"iconBg": colors["iconBg"],
|
||||||
|
"iconFg": colors["iconFg"],
|
||||||
|
"perms": (menu.perms or "").strip() or None,
|
||||||
|
}
|
||||||
|
if parent is not None and parent.id != root.id:
|
||||||
|
item["groupTitle"] = parent.menu_name
|
||||||
|
items.append(item)
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
@router.get("/menus")
|
@router.get("/menus")
|
||||||
async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
|
async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
|
||||||
try:
|
try:
|
||||||
@@ -143,38 +203,14 @@ async def list_my_mobile_menus(request: Request, db: Session = Depends(get_db)):
|
|||||||
return error_response(401, "未授权")
|
return error_response(401, "未授权")
|
||||||
|
|
||||||
user_id = str(user.get("user_id") or user.get("sub") or "").strip()
|
user_id = str(user.get("user_id") or user.get("sub") or "").strip()
|
||||||
if not user_id:
|
tenant_no = str(user.get("tenant_no") or "").strip()
|
||||||
|
if not user_id or not tenant_no:
|
||||||
return error_response(401, "未授权")
|
return error_response(401, "未授权")
|
||||||
|
web_role = resolve_web_role_for_user(db, user_id, tenant_no=tenant_no)
|
||||||
root = _find_mobile_root(db)
|
items = list_mobile_menu_items(
|
||||||
if not root:
|
db,
|
||||||
return success_response({"list": []})
|
user_id=user_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
descendants = _collect_descendants(db, root.id)
|
is_super_admin=web_role == "super_admin",
|
||||||
web_role = resolve_web_role_for_user(db, user_id)
|
)
|
||||||
allowed_ids = None if web_role == "super_admin" else _user_menu_ids(db, user_id)
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for m in descendants:
|
|
||||||
if (m.menu_type or "").upper() == "F":
|
|
||||||
continue
|
|
||||||
path = (m.path or "").strip()
|
|
||||||
if not path:
|
|
||||||
continue
|
|
||||||
if "form-sink" in path.lower() or "水槽" in str(m.menu_name or ""):
|
|
||||||
continue
|
|
||||||
if allowed_ids is not None and m.id not in allowed_ids:
|
|
||||||
continue
|
|
||||||
colors = _mobile_menu_icon_colors(path)
|
|
||||||
items.append(
|
|
||||||
{
|
|
||||||
"id": m.id,
|
|
||||||
"title": m.menu_name,
|
|
||||||
"path": path if path.startswith("/") else f"/{path}",
|
|
||||||
"icon": _mobile_menu_icon(path),
|
|
||||||
"iconBg": colors["iconBg"],
|
|
||||||
"iconFg": colors["iconFg"],
|
|
||||||
"perms": (m.perms or "").strip() or None,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return success_response({"list": items})
|
return success_response({"list": items})
|
||||||
@@ -19,6 +19,7 @@ from app.services.oa_receive_account import normalize_account_fields
|
|||||||
router = APIRouter(prefix="/web/admin/oa-approval", tags=["OA审批"])
|
router = APIRouter(prefix="/web/admin/oa-approval", tags=["OA审批"])
|
||||||
|
|
||||||
OA_START_HISTORY_ALL_PERM = "oa:start:history:all"
|
OA_START_HISTORY_ALL_PERM = "oa:start:history:all"
|
||||||
|
OA_DONE_BANK_RECEIPT_PERM = "oa:done:bank-receipt"
|
||||||
|
|
||||||
|
|
||||||
def _get_tenant_no(db: Session, user: dict) -> str:
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
@@ -83,7 +84,15 @@ async def list_receive_account_options(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
return success_response({"list": ora_crud.list_options(db, tenant_no=tenant_no)})
|
return success_response(
|
||||||
|
{
|
||||||
|
"list": ora_crud.list_options(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user.get("user_id") or "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/receive-accounts/batch-delete")
|
@router.post("/receive-accounts/batch-delete")
|
||||||
@@ -773,6 +782,19 @@ class OaCommentCreateBody(BaseModel):
|
|||||||
mention_user_ids: Optional[List[str]] = Field(None, alias="mentionUserIds")
|
mention_user_ids: Optional[List[str]] = Field(None, alias="mentionUserIds")
|
||||||
|
|
||||||
|
|
||||||
|
class OaBankReceiptItemBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
file_url: str = Field(..., alias="fileUrl")
|
||||||
|
file_name: Optional[str] = Field(None, alias="fileName")
|
||||||
|
file_type: Optional[str] = Field(None, alias="fileType")
|
||||||
|
file_size: Optional[int] = Field(None, alias="fileSize")
|
||||||
|
|
||||||
|
|
||||||
|
class OaBankReceiptReplaceBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
items: List[OaBankReceiptItemBody] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class OaResubmitBody(BaseModel):
|
class OaResubmitBody(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
|
form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
|
||||||
@@ -914,6 +936,7 @@ async def list_oa_instance_history(
|
|||||||
applyTimeTo: str = Query(""),
|
applyTimeTo: str = Query(""),
|
||||||
finishTimeFrom: str = Query(""),
|
finishTimeFrom: str = Query(""),
|
||||||
finishTimeTo: str = Query(""),
|
finishTimeTo: str = Query(""),
|
||||||
|
initiatorDeptId: str = Query(""),
|
||||||
page: int = Query(1),
|
page: int = Query(1),
|
||||||
pageSize: int = Query(20, alias="pageSize"),
|
pageSize: int = Query(20, alias="pageSize"),
|
||||||
user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
@@ -938,6 +961,7 @@ async def list_oa_instance_history(
|
|||||||
finish_time_from=finishTimeFrom or None,
|
finish_time_from=finishTimeFrom or None,
|
||||||
finish_time_to=finishTimeTo or None,
|
finish_time_to=finishTimeTo or None,
|
||||||
instance_status=instanceStatus or None,
|
instance_status=instanceStatus or None,
|
||||||
|
initiator_dept_id=initiatorDeptId or None,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=pageSize,
|
page_size=pageSize,
|
||||||
)
|
)
|
||||||
@@ -957,6 +981,7 @@ async def export_oa_instance_history(
|
|||||||
applyTimeTo: str = Query(""),
|
applyTimeTo: str = Query(""),
|
||||||
finishTimeFrom: str = Query(""),
|
finishTimeFrom: str = Query(""),
|
||||||
finishTimeTo: str = Query(""),
|
finishTimeTo: str = Query(""),
|
||||||
|
initiatorDeptId: str = Query(""),
|
||||||
user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -981,6 +1006,7 @@ async def export_oa_instance_history(
|
|||||||
finish_time_from=finishTimeFrom or None,
|
finish_time_from=finishTimeFrom or None,
|
||||||
finish_time_to=finishTimeTo or None,
|
finish_time_to=finishTimeTo or None,
|
||||||
instance_status=instanceStatus or None,
|
instance_status=instanceStatus or None,
|
||||||
|
initiator_dept_id=initiatorDeptId or None,
|
||||||
)
|
)
|
||||||
return success_response(data)
|
return success_response(data)
|
||||||
except OaEngineError as e:
|
except OaEngineError as e:
|
||||||
@@ -1028,6 +1054,57 @@ async def list_oa_instance_form_attachments(
|
|||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/instances/{instance_id}/bank-receipts")
|
||||||
|
async def list_oa_bank_receipts(
|
||||||
|
instance_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
from app.core.menu_guard import user_has_menu_perm
|
||||||
|
from app.services import oa_bank_receipt as br
|
||||||
|
|
||||||
|
if not user_has_menu_perm(db, user, OA_DONE_BANK_RECEIPT_PERM):
|
||||||
|
return error_response(403, "无权上传银行回执单")
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items = br.list_bank_receipts(db, instance_id=instance_id, tenant_no=tenant_no)
|
||||||
|
return success_response({"items": items})
|
||||||
|
except br.OaBankReceiptError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/instances/{instance_id}/bank-receipts")
|
||||||
|
async def replace_oa_bank_receipts(
|
||||||
|
instance_id: str,
|
||||||
|
body: OaBankReceiptReplaceBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
from app.core.menu_guard import user_has_menu_perm
|
||||||
|
from app.services import oa_bank_receipt as br
|
||||||
|
|
||||||
|
if not user_has_menu_perm(db, user, OA_DONE_BANK_RECEIPT_PERM):
|
||||||
|
return error_response(403, "无权上传银行回执单")
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items = br.replace_bank_receipts(
|
||||||
|
db,
|
||||||
|
instance_id=instance_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=[it.model_dump(by_alias=True) for it in (body.items or [])],
|
||||||
|
user_id=user.get("user_id") or "",
|
||||||
|
dept_id=user.get("dept_id") or "",
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return success_response({"items": items})
|
||||||
|
except br.OaBankReceiptError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(400, str(e))
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@router.get("/instances/{instance_id}/comments")
|
@router.get("/instances/{instance_id}/comments")
|
||||||
async def list_oa_instance_comments(
|
async def list_oa_instance_comments(
|
||||||
instance_id: str,
|
instance_id: str,
|
||||||
@@ -1188,6 +1265,46 @@ async def oa_cc_unread_count(
|
|||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/mentions/unread-count")
|
||||||
|
async def oa_mention_unread_count(
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
from app.services import oa_instance_comment as oa_comment
|
||||||
|
from app.services.oa_engine import OaEngineError
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
n = oa_comment.count_mention_unread(
|
||||||
|
db, tenant_no=tenant_no, user_id=user.get("user_id") or ""
|
||||||
|
)
|
||||||
|
return success_response({"count": int(n)})
|
||||||
|
except OaEngineError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/mentions/{instance_id}/read")
|
||||||
|
async def oa_mention_mark_read(
|
||||||
|
instance_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
from app.services import oa_instance_comment as oa_comment
|
||||||
|
from app.services.oa_engine import OaEngineError
|
||||||
|
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
data = oa_comment.mark_mention_instance_read(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user.get("user_id") or "",
|
||||||
|
instance_id=instance_id,
|
||||||
|
)
|
||||||
|
return success_response(data, message="已标记已读")
|
||||||
|
except OaEngineError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tasks/cc/read-batch")
|
@router.post("/tasks/cc/read-batch")
|
||||||
async def oa_cc_read_batch(
|
async def oa_cc_read_batch(
|
||||||
body: OaCcReadBatchBody,
|
body: OaCcReadBatchBody,
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ class ScheduleProductNestedBody(BaseModel):
|
|||||||
project_name: Optional[str] = Field(None, alias="projectName")
|
project_name: Optional[str] = Field(None, alias="projectName")
|
||||||
cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
|
cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
|
||||||
spec_text: Optional[str] = Field(None, alias="specText")
|
spec_text: Optional[str] = Field(None, alias="specText")
|
||||||
|
package_id: Optional[str] = Field(None, alias="packageId")
|
||||||
|
package_text: Optional[str] = Field(None, alias="packageText")
|
||||||
agreement_price: Optional[float] = Field(None, alias="agreementPrice")
|
agreement_price: Optional[float] = Field(None, alias="agreementPrice")
|
||||||
actual_price: Optional[float] = Field(None, alias="actualPrice")
|
actual_price: Optional[float] = Field(None, alias="actualPrice")
|
||||||
quantity: Optional[int] = 1
|
quantity: Optional[int] = 1
|
||||||
@@ -167,6 +169,8 @@ class InfusionScheduleNestedBody(BaseModel):
|
|||||||
infusion_location: Optional[str] = Field(None, alias="infusionLocation")
|
infusion_location: Optional[str] = Field(None, alias="infusionLocation")
|
||||||
infusion_method: Optional[str] = Field(None, alias="infusionMethod")
|
infusion_method: Optional[str] = Field(None, alias="infusionMethod")
|
||||||
amount: Optional[float] = None
|
amount: Optional[float] = None
|
||||||
|
package_id: Optional[str] = Field(None, alias="packageId")
|
||||||
|
package_text: Optional[str] = Field(None, alias="packageText")
|
||||||
cell_products: Optional[list[ScheduleProductNestedBody]] = Field(None, alias="cellProducts")
|
cell_products: Optional[list[ScheduleProductNestedBody]] = Field(None, alias="cellProducts")
|
||||||
|
|
||||||
|
|
||||||
@@ -500,6 +504,8 @@ class SaleScheduleProductLineBody(BaseModel):
|
|||||||
project_name: Optional[str] = Field(None, alias="projectName")
|
project_name: Optional[str] = Field(None, alias="projectName")
|
||||||
cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
|
cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
|
||||||
spec_text: Optional[str] = Field(None, alias="specText")
|
spec_text: Optional[str] = Field(None, alias="specText")
|
||||||
|
package_id: Optional[str] = Field(None, alias="packageId")
|
||||||
|
package_text: Optional[str] = Field(None, alias="packageText")
|
||||||
agreement_price: Optional[float] = Field(None, alias="agreementPrice")
|
agreement_price: Optional[float] = Field(None, alias="agreementPrice")
|
||||||
actual_price: Optional[float] = Field(None, alias="actualPrice")
|
actual_price: Optional[float] = Field(None, alias="actualPrice")
|
||||||
quantity: Optional[int] = 1
|
quantity: Optional[int] = 1
|
||||||
|
|||||||
@@ -228,6 +228,108 @@ async def delete_cell_product_spec(
|
|||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2b. 细胞产品包装
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class CellProductPackageCreateBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
package_code: str = Field(..., alias="packageCode")
|
||||||
|
package_name: str = Field(..., alias="packageName")
|
||||||
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CellProductPackageUpdateBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
package_code: Optional[str] = Field(None, alias="packageCode")
|
||||||
|
package_name: Optional[str] = Field(None, alias="packageName")
|
||||||
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/cell-product-packages")
|
||||||
|
async def list_cell_product_packages(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = ps_svc.list_cell_product_packages(
|
||||||
|
db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
|
||||||
|
)
|
||||||
|
tp = ps_svc.total_pages(total, pageSize)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cell-product-packages")
|
||||||
|
async def create_cell_product_package(
|
||||||
|
body: CellProductPackageCreateBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
obj = ps_svc.create_cell_product_package(
|
||||||
|
db,
|
||||||
|
package_code=body.package_code,
|
||||||
|
package_name=body.package_name,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
order_num=body.order_num,
|
||||||
|
remark=body.remark,
|
||||||
|
user_id=user.get("user_id"),
|
||||||
|
)
|
||||||
|
return success_response(ps_svc._row_to_dict_cell_product_package(obj), message="创建成功")
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/cell-product-packages/{row_id}")
|
||||||
|
async def update_cell_product_package(
|
||||||
|
row_id: str,
|
||||||
|
body: CellProductPackageUpdateBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
||||||
|
obj = ps_svc.update_cell_product_package(
|
||||||
|
db,
|
||||||
|
row_id,
|
||||||
|
package_code=payload.get("package_code"),
|
||||||
|
package_name=payload.get("package_name"),
|
||||||
|
order_num=payload.get("order_num"),
|
||||||
|
remark=payload.get("remark"),
|
||||||
|
user_id=user.get("user_id"),
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
)
|
||||||
|
return success_response(ps_svc._row_to_dict_cell_product_package(obj), message="更新成功")
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/cell-product-packages/{row_id}")
|
||||||
|
async def delete_cell_product_package(
|
||||||
|
row_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
ps_svc.soft_delete_cell_product_package(db, row_id, tenant_no=tenant_no)
|
||||||
|
return success_response(None, message="删除成功")
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3. 细胞产品价格
|
# 3. 细胞产品价格
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,448 @@
|
|||||||
|
"""预签约订单 / 意向金转账 / 转正式 API"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.data_scope import get_user_dept_id
|
||||||
|
from app.core.menu_guard import require_admin_or_super_admin
|
||||||
|
from app.core.response import error_response, success_response
|
||||||
|
from app.crud import presign_order as po_svc
|
||||||
|
from app.models.models import SysUser
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["细胞存储-预签约订单"])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class PresignOrderBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
order_date: Optional[str] = Field(None, alias="orderDate")
|
||||||
|
protocol_no: Optional[str] = Field(None, alias="protocolNo")
|
||||||
|
mother_name: Optional[str] = Field(None, alias="motherName")
|
||||||
|
expected_delivery_date: Optional[str] = Field(None, alias="expectedDeliveryDate")
|
||||||
|
region_id: Optional[str] = Field(None, alias="regionId")
|
||||||
|
hospital_id: Optional[str] = Field(None, alias="hospitalId")
|
||||||
|
sample_collect_time: Optional[str] = Field(None, alias="sampleCollectTime")
|
||||||
|
pay_mode: Optional[str] = Field(None, alias="payMode")
|
||||||
|
contract_amount: Optional[float] = Field(None, alias="contractAmount")
|
||||||
|
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
|
||||||
|
sales_user_id: Optional[str] = Field(None, alias="salesUserId")
|
||||||
|
region_manager_user_id: Optional[str] = Field(None, alias="regionManagerUserId")
|
||||||
|
order_status: Optional[str] = Field(None, alias="orderStatus")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
sample_type_ids: Optional[List[str]] = Field(None, alias="sampleTypeIds")
|
||||||
|
transfers: Optional[List[Dict[str, Any]]] = None
|
||||||
|
|
||||||
|
@field_validator("order_date", "expected_delivery_date")
|
||||||
|
@classmethod
|
||||||
|
def validate_ymd(cls, value: Optional[str]) -> Optional[str]:
|
||||||
|
if value in (None, ""):
|
||||||
|
return value
|
||||||
|
text = value.strip()
|
||||||
|
if len(text) != 8 or not text.isdigit():
|
||||||
|
raise ValueError("日期必须使用 YYYYMMDD 格式")
|
||||||
|
try:
|
||||||
|
datetime.strptime(text, "%Y%m%d")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError("日期必须是有效的 YYYYMMDD") from exc
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class PresignTransferBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
transfer_amount: Optional[float] = Field(None, alias="transferAmount")
|
||||||
|
transfer_time: Optional[str] = Field(None, alias="transferTime")
|
||||||
|
transfer_type: Optional[str] = Field(None, alias="transferType")
|
||||||
|
transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceConfirmBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
finance_confirm_attachments: Optional[str] = Field(
|
||||||
|
None, alias="financeConfirmAttachments"
|
||||||
|
)
|
||||||
|
finance_remark: Optional[str] = Field(None, alias="financeRemark")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/presign-orders")
|
||||||
|
async def list_presign_orders(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
orderStatus: Optional[str] = Query(None, alias="orderStatus"),
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = po_svc.list_presign_orders(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
page=page,
|
||||||
|
page_size=pageSize,
|
||||||
|
keyword=keyword,
|
||||||
|
order_status=orderStatus,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": pageSize}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/presign-orders/{order_id}")
|
||||||
|
async def get_presign_order(
|
||||||
|
order_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(db, order_id, tenant_no, user)
|
||||||
|
data = po_svc.get_presign_order(db, order_id=order_id, tenant_no=tenant_no)
|
||||||
|
if not data:
|
||||||
|
return error_response(404, "预签约订单不存在")
|
||||||
|
return success_response(data)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/presign-orders")
|
||||||
|
async def create_presign_order(
|
||||||
|
body: PresignOrderBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
dumped = body.model_dump(by_alias=False)
|
||||||
|
obj = po_svc.create_presign_order(
|
||||||
|
db,
|
||||||
|
create_dept_id=get_user_dept_id(db, user_id) or "",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
fields=dumped,
|
||||||
|
sample_type_ids=body.sample_type_ids,
|
||||||
|
transfers=body.transfers,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc.order_to_dict(db, obj, tenant_no, with_children=True),
|
||||||
|
message="创建成功",
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/presign-orders/{order_id}")
|
||||||
|
async def update_presign_order(
|
||||||
|
order_id: str,
|
||||||
|
body: PresignOrderBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(db, order_id, tenant_no, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
dumped = body.model_dump(by_alias=False, exclude_unset=True)
|
||||||
|
sample_type_ids = dumped.pop("sample_type_ids", None)
|
||||||
|
dumped.pop("transfers", None)
|
||||||
|
obj = po_svc.update_presign_order(
|
||||||
|
db,
|
||||||
|
create_dept_id=get_user_dept_id(db, user_id) or "",
|
||||||
|
order_id=order_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
fields=dumped,
|
||||||
|
sample_type_ids=sample_type_ids,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc.order_to_dict(db, obj, tenant_no, with_children=True),
|
||||||
|
message="保存成功",
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/presign-orders/{order_id}")
|
||||||
|
async def delete_presign_order(
|
||||||
|
order_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(db, order_id, tenant_no, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
po_svc.soft_delete_presign_order(
|
||||||
|
db, order_id=order_id, tenant_no=tenant_no, user_id=user_id
|
||||||
|
)
|
||||||
|
return success_response(None, message="删除成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/presign-orders/{order_id}/convert")
|
||||||
|
async def convert_presign_order(
|
||||||
|
order_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(db, order_id, tenant_no, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
data = po_svc.convert_to_protocol(
|
||||||
|
db, order_id=order_id, tenant_no=tenant_no, user_id=user_id,
|
||||||
|
create_dept_id=get_user_dept_id(db, user_id) or "",
|
||||||
|
)
|
||||||
|
return success_response(data, message="已转正式协议")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/presign-orders/{order_id}/transfers")
|
||||||
|
async def create_presign_transfer(
|
||||||
|
order_id: str,
|
||||||
|
body: PresignTransferBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(db, order_id, tenant_no, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
obj = po_svc.add_transfer(
|
||||||
|
db,
|
||||||
|
create_dept_id=get_user_dept_id(db, user_id) or "",
|
||||||
|
presign_order_id=order_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
fields=body.model_dump(by_alias=False),
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc._transfer_to_dict(db, obj, tenant_no), message="添加成功"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/presign-transfers/{transfer_id}")
|
||||||
|
async def update_presign_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
body: PresignTransferBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(
|
||||||
|
db, transfer_id, tenant_no, user, transfer=True
|
||||||
|
)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
obj = po_svc.update_transfer_draft(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
fields=body.model_dump(by_alias=False, exclude_unset=True),
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc._transfer_to_dict(db, obj, tenant_no), message="保存成功"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/presign-transfers/{transfer_id}/submit-receipt-finance")
|
||||||
|
async def submit_presign_transfer_finance(
|
||||||
|
transfer_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(
|
||||||
|
db, transfer_id, tenant_no, user, transfer=True
|
||||||
|
)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
obj = po_svc.submit_receipt_finance(
|
||||||
|
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc._transfer_to_dict(db, obj, tenant_no), message="已提交财务确认"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/presign-transfers/{transfer_id}/withdraw")
|
||||||
|
async def withdraw_presign_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(
|
||||||
|
db, transfer_id, tenant_no, user, transfer=True
|
||||||
|
)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
obj = po_svc.withdraw_receipt(
|
||||||
|
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc._transfer_to_dict(db, obj, tenant_no), message="已撤回"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/presign-transfers/receipt-confirm")
|
||||||
|
async def list_presign_receipt_confirm(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = po_svc.list_presign_transfers_for_finance(
|
||||||
|
db,
|
||||||
|
pay_status="jfqrz",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
keyword=keyword,
|
||||||
|
page=page,
|
||||||
|
page_size=pageSize,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": pageSize}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/presign-transfers/receipt-confirmed")
|
||||||
|
async def list_presign_receipt_confirmed(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = po_svc.list_presign_transfers_for_finance(
|
||||||
|
db,
|
||||||
|
pay_status="yjf",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
keyword=keyword,
|
||||||
|
page=page,
|
||||||
|
page_size=pageSize,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": pageSize}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/presign-transfers/{transfer_id}/receipt-confirm")
|
||||||
|
async def confirm_presign_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
body: Optional[FinanceConfirmBody] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(
|
||||||
|
db, transfer_id, tenant_no, user, transfer=True
|
||||||
|
)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
obj = po_svc.confirm_receipt(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
finance_confirm_attachments=body.finance_confirm_attachments if body else None,
|
||||||
|
finance_remark=body.finance_remark if body else None,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc._transfer_to_dict(db, obj, tenant_no), message="确认收款成功"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/presign-transfers/{transfer_id}/receipt-reject")
|
||||||
|
async def reject_presign_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
po_svc.assert_presign_data_access(
|
||||||
|
db, transfer_id, tenant_no, user, transfer=True
|
||||||
|
)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
obj = po_svc.reject_receipt(
|
||||||
|
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
po_svc._transfer_to_dict(db, obj, tenant_no), message="已退回"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
@@ -1277,6 +1277,7 @@ async def delete_consumable_pack(
|
|||||||
|
|
||||||
class ProcessReagentInput(BaseModel):
|
class ProcessReagentInput(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
reagent_code: Optional[str] = Field(None, alias="reagentCode")
|
reagent_code: Optional[str] = Field(None, alias="reagentCode")
|
||||||
reagent_name: Optional[str] = Field(None, alias="reagentName")
|
reagent_name: Optional[str] = Field(None, alias="reagentName")
|
||||||
spec: Optional[str] = None
|
spec: Optional[str] = None
|
||||||
@@ -1288,6 +1289,7 @@ class ProcessReagentInput(BaseModel):
|
|||||||
|
|
||||||
class ProcessConsumableInput(BaseModel):
|
class ProcessConsumableInput(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
consumable_code: Optional[str] = Field(None, alias="consumableCode")
|
consumable_code: Optional[str] = Field(None, alias="consumableCode")
|
||||||
consumable_name: Optional[str] = Field(None, alias="consumableName")
|
consumable_name: Optional[str] = Field(None, alias="consumableName")
|
||||||
spec_model: Optional[str] = Field(None, alias="specModel")
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
||||||
@@ -1299,6 +1301,7 @@ class ProcessConsumableInput(BaseModel):
|
|||||||
|
|
||||||
class ProcessOutputInput(BaseModel):
|
class ProcessOutputInput(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
output_type_id: Optional[str] = Field(None, alias="outputTypeId")
|
output_type_id: Optional[str] = Field(None, alias="outputTypeId")
|
||||||
output_name: Optional[str] = Field(None, alias="outputName")
|
output_name: Optional[str] = Field(None, alias="outputName")
|
||||||
output_barcode: Optional[str] = Field(None, alias="outputBarcode")
|
output_barcode: Optional[str] = Field(None, alias="outputBarcode")
|
||||||
@@ -1307,6 +1310,7 @@ class ProcessOutputInput(BaseModel):
|
|||||||
|
|
||||||
class ProcessDataCollectionInput(BaseModel):
|
class ProcessDataCollectionInput(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
collection_name: str = Field(..., alias="collectionName")
|
collection_name: str = Field(..., alias="collectionName")
|
||||||
unit: Optional[str] = None
|
unit: Optional[str] = None
|
||||||
order_num: Optional[int] = Field(0, alias="orderNum")
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
||||||
@@ -2734,6 +2738,7 @@ async def replace_task_step_reagents(
|
|||||||
for it in body:
|
for it in body:
|
||||||
d = it.model_dump(by_alias=False)
|
d = it.model_dump(by_alias=False)
|
||||||
items_data.append({
|
items_data.append({
|
||||||
|
"id": d.get("id"),
|
||||||
"reagent_code": d.get("material_code"),
|
"reagent_code": d.get("material_code"),
|
||||||
"reagent_name": d.get("material_name"),
|
"reagent_name": d.get("material_name"),
|
||||||
"spec": d.get("spec"),
|
"spec": d.get("spec"),
|
||||||
@@ -2767,6 +2772,7 @@ async def replace_task_step_consumables(
|
|||||||
for it in body:
|
for it in body:
|
||||||
d = it.model_dump(by_alias=False)
|
d = it.model_dump(by_alias=False)
|
||||||
items_data.append({
|
items_data.append({
|
||||||
|
"id": d.get("id"),
|
||||||
"consumable_code": d.get("material_code"),
|
"consumable_code": d.get("material_code"),
|
||||||
"consumable_name": d.get("material_name"),
|
"consumable_name": d.get("material_name"),
|
||||||
"spec_model": d.get("spec_model"),
|
"spec_model": d.get("spec_model"),
|
||||||
@@ -2800,6 +2806,7 @@ async def replace_task_step_configured_reagents(
|
|||||||
for it in body:
|
for it in body:
|
||||||
d = it.model_dump(by_alias=False)
|
d = it.model_dump(by_alias=False)
|
||||||
items_data.append({
|
items_data.append({
|
||||||
|
"id": d.get("id"),
|
||||||
"configured_reagent_code": d.get("material_code"),
|
"configured_reagent_code": d.get("material_code"),
|
||||||
"configured_reagent_name": d.get("material_name"),
|
"configured_reagent_name": d.get("material_name"),
|
||||||
"spec": d.get("spec"),
|
"spec": d.get("spec"),
|
||||||
@@ -2851,8 +2858,10 @@ async def replace_task_step_outputs(
|
|||||||
try:
|
try:
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
items_data = [it.model_dump(by_alias=False) for it in body]
|
items_data = [it.model_dump(by_alias=False) for it in body]
|
||||||
production_task_svc.replace_step_outputs(db, step_id, tenant_no, user.get("user_id"), items_data)
|
saved = production_task_svc.replace_step_outputs(
|
||||||
return success_response(None, message="更新成功")
|
db, step_id, tenant_no, user.get("user_id"), items_data
|
||||||
|
)
|
||||||
|
return success_response(saved, message="更新成功")
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
return error_response(403, str(e))
|
return error_response(403, str(e))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@@ -739,6 +739,7 @@ class StockCheckCreateBody(BaseModel):
|
|||||||
|
|
||||||
class StockCheckUnusedLineItem(BaseModel):
|
class StockCheckUnusedLineItem(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
usage_id: Optional[str] = Field(None, alias="usageId")
|
usage_id: Optional[str] = Field(None, alias="usageId")
|
||||||
batch_id: Optional[str] = Field(None, alias="batchId")
|
batch_id: Optional[str] = Field(None, alias="batchId")
|
||||||
material_code: Optional[str] = Field(None, alias="materialCode")
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
||||||
@@ -926,6 +927,7 @@ async def cancel_production_material_stock_check(
|
|||||||
|
|
||||||
class FormulaLineBody(BaseModel):
|
class FormulaLineBody(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
component_material_id: str = Field(..., alias="componentMaterialId")
|
component_material_id: str = Field(..., alias="componentMaterialId")
|
||||||
component_material_type: str = Field(..., alias="componentMaterialType")
|
component_material_type: str = Field(..., alias="componentMaterialType")
|
||||||
standard_qty: float = Field(..., gt=0, alias="standardQty")
|
standard_qty: float = Field(..., gt=0, alias="standardQty")
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
"""协议级退费登记 API"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.menu_guard import require_admin_or_super_admin
|
||||||
|
from app.core.response import error_response, success_response
|
||||||
|
from app.crud import protocol_refund_record as rr_svc
|
||||||
|
from app.models.models import (
|
||||||
|
BizCollectionProtocol,
|
||||||
|
BizProtocolRefundRecord,
|
||||||
|
SysUser,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["细胞存储-退费登记"])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _protocol_accessible(db: Session, protocol_id: str, tenant_no: str, user: dict) -> bool:
|
||||||
|
from app.core.data_scope import apply_data_scope
|
||||||
|
|
||||||
|
q = db.query(BizCollectionProtocol).filter(
|
||||||
|
BizCollectionProtocol.id == protocol_id,
|
||||||
|
BizCollectionProtocol.del_flag == 0,
|
||||||
|
BizCollectionProtocol.tenant_no == tenant_no,
|
||||||
|
)
|
||||||
|
q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
|
||||||
|
return q.first() is not None
|
||||||
|
|
||||||
|
|
||||||
|
class RefundRecordBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
refund_amount: float = Field(..., alias="refundAmount")
|
||||||
|
refund_time: Optional[str] = Field(None, alias="refundTime")
|
||||||
|
refund_remark: Optional[str] = Field(None, alias="refundRemark")
|
||||||
|
refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
|
||||||
|
workflow_instance_id: Optional[str] = Field(None, alias="workflowInstanceId")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RefundRecordUpdateBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
refund_amount: Optional[float] = Field(None, alias="refundAmount")
|
||||||
|
refund_time: Optional[str] = Field(None, alias="refundTime")
|
||||||
|
refund_remark: Optional[str] = Field(None, alias="refundRemark")
|
||||||
|
refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
|
||||||
|
workflow_instance_id: Optional[str] = Field(None, alias="workflowInstanceId")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/collection-protocols/{protocol_id}/refund-records")
|
||||||
|
async def list_protocol_refund_records(
|
||||||
|
protocol_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(404, "协议单不存在或无权查看")
|
||||||
|
data = rr_svc.list_refund_records(
|
||||||
|
db, collection_protocol_id=protocol_id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
return success_response(data)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/collection-protocols/{protocol_id}/refund-records")
|
||||||
|
async def create_protocol_refund_record(
|
||||||
|
protocol_id: str,
|
||||||
|
body: RefundRecordBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(404, "协议单不存在或无权操作")
|
||||||
|
obj = rr_svc.create_refund_record(
|
||||||
|
db,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
refund_amount=body.refund_amount,
|
||||||
|
refund_time=body.refund_time,
|
||||||
|
refund_remark=body.refund_remark,
|
||||||
|
refund_attachments=body.refund_attachments,
|
||||||
|
workflow_instance_id=body.workflow_instance_id,
|
||||||
|
remark=body.remark,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
rr_svc._record_to_dict(db, obj, tenant_no), message="登记成功"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/protocol-refund-records/{record_id}")
|
||||||
|
async def update_protocol_refund_record(
|
||||||
|
record_id: str,
|
||||||
|
body: RefundRecordUpdateBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
existing = (
|
||||||
|
db.query(BizProtocolRefundRecord)
|
||||||
|
.filter(
|
||||||
|
BizProtocolRefundRecord.id == record_id,
|
||||||
|
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||||
|
BizProtocolRefundRecord.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not existing:
|
||||||
|
return error_response(404, "退费登记不存在")
|
||||||
|
if not _protocol_accessible(
|
||||||
|
db, existing.collection_protocol_id, tenant_no, user
|
||||||
|
):
|
||||||
|
return error_response(403, "无权操作该协议")
|
||||||
|
dumped = body.model_dump(by_alias=False, exclude_unset=True)
|
||||||
|
obj = rr_svc.update_refund_record(
|
||||||
|
db,
|
||||||
|
record_id=record_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
refund_amount=dumped.get("refund_amount"),
|
||||||
|
refund_time=dumped.get("refund_time"),
|
||||||
|
refund_remark=dumped.get("refund_remark"),
|
||||||
|
refund_attachments=dumped.get("refund_attachments"),
|
||||||
|
workflow_instance_id=dumped.get("workflow_instance_id"),
|
||||||
|
remark=dumped.get("remark"),
|
||||||
|
fields_set=set(dumped.keys()),
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
rr_svc._record_to_dict(db, obj, tenant_no), message="保存成功"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/protocol-refund-records/{record_id}")
|
||||||
|
async def delete_protocol_refund_record(
|
||||||
|
record_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
existing = (
|
||||||
|
db.query(BizProtocolRefundRecord)
|
||||||
|
.filter(
|
||||||
|
BizProtocolRefundRecord.id == record_id,
|
||||||
|
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||||
|
BizProtocolRefundRecord.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not existing:
|
||||||
|
return error_response(404, "退费登记不存在")
|
||||||
|
if not _protocol_accessible(
|
||||||
|
db, existing.collection_protocol_id, tenant_no, user
|
||||||
|
):
|
||||||
|
return error_response(403, "无权操作该协议")
|
||||||
|
rr_svc.soft_delete_refund_record(
|
||||||
|
db, record_id=record_id, tenant_no=tenant_no, user_id=user_id
|
||||||
|
)
|
||||||
|
return success_response(None, message="删除成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
"""采集协议样本采集信息 API"""
|
"""采集协议样本采集信息 API"""
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.menu_guard import require_any_menu_code
|
from app.core.menu_guard import require_any_menu_code
|
||||||
from app.core.response import error_response, success_response
|
from app.core.response import error_response, success_response
|
||||||
|
from app.core.utils import parse_date, parse_datetime
|
||||||
from app.crud import protocol_sample_collection as svc
|
from app.crud import protocol_sample_collection as svc
|
||||||
|
from app.crud import sample_info_quick_register as quick_svc
|
||||||
from app.crud.collection_protocol import total_pages
|
from app.crud.collection_protocol import total_pages
|
||||||
from app.models.models import SysUser
|
from app.models.models import SysUser
|
||||||
|
from app.services.sample_customer_service_register import has_any_customer_service_payload
|
||||||
|
|
||||||
router = APIRouter(prefix="/web/admin", tags=["采集协议样本采集信息"])
|
router = APIRouter(prefix="/web/admin", tags=["采集协议样本采集信息"])
|
||||||
|
|
||||||
@@ -17,6 +21,11 @@ _SAMPLE_INFO_MENU = require_any_menu_code(
|
|||||||
"cell-storage:sample-info:list",
|
"cell-storage:sample-info:list",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_QUICK_REGISTER_MENU = require_any_menu_code(
|
||||||
|
"cell-storage:sample-info:list",
|
||||||
|
"research-donation:sample-info:list",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_tenant_no(db: Session, user: dict) -> str:
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
user_id = user.get("user_id")
|
user_id = user.get("user_id")
|
||||||
@@ -44,6 +53,12 @@ async def list_protocol_sample_collections(
|
|||||||
pageSize: int = Query(10, ge=1, le=100),
|
pageSize: int = Query(10, ge=1, le=100),
|
||||||
keyword: Optional[str] = None,
|
keyword: Optional[str] = None,
|
||||||
sampleStatus: Optional[str] = Query(None, alias="sampleStatus"),
|
sampleStatus: Optional[str] = Query(None, alias="sampleStatus"),
|
||||||
|
collectionFlag: Optional[str] = Query(None, alias="collectionFlag"),
|
||||||
|
receptionFlag: Optional[str] = Query(None, alias="receptionFlag"),
|
||||||
|
qcFlag: Optional[str] = Query(None, alias="qcFlag"),
|
||||||
|
storageFlag: Optional[str] = Query(None, alias="storageFlag"),
|
||||||
|
storageCertHandled: Optional[str] = Query(None, alias="storageCertHandled"),
|
||||||
|
insuranceHandled: Optional[str] = Query(None, alias="insuranceHandled"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user: dict = Depends(_SAMPLE_INFO_MENU),
|
user: dict = Depends(_SAMPLE_INFO_MENU),
|
||||||
):
|
):
|
||||||
@@ -56,6 +71,12 @@ async def list_protocol_sample_collections(
|
|||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
sample_status=sampleStatus,
|
sample_status=sampleStatus,
|
||||||
|
collection_flag=collectionFlag,
|
||||||
|
reception_flag=receptionFlag,
|
||||||
|
qc_flag=qcFlag,
|
||||||
|
storage_flag=storageFlag,
|
||||||
|
storage_cert_handled=storageCertHandled,
|
||||||
|
insurance_handled=insuranceHandled,
|
||||||
user_info=user,
|
user_info=user,
|
||||||
)
|
)
|
||||||
return success_response(_page_payload(items, total, page, pageSize))
|
return success_response(_page_payload(items, total, page, pageSize))
|
||||||
@@ -63,3 +84,321 @@ async def list_protocol_sample_collections(
|
|||||||
return error_response(403, str(e))
|
return error_response(403, str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class QuickRegisterItem(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
sample_code: str = Field(..., alias="sampleCode")
|
||||||
|
protocol_id: str = Field(..., alias="protocolId")
|
||||||
|
|
||||||
|
|
||||||
|
class QuickRegisterBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
kind: str
|
||||||
|
reception_source: str = Field(..., alias="receptionSource")
|
||||||
|
items: List[QuickRegisterItem]
|
||||||
|
handle_time: Optional[str] = Field(None, alias="handleTime")
|
||||||
|
handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
|
||||||
|
attachments: Optional[str] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
mail_cert_time: Optional[str] = Field(None, alias="mailCertTime")
|
||||||
|
policy_send_time: Optional[str] = Field(None, alias="policySendTime")
|
||||||
|
storage_handle_time: Optional[str] = Field(None, alias="storageHandleTime")
|
||||||
|
storage_handler_user_id: Optional[str] = Field(None, alias="storageHandlerUserId")
|
||||||
|
storage_attachments: Optional[str] = Field(None, alias="storageAttachments")
|
||||||
|
storage_remark: Optional[str] = Field(None, alias="storageRemark")
|
||||||
|
cert_handle_time: Optional[str] = Field(None, alias="certHandleTime")
|
||||||
|
cert_handler_user_id: Optional[str] = Field(None, alias="certHandlerUserId")
|
||||||
|
cert_attachments: Optional[str] = Field(None, alias="certAttachments")
|
||||||
|
cert_remark: Optional[str] = Field(None, alias="certRemark")
|
||||||
|
insurance_handle_time: Optional[str] = Field(None, alias="insuranceHandleTime")
|
||||||
|
insurance_handler_user_id: Optional[str] = Field(None, alias="insuranceHandlerUserId")
|
||||||
|
insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
|
||||||
|
insurance_remark: Optional[str] = Field(None, alias="insuranceRemark")
|
||||||
|
sms_send_date: Optional[str] = Field(None, alias="smsSendDate")
|
||||||
|
sms_send_status: Optional[str] = Field(None, alias="smsSendStatus")
|
||||||
|
microbe_followup_date: Optional[str] = Field(None, alias="microbeFollowupDate")
|
||||||
|
microbe_followup_status: Optional[str] = Field(None, alias="microbeFollowupStatus")
|
||||||
|
fullmoon_followup_date: Optional[str] = Field(None, alias="fullmoonFollowupDate")
|
||||||
|
fullmoon_followup_status: Optional[str] = Field(None, alias="fullmoonFollowupStatus")
|
||||||
|
household_registered: Optional[str] = Field(None, alias="householdRegistered")
|
||||||
|
einvoice_send_time: Optional[str] = Field(None, alias="einvoiceSendTime")
|
||||||
|
effective_address: Optional[str] = Field(None, alias="effectiveAddress")
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_dt(raw: Optional[str]):
|
||||||
|
if raw is None:
|
||||||
|
return None, False
|
||||||
|
text = str(raw).strip()
|
||||||
|
if not text:
|
||||||
|
return None, False
|
||||||
|
return parse_datetime(text), True
|
||||||
|
|
||||||
|
|
||||||
|
def _customer_service_incoming_from_body(body: QuickRegisterBody) -> dict:
|
||||||
|
return {
|
||||||
|
"sms_send_date": parse_date(body.sms_send_date) if body.sms_send_date else None,
|
||||||
|
"sms_send_status": body.sms_send_status,
|
||||||
|
"microbe_followup_date": parse_date(body.microbe_followup_date)
|
||||||
|
if body.microbe_followup_date
|
||||||
|
else None,
|
||||||
|
"microbe_followup_status": body.microbe_followup_status,
|
||||||
|
"fullmoon_followup_date": parse_date(body.fullmoon_followup_date)
|
||||||
|
if body.fullmoon_followup_date
|
||||||
|
else None,
|
||||||
|
"fullmoon_followup_status": body.fullmoon_followup_status,
|
||||||
|
"household_registered": body.household_registered,
|
||||||
|
"einvoice_send_time": parse_datetime(body.einvoice_send_time)
|
||||||
|
if body.einvoice_send_time
|
||||||
|
else None,
|
||||||
|
"effective_address": body.effective_address,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-info/quick-register")
|
||||||
|
async def quick_register_sample_info(
|
||||||
|
body: QuickRegisterBody,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: dict = Depends(_QUICK_REGISTER_MENU),
|
||||||
|
):
|
||||||
|
kind = (body.kind or "").strip()
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
ht = parse_datetime(body.handle_time) if body.handle_time else None
|
||||||
|
items = [
|
||||||
|
{"sampleCode": it.sample_code, "protocolId": it.protocol_id} for it in (body.items or [])
|
||||||
|
]
|
||||||
|
source = (body.reception_source or "").strip()
|
||||||
|
attachments = body.attachments or ""
|
||||||
|
remark = body.remark or ""
|
||||||
|
mail_t, update_mail = _optional_dt(body.mail_cert_time)
|
||||||
|
policy_t, update_policy = _optional_dt(body.policy_send_time)
|
||||||
|
cs_incoming = _customer_service_incoming_from_body(body)
|
||||||
|
|
||||||
|
if kind == "storage":
|
||||||
|
if ht is None:
|
||||||
|
return error_response(400, "请填写办理时间")
|
||||||
|
result = quick_svc.batch_quick_register_storage(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
reception_source=source,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=ht,
|
||||||
|
handler_user_id=body.handler_user_id,
|
||||||
|
attachments=attachments,
|
||||||
|
remark=remark,
|
||||||
|
)
|
||||||
|
elif kind == "storage_cert":
|
||||||
|
if source != "cell_storage":
|
||||||
|
return error_response(400, "储存证仅支持细胞存储样本")
|
||||||
|
if ht is None:
|
||||||
|
return error_response(400, "请填写办理时间")
|
||||||
|
result = quick_svc.batch_handle_storage_certs(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=ht,
|
||||||
|
handler_user_id=body.handler_user_id,
|
||||||
|
attachments=attachments,
|
||||||
|
remark=remark,
|
||||||
|
mail_cert_time=mail_t,
|
||||||
|
update_mail_cert_time=update_mail,
|
||||||
|
)
|
||||||
|
elif kind == "insurance":
|
||||||
|
if source != "cell_storage":
|
||||||
|
return error_response(400, "保险仅支持细胞存储样本")
|
||||||
|
if ht is None:
|
||||||
|
return error_response(400, "请填写办理时间")
|
||||||
|
result = quick_svc.batch_handle_insurance(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=ht,
|
||||||
|
handler_user_id=body.handler_user_id,
|
||||||
|
attachments=attachments,
|
||||||
|
remark=remark,
|
||||||
|
policy_send_time=policy_t,
|
||||||
|
update_policy_send_time=update_policy,
|
||||||
|
)
|
||||||
|
elif kind in ("sms", "followup", "fulfillment"):
|
||||||
|
if source != "cell_storage":
|
||||||
|
return error_response(400, "客户服务登记仅支持细胞存储样本")
|
||||||
|
result = quick_svc.batch_register_customer_service(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
incoming=cs_incoming,
|
||||||
|
)
|
||||||
|
elif kind == "all":
|
||||||
|
if source != "cell_storage":
|
||||||
|
return error_response(400, "登记全部仅支持细胞存储样本")
|
||||||
|
split = any(
|
||||||
|
value is not None
|
||||||
|
for value in (
|
||||||
|
body.storage_handle_time,
|
||||||
|
body.storage_handler_user_id,
|
||||||
|
body.storage_attachments,
|
||||||
|
body.storage_remark,
|
||||||
|
body.cert_handle_time,
|
||||||
|
body.insurance_handle_time,
|
||||||
|
body.cert_attachments,
|
||||||
|
body.insurance_attachments,
|
||||||
|
body.cert_remark,
|
||||||
|
body.insurance_remark,
|
||||||
|
body.cert_handler_user_id,
|
||||||
|
body.insurance_handler_user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if split:
|
||||||
|
storage_ht = (
|
||||||
|
parse_datetime(body.storage_handle_time)
|
||||||
|
if (body.storage_handle_time or "").strip()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
storage_handler = body.storage_handler_user_id
|
||||||
|
storage_atts = body.storage_attachments or ""
|
||||||
|
storage_remark = body.storage_remark or ""
|
||||||
|
cert_ht = (
|
||||||
|
parse_datetime(body.cert_handle_time)
|
||||||
|
if (body.cert_handle_time or "").strip()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
ins_ht = (
|
||||||
|
parse_datetime(body.insurance_handle_time)
|
||||||
|
if (body.insurance_handle_time or "").strip()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
cert_handler = body.cert_handler_user_id
|
||||||
|
ins_handler = body.insurance_handler_user_id
|
||||||
|
cert_atts = body.cert_attachments or ""
|
||||||
|
ins_atts = body.insurance_attachments or ""
|
||||||
|
cert_remark = body.cert_remark or ""
|
||||||
|
ins_remark = body.insurance_remark or ""
|
||||||
|
else:
|
||||||
|
storage_ht = None
|
||||||
|
storage_handler = None
|
||||||
|
storage_atts = ""
|
||||||
|
storage_remark = ""
|
||||||
|
cert_ht = ins_ht = ht
|
||||||
|
cert_handler = ins_handler = body.handler_user_id
|
||||||
|
cert_atts = ins_atts = attachments
|
||||||
|
cert_remark = ins_remark = remark
|
||||||
|
storage_side = bool(
|
||||||
|
storage_ht is not None
|
||||||
|
or (storage_atts or "").strip()
|
||||||
|
or (storage_remark or "").strip()
|
||||||
|
)
|
||||||
|
cert_side = bool(
|
||||||
|
cert_ht is not None
|
||||||
|
or update_mail
|
||||||
|
or (cert_atts or "").strip()
|
||||||
|
or (cert_remark or "").strip()
|
||||||
|
)
|
||||||
|
ins_side = bool(
|
||||||
|
ins_ht is not None
|
||||||
|
or update_policy
|
||||||
|
or (ins_atts or "").strip()
|
||||||
|
or (ins_remark or "").strip()
|
||||||
|
)
|
||||||
|
if storage_side and storage_ht is None:
|
||||||
|
return error_response(400, "请填写入库办理时间")
|
||||||
|
if cert_side and cert_ht is None:
|
||||||
|
return error_response(400, "请填写储存证办理时间")
|
||||||
|
if ins_side and ins_ht is None:
|
||||||
|
return error_response(400, "请填写保险办理时间")
|
||||||
|
wrote_any = False
|
||||||
|
success = 0
|
||||||
|
fail = 0
|
||||||
|
if storage_ht is not None:
|
||||||
|
storage_r = quick_svc.batch_quick_register_storage(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
reception_source=source,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=storage_ht,
|
||||||
|
handler_user_id=storage_handler,
|
||||||
|
attachments=storage_atts,
|
||||||
|
remark=storage_remark,
|
||||||
|
do_commit=False,
|
||||||
|
)
|
||||||
|
success = max(success, storage_r.get("success", 0))
|
||||||
|
fail = max(fail, storage_r.get("fail", 0))
|
||||||
|
wrote_any = True
|
||||||
|
if cert_ht is not None:
|
||||||
|
cert_r = quick_svc.batch_handle_storage_certs(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=cert_ht,
|
||||||
|
handler_user_id=cert_handler,
|
||||||
|
attachments=cert_atts,
|
||||||
|
remark=cert_remark,
|
||||||
|
mail_cert_time=mail_t,
|
||||||
|
update_mail_cert_time=update_mail,
|
||||||
|
do_commit=False,
|
||||||
|
)
|
||||||
|
success = max(success, cert_r.get("success", 0))
|
||||||
|
fail = max(fail, cert_r.get("fail", 0))
|
||||||
|
wrote_any = True
|
||||||
|
if ins_ht is not None:
|
||||||
|
ins_r = quick_svc.batch_handle_insurance(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=ins_ht,
|
||||||
|
handler_user_id=ins_handler,
|
||||||
|
attachments=ins_atts,
|
||||||
|
remark=ins_remark,
|
||||||
|
policy_send_time=policy_t,
|
||||||
|
update_policy_send_time=update_policy,
|
||||||
|
do_commit=False,
|
||||||
|
)
|
||||||
|
success = max(success, ins_r.get("success", 0))
|
||||||
|
fail = max(fail, ins_r.get("fail", 0))
|
||||||
|
wrote_any = True
|
||||||
|
if has_any_customer_service_payload(cs_incoming):
|
||||||
|
cs_r = quick_svc.batch_register_customer_service(
|
||||||
|
db,
|
||||||
|
items=items,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
incoming=cs_incoming,
|
||||||
|
do_commit=False,
|
||||||
|
)
|
||||||
|
success = max(success, cs_r.get("success", 0))
|
||||||
|
fail = max(fail, cs_r.get("fail", 0))
|
||||||
|
wrote_any = True
|
||||||
|
if not wrote_any:
|
||||||
|
return error_response(400, "请至少填写一项登记信息")
|
||||||
|
db.commit()
|
||||||
|
result = {"success": success, "fail": fail}
|
||||||
|
else:
|
||||||
|
return error_response(400, "登记类型无效")
|
||||||
|
return success_response(result, message="登记完成")
|
||||||
|
except PermissionError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(400, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
@@ -0,0 +1,646 @@
|
|||||||
|
"""按样本缴费方案 / 转账笔财务确认 API(payment_model_version=2)"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.menu_guard import require_admin_or_super_admin
|
||||||
|
from app.core.response import error_response, success_response
|
||||||
|
from app.crud import protocol_sample_payment as psp_svc
|
||||||
|
from app.models.models import BizCollectionProtocol, SysUser
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["细胞存储-按样本缴费"])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _protocol_accessible(db: Session, protocol_id: str, tenant_no: str, user: dict) -> bool:
|
||||||
|
from app.core.data_scope import apply_data_scope
|
||||||
|
|
||||||
|
q = db.query(BizCollectionProtocol).filter(
|
||||||
|
BizCollectionProtocol.id == protocol_id,
|
||||||
|
BizCollectionProtocol.del_flag == 0,
|
||||||
|
BizCollectionProtocol.tenant_no == tenant_no,
|
||||||
|
)
|
||||||
|
q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
|
||||||
|
return q.first() is not None
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
id: Optional[str] = None
|
||||||
|
schedule_no: Optional[int] = Field(None, alias="scheduleNo")
|
||||||
|
due_amount: Optional[float] = Field(None, alias="dueAmount")
|
||||||
|
due_date: Optional[str] = Field(None, alias="dueDate")
|
||||||
|
schedule_status: Optional[str] = Field(None, alias="scheduleStatus")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
transfers: Optional[List[Dict[str, Any]]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SamplePaymentBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
id: Optional[str] = None
|
||||||
|
sample_code: str = Field(..., alias="sampleCode")
|
||||||
|
protocol_sample_id: Optional[str] = Field(None, alias="protocolSampleId")
|
||||||
|
pay_mode: str = Field(..., alias="payMode")
|
||||||
|
contract_amount: Optional[float] = Field(None, alias="contractAmount")
|
||||||
|
installment_count: Optional[int] = Field(None, alias="installmentCount")
|
||||||
|
installment_interval_months: Optional[float] = Field(
|
||||||
|
None, alias="installmentIntervalMonths"
|
||||||
|
)
|
||||||
|
installment_start_date: Optional[str] = Field(None, alias="installmentStartDate")
|
||||||
|
installment_advance_days: Optional[int] = Field(None, alias="installmentAdvanceDays")
|
||||||
|
installment_ratios: Optional[Any] = Field(None, alias="installmentRatios")
|
||||||
|
lump_sum_deadline: Optional[str] = Field(None, alias="lumpSumDeadline")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
schedules: Optional[List[ScheduleBody]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SamplePaymentsPutBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
sample_payments: List[SamplePaymentBody] = Field(
|
||||||
|
default_factory=list, alias="samplePayments"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TransferCreateBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
schedule_id: str = Field(..., alias="scheduleId")
|
||||||
|
transfer_amount: Optional[float] = Field(None, alias="transferAmount")
|
||||||
|
transfer_time: Optional[str] = Field(None, alias="transferTime")
|
||||||
|
transfer_type: Optional[str] = Field(None, alias="transferType")
|
||||||
|
transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class TransferDraftBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
transfer_amount: Optional[float] = Field(None, alias="transferAmount")
|
||||||
|
transfer_time: Optional[str] = Field(None, alias="transferTime")
|
||||||
|
transfer_type: Optional[str] = Field(None, alias="transferType")
|
||||||
|
transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceConfirmBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
finance_confirm_attachments: Optional[str] = Field(
|
||||||
|
None, alias="financeConfirmAttachments"
|
||||||
|
)
|
||||||
|
finance_remark: Optional[str] = Field(None, alias="financeRemark")
|
||||||
|
|
||||||
|
|
||||||
|
def _ratios_to_str(ratios: Any) -> Optional[str]:
|
||||||
|
if ratios is None:
|
||||||
|
return None
|
||||||
|
if isinstance(ratios, str):
|
||||||
|
return ratios.strip() or None
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.dumps(ratios, ensure_ascii=False)
|
||||||
|
except Exception:
|
||||||
|
return str(ratios)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_sample_payments(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
protocol_id: str,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: str,
|
||||||
|
payments: List[SamplePaymentBody],
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
for item in payments:
|
||||||
|
prev = psp_svc.find_active_sample_payment(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
sample_payment_id=item.id,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
sample_code=item.sample_code,
|
||||||
|
)
|
||||||
|
prev_mode = (prev.pay_mode or "").strip() if prev else ""
|
||||||
|
sp = psp_svc.upsert_sample_payment(
|
||||||
|
db,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
sample_code=item.sample_code,
|
||||||
|
pay_mode=item.pay_mode,
|
||||||
|
contract_amount=item.contract_amount,
|
||||||
|
protocol_sample_id=item.protocol_sample_id,
|
||||||
|
installment_count=item.installment_count,
|
||||||
|
installment_interval_months=item.installment_interval_months,
|
||||||
|
installment_start_date=item.installment_start_date,
|
||||||
|
installment_advance_days=item.installment_advance_days,
|
||||||
|
installment_ratios=_ratios_to_str(item.installment_ratios),
|
||||||
|
lump_sum_deadline=item.lump_sum_deadline,
|
||||||
|
remark=item.remark,
|
||||||
|
sample_payment_id=item.id,
|
||||||
|
)
|
||||||
|
next_mode = (item.pay_mode or "").strip()
|
||||||
|
if (
|
||||||
|
prev_mode == "fq"
|
||||||
|
and next_mode in ("fgd", "dj")
|
||||||
|
and psp_svc.sample_payment_has_yjf(
|
||||||
|
db, sample_payment_id=sp.id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
):
|
||||||
|
psp_svc.apply_fq_to_fgd_merge(
|
||||||
|
db,
|
||||||
|
sample_payment_id=sp.id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
target_pay_mode=next_mode,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if item.schedules is not None:
|
||||||
|
schedule_payloads = [s.model_dump(by_alias=False) for s in item.schedules]
|
||||||
|
psp_svc.sync_schedules_and_transfers(
|
||||||
|
db,
|
||||||
|
sample_payment_id=sp.id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
schedules=schedule_payloads,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/payment-chase")
|
||||||
|
async def list_payment_chase(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
chaseType: Optional[str] = Query(None, alias="chaseType"),
|
||||||
|
payMode: Optional[str] = Query(None, alias="payMode"),
|
||||||
|
salesUserId: Optional[str] = Query(None, alias="salesUserId"),
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""催缴信息:逾期未清 ∪ 未来 30 天内应缴未清(一行一期)。"""
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = psp_svc.list_payment_chase_page(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
keyword=keyword,
|
||||||
|
chase_type=chaseType,
|
||||||
|
pay_mode=payMode,
|
||||||
|
sales_user_id=salesUserId,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-payments")
|
||||||
|
async def list_sample_payments(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
payMode: Optional[str] = Query(None, alias="payMode"),
|
||||||
|
progressStatus: Optional[str] = Query(None, alias="progressStatus"),
|
||||||
|
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
|
||||||
|
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""按样本缴费方案分页列表(独立缴费信息菜单)。"""
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = psp_svc.list_sample_payments_page(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
keyword=keyword,
|
||||||
|
pay_mode=payMode,
|
||||||
|
progress_status=progressStatus,
|
||||||
|
due_date_start=installmentDueDateStart,
|
||||||
|
due_date_end=installmentDueDateEnd,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": page_size}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-payments/{sample_payment_id}")
|
||||||
|
async def get_sample_payment(
|
||||||
|
sample_payment_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
data = psp_svc.get_sample_payment_detail(
|
||||||
|
db,
|
||||||
|
sample_payment_id=sample_payment_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
if not data:
|
||||||
|
return error_response(403, "无权查看或记录不存在")
|
||||||
|
return success_response(data)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/collection-protocols/{protocol_id}/sample-payments")
|
||||||
|
async def get_sample_payments(
|
||||||
|
protocol_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(404, "协议单不存在或无权查看")
|
||||||
|
data = psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
|
||||||
|
return success_response(data)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/collection-protocols/{protocol_id}/sample-payments")
|
||||||
|
async def put_sample_payments(
|
||||||
|
protocol_id: str,
|
||||||
|
body: SamplePaymentsPutBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||||
|
return error_response(404, "协议单不存在或无权操作")
|
||||||
|
# pydantic v2: may need model_validate with populate
|
||||||
|
payments = body.sample_payments
|
||||||
|
if hasattr(body, "model_dump"):
|
||||||
|
raw = body.model_dump(by_alias=True)
|
||||||
|
payments = [
|
||||||
|
SamplePaymentBody.model_validate(x)
|
||||||
|
for x in (raw.get("samplePayments") or raw.get("sample_payments") or [])
|
||||||
|
]
|
||||||
|
data = _save_sample_payments(
|
||||||
|
db,
|
||||||
|
protocol_id=protocol_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
payments=payments or body.sample_payments,
|
||||||
|
)
|
||||||
|
return success_response(data, message="保存成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/payment-transfers")
|
||||||
|
async def create_payment_transfer(
|
||||||
|
body: TransferCreateBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
sch = psp_svc._get_schedule_or_raise(
|
||||||
|
db, schedule_id=body.schedule_id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
if not _protocol_accessible(db, sch.collection_protocol_id, tenant_no, user):
|
||||||
|
return error_response(403, "无权操作该协议")
|
||||||
|
obj = psp_svc.add_transfer(
|
||||||
|
db,
|
||||||
|
schedule_id=body.schedule_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
transfer_amount=body.transfer_amount,
|
||||||
|
transfer_time=body.transfer_time,
|
||||||
|
transfer_type=body.transfer_type,
|
||||||
|
transfer_attachments=body.transfer_attachments,
|
||||||
|
remark=body.remark,
|
||||||
|
)
|
||||||
|
from app.crud.protocol_sample_payment import _transfer_to_dict
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
_transfer_to_dict(db, obj, tenant_no), message="创建成功"
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/payment-transfers/{transfer_id}")
|
||||||
|
async def update_payment_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
body: TransferDraftBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
fields = body.model_dump(by_alias=False, exclude_unset=True)
|
||||||
|
locked_transfer = psp_svc._get_transfer_with_schedule_lock(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
obj = psp_svc.update_transfer_draft(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
fields=fields,
|
||||||
|
locked_transfer=locked_transfer,
|
||||||
|
)
|
||||||
|
from app.crud.protocol_sample_payment import _transfer_to_dict
|
||||||
|
|
||||||
|
return success_response(_transfer_to_dict(db, obj, tenant_no), message="保存成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/payment-transfers/{transfer_id}/submit-receipt-finance")
|
||||||
|
async def submit_payment_transfer_finance(
|
||||||
|
transfer_id: str,
|
||||||
|
body: Optional[TransferDraftBody] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
draft = body.model_dump(by_alias=True, exclude_unset=True) if body else None
|
||||||
|
locked_transfer = psp_svc._get_transfer_with_schedule_lock(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
obj = psp_svc.submit_receipt_finance(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
draft_fields=draft,
|
||||||
|
locked_transfer=locked_transfer,
|
||||||
|
)
|
||||||
|
from app.crud.protocol_sample_payment import _transfer_to_dict
|
||||||
|
|
||||||
|
return success_response(_transfer_to_dict(db, obj, tenant_no), message="已提交财务确认")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/payment-transfers/{transfer_id}/withdraw")
|
||||||
|
async def withdraw_payment_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
locked_transfer = psp_svc._get_transfer_with_schedule_lock(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
obj = psp_svc.withdraw_receipt(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
locked_transfer=locked_transfer,
|
||||||
|
)
|
||||||
|
from app.crud.protocol_sample_payment import _transfer_to_dict
|
||||||
|
|
||||||
|
return success_response(_transfer_to_dict(db, obj, tenant_no), message="已撤回")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/payment-transfers/{transfer_id}")
|
||||||
|
async def delete_payment_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
obj = psp_svc._get_transfer_or_raise(
|
||||||
|
db, transfer_id=transfer_id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
|
||||||
|
return error_response(403, "无权操作该协议")
|
||||||
|
psp_svc.soft_delete_payment_transfer(
|
||||||
|
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
|
||||||
|
)
|
||||||
|
return success_response(None, message="删除成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/payment-transfers")
|
||||||
|
async def list_payment_transfers(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
payStatus: str = Query("dfj", alias="payStatus"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
|
||||||
|
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""按转账笔状态分页(缴费信息看板 / 财务共用)。"""
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = psp_svc.list_transfers_for_finance(
|
||||||
|
db,
|
||||||
|
pay_status=payStatus,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
keyword=keyword,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
user_info=user,
|
||||||
|
due_date_start=installmentDueDateStart,
|
||||||
|
due_date_end=installmentDueDateEnd,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": page_size}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/payment-transfers/receipt-confirm")
|
||||||
|
async def list_receipt_confirm(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
|
||||||
|
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = psp_svc.list_transfers_for_finance(
|
||||||
|
db,
|
||||||
|
pay_status="jfqrz",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
keyword=keyword,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
user_info=user,
|
||||||
|
due_date_start=installmentDueDateStart,
|
||||||
|
due_date_end=installmentDueDateEnd,
|
||||||
|
)
|
||||||
|
return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/payment-transfers/receipt-confirmed")
|
||||||
|
async def list_receipt_confirmed(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
|
||||||
|
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = psp_svc.list_transfers_for_finance(
|
||||||
|
db,
|
||||||
|
pay_status="yjf",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
keyword=keyword,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
user_info=user,
|
||||||
|
due_date_start=installmentDueDateStart,
|
||||||
|
due_date_end=installmentDueDateEnd,
|
||||||
|
)
|
||||||
|
return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/payment-transfers/{transfer_id}/receipt-confirm")
|
||||||
|
async def confirm_payment_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
body: Optional[FinanceConfirmBody] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
locked_transfer = psp_svc._get_transfer_with_schedule_lock(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
obj = psp_svc.confirm_receipt(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
finance_confirm_attachments=body.finance_confirm_attachments if body else None,
|
||||||
|
finance_remark=body.finance_remark if body else None,
|
||||||
|
locked_transfer=locked_transfer,
|
||||||
|
)
|
||||||
|
from app.crud.protocol_sample_payment import _transfer_to_dict
|
||||||
|
|
||||||
|
return success_response(_transfer_to_dict(db, obj, tenant_no), message="确认收款成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/payment-transfers/{transfer_id}/receipt-reject")
|
||||||
|
async def reject_payment_transfer(
|
||||||
|
transfer_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
locked_transfer = psp_svc._get_transfer_with_schedule_lock(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
)
|
||||||
|
obj = psp_svc.reject_receipt(
|
||||||
|
db,
|
||||||
|
transfer_id=transfer_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
locked_transfer=locked_transfer,
|
||||||
|
)
|
||||||
|
from app.crud.protocol_sample_payment import _transfer_to_dict
|
||||||
|
|
||||||
|
return success_response(_transfer_to_dict(db, obj, tenant_no), message="已退回")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
@@ -120,11 +120,14 @@ async def web_login(
|
|||||||
if user.status != "1":
|
if user.status != "1":
|
||||||
return error_response(403, "账号已被停用,请联系管理员")
|
return error_response(403, "账号已被停用,请联系管理员")
|
||||||
|
|
||||||
if not user_has_active_roles(db, user.id):
|
tenant_no = (getattr(user, "tenant_no", None) or "").strip() or None
|
||||||
|
if not user_has_active_roles(db, user.id, tenant_no=tenant_no):
|
||||||
return error_response(403, "账号未分配角色,请联系管理员")
|
return error_response(403, "账号未分配角色,请联系管理员")
|
||||||
|
|
||||||
web_role = resolve_web_role_for_user(db, user.id)
|
web_role = resolve_web_role_for_user(
|
||||||
role_names = get_user_role_names(db, user.id)
|
db, user.id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
role_names = get_user_role_names(db, user.id, tenant_no=tenant_no)
|
||||||
|
|
||||||
session_id = generate_uuid()
|
session_id = generate_uuid()
|
||||||
access_jti = generate_uuid()
|
access_jti = generate_uuid()
|
||||||
@@ -227,7 +230,10 @@ async def refresh_token(body: dict, db: Session = Depends(get_db)):
|
|||||||
if not user or user.del_flag != 0 or user.status != "1":
|
if not user or user.del_flag != 0 or user.status != "1":
|
||||||
return error_response(401, "用户不存在或已禁用")
|
return error_response(401, "用户不存在或已禁用")
|
||||||
|
|
||||||
web_role = resolve_web_role_for_user(db, user.id)
|
tenant_no = (getattr(user, "tenant_no", None) or "").strip() or None
|
||||||
|
web_role = resolve_web_role_for_user(
|
||||||
|
db, user.id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
|
||||||
await revoke_refresh_token(refresh_jti=refresh_jti)
|
await revoke_refresh_token(refresh_jti=refresh_jti)
|
||||||
await extend_session(session_id=session_id)
|
await extend_session(session_id=session_id)
|
||||||
@@ -320,8 +326,13 @@ async def update_current_user_profile(
|
|||||||
user = user_crud.get(db, user_id)
|
user = user_crud.get(db, user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return error_response(404, "用户不存在")
|
return error_response(404, "用户不存在")
|
||||||
web_role = resolve_web_role_for_user(db, user.id)
|
tenant_no = (getattr(user, "tenant_no", None) or "").strip() or None
|
||||||
role_names = get_user_role_names(db, user.id)
|
web_role = resolve_web_role_for_user(
|
||||||
|
db, user.id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
role_names = get_user_role_names(
|
||||||
|
db, user.id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
return success_response(build_user_info_payload(user, web_role, role_names, db=db))
|
return success_response(build_user_info_payload(user, web_role, role_names, db=db))
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
return error_response(401, "Token无效或已过期")
|
return error_response(401, "Token无效或已过期")
|
||||||
@@ -419,7 +430,11 @@ async def get_web_auth_menu_codes(request: Request, db: Session = Depends(get_db
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
codes = iter_user_menu_perm_codes(db, current_user.get("user_id") or "")
|
codes = iter_user_menu_perm_codes(
|
||||||
|
db,
|
||||||
|
current_user.get("user_id") or "",
|
||||||
|
tenant_no=current_user.get("tenant_no"),
|
||||||
|
)
|
||||||
menu_codes = sorted(codes)
|
menu_codes = sorted(codes)
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
menu_codes = []
|
menu_codes = []
|
||||||
|
|||||||
@@ -826,6 +826,9 @@ async def list_qc_work_orders(
|
|||||||
infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
|
infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
|
||||||
triggerSource: Optional[str] = Query(None, alias="triggerSource"),
|
triggerSource: Optional[str] = Query(None, alias="triggerSource"),
|
||||||
sampleTypeId: Optional[str] = Query(None, alias="sampleTypeId"),
|
sampleTypeId: Optional[str] = Query(None, alias="sampleTypeId"),
|
||||||
|
protocolStatus: Optional[str] = Query(
|
||||||
|
None, alias="protocolStatus", description="协议/捐赠状态,多个用英文逗号分隔"
|
||||||
|
),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
||||||
):
|
):
|
||||||
@@ -842,6 +845,7 @@ async def list_qc_work_orders(
|
|||||||
reception_source=receptionSource,
|
reception_source=receptionSource,
|
||||||
trigger_source=triggerSource,
|
trigger_source=triggerSource,
|
||||||
sample_type_id=sampleTypeId,
|
sample_type_id=sampleTypeId,
|
||||||
|
protocol_status=protocolStatus,
|
||||||
create_time_start=createTimeStart,
|
create_time_start=createTimeStart,
|
||||||
create_time_end=createTimeEnd,
|
create_time_end=createTimeEnd,
|
||||||
infusion_time_start=infusionTimeStart,
|
infusion_time_start=infusionTimeStart,
|
||||||
@@ -2448,6 +2452,7 @@ class StockCheckCreateBody(BaseModel):
|
|||||||
|
|
||||||
class StockCheckUnusedLineItem(BaseModel):
|
class StockCheckUnusedLineItem(BaseModel):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
id: Optional[str] = None
|
||||||
usage_id: Optional[str] = Field(None, alias="usageId")
|
usage_id: Optional[str] = Field(None, alias="usageId")
|
||||||
batch_id: Optional[str] = Field(None, alias="batchId")
|
batch_id: Optional[str] = Field(None, alias="batchId")
|
||||||
material_code: Optional[str] = Field(None, alias="materialCode")
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ from app.core.response import error_response, success_response
|
|||||||
from app.crud import research_donation as rd_svc
|
from app.crud import research_donation as rd_svc
|
||||||
from app.models.models import SysUser
|
from app.models.models import SysUser
|
||||||
from app.models.research_donation import BizResearchDonation
|
from app.models.research_donation import BizResearchDonation
|
||||||
|
from app.services.sample_collect_wx_notify import (
|
||||||
|
PROTOCOL_KIND_DONATION,
|
||||||
|
sample_codes_from_orm_rows,
|
||||||
|
sample_codes_from_payload,
|
||||||
|
schedule_sample_collect_notify,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/web/admin", tags=["科研捐赠管理"])
|
router = APIRouter(prefix="/web/admin", tags=["科研捐赠管理"])
|
||||||
|
|
||||||
@@ -119,6 +125,17 @@ class ConvertFromStorageBody(BaseModel):
|
|||||||
protocol_id: str = Field(..., alias="protocolId")
|
protocol_id: str = Field(..., alias="protocolId")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_donation_sensitive_fields(
|
||||||
|
db: Session,
|
||||||
|
user: dict,
|
||||||
|
body: BaseModel,
|
||||||
|
*,
|
||||||
|
creating: bool,
|
||||||
|
) -> None:
|
||||||
|
"""保留钩子;不再校验菜单权限(与采集协议敏感字段策略对齐)。"""
|
||||||
|
_ = (db, user, body, creating)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 1. 列表
|
# 1. 列表
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -229,6 +246,7 @@ async def create_research_donation(
|
|||||||
user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
assert_donation_sensitive_fields(db, user, body, creating=True)
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
user_id = _get_current_user_id(user)
|
user_id = _get_current_user_id(user)
|
||||||
try:
|
try:
|
||||||
@@ -242,6 +260,17 @@ async def create_research_donation(
|
|||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
|
schedule_sample_collect_notify(
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
protocol_id=obj.id,
|
||||||
|
protocol_kind=PROTOCOL_KIND_DONATION,
|
||||||
|
before_codes=[],
|
||||||
|
submitted_codes=(
|
||||||
|
sample_codes_from_payload(body.samples)
|
||||||
|
if body.samples is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
return success_response(
|
return success_response(
|
||||||
rd_svc._donation_to_dict(obj, db, tenant_no),
|
rd_svc._donation_to_dict(obj, db, tenant_no),
|
||||||
message="创建成功",
|
message="创建成功",
|
||||||
@@ -262,10 +291,15 @@ async def update_research_donation(
|
|||||||
user: dict = Depends(require_admin_or_super_admin),
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
|
assert_donation_sensitive_fields(db, user, body, creating=False)
|
||||||
tenant_no = _get_tenant_no(db, user)
|
tenant_no = _get_tenant_no(db, user)
|
||||||
user_id = _get_current_user_id(user)
|
user_id = _get_current_user_id(user)
|
||||||
try:
|
try:
|
||||||
data = body.model_dump(exclude_unset=True, by_alias=True)
|
data = body.model_dump(exclude_unset=True, by_alias=True)
|
||||||
|
before_codes = sample_codes_from_orm_rows(
|
||||||
|
rd_svc.list_samples(db, donation_id, tenant_no)
|
||||||
|
)
|
||||||
|
unset_fields = body.model_dump(exclude_unset=True)
|
||||||
obj = rd_svc.update_donation(
|
obj = rd_svc.update_donation(
|
||||||
db,
|
db,
|
||||||
donation_id,
|
donation_id,
|
||||||
@@ -273,6 +307,17 @@ async def update_research_donation(
|
|||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
|
schedule_sample_collect_notify(
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
protocol_id=obj.id,
|
||||||
|
protocol_kind=PROTOCOL_KIND_DONATION,
|
||||||
|
before_codes=before_codes,
|
||||||
|
submitted_codes=(
|
||||||
|
sample_codes_from_payload(body.samples)
|
||||||
|
if "samples" in unset_fields
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
return success_response(
|
return success_response(
|
||||||
rd_svc._donation_to_dict(obj, db, tenant_no),
|
rd_svc._donation_to_dict(obj, db, tenant_no),
|
||||||
message="更新成功",
|
message="更新成功",
|
||||||
@@ -397,6 +442,32 @@ async def change_donation_status(
|
|||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/research-donations/{donation_id}/status-changes")
|
||||||
|
async def list_research_donation_status_changes(
|
||||||
|
donation_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
obj = (
|
||||||
|
db.query(BizResearchDonation)
|
||||||
|
.filter(
|
||||||
|
BizResearchDonation.id == donation_id,
|
||||||
|
BizResearchDonation.tenant_no == tenant_no,
|
||||||
|
BizResearchDonation.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not obj:
|
||||||
|
return error_response(404, "捐赠单不存在")
|
||||||
|
items = rd_svc.list_donation_status_changes(
|
||||||
|
db,
|
||||||
|
research_donation_id=donation_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
)
|
||||||
|
return success_response({"list": items})
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 10. 细胞存储 → 科研捐赠
|
# 10. 细胞存储 → 科研捐赠
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ async def list_donation_sample_collections(
|
|||||||
pageSize: int = Query(10, ge=1, le=100),
|
pageSize: int = Query(10, ge=1, le=100),
|
||||||
keyword: Optional[str] = None,
|
keyword: Optional[str] = None,
|
||||||
sampleStatus: Optional[str] = Query(None, alias="sampleStatus"),
|
sampleStatus: Optional[str] = Query(None, alias="sampleStatus"),
|
||||||
|
collectionFlag: Optional[str] = Query(None, alias="collectionFlag"),
|
||||||
|
receptionFlag: Optional[str] = Query(None, alias="receptionFlag"),
|
||||||
|
qcFlag: Optional[str] = Query(None, alias="qcFlag"),
|
||||||
|
storageFlag: Optional[str] = Query(None, alias="storageFlag"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user: dict = Depends(_SAMPLE_INFO_MENU),
|
user: dict = Depends(_SAMPLE_INFO_MENU),
|
||||||
):
|
):
|
||||||
@@ -56,6 +60,10 @@ async def list_donation_sample_collections(
|
|||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
sample_status=sampleStatus,
|
sample_status=sampleStatus,
|
||||||
|
collection_flag=collectionFlag,
|
||||||
|
reception_flag=receptionFlag,
|
||||||
|
qc_flag=qcFlag,
|
||||||
|
storage_flag=storageFlag,
|
||||||
user_info=user,
|
user_info=user,
|
||||||
)
|
)
|
||||||
return success_response(_page_payload(items, total, page, pageSize))
|
return success_response(_page_payload(items, total, page, pageSize))
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
"""样本级储存证 / 保险办理 API"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.menu_guard import require_admin_or_super_admin, require_any_menu_code
|
||||||
|
from app.core.response import error_response, success_response
|
||||||
|
from app.core.utils import parse_datetime
|
||||||
|
from app.crud import sample_insurance as insurance_crud
|
||||||
|
from app.crud import sample_storage_cert as cert_crud
|
||||||
|
from app.models.models import BizProtocolSample, SysUser
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["样本储存证保险"])
|
||||||
|
|
||||||
|
_MENU = require_any_menu_code("cell-storage:sample-info:list")
|
||||||
|
_WRITE_MENU = require_admin_or_super_admin
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_protocol_id(db: Session, tenant_no: str, sample_code: str) -> Optional[str]:
|
||||||
|
row = (
|
||||||
|
db.query(BizProtocolSample)
|
||||||
|
.filter(
|
||||||
|
BizProtocolSample.tenant_no == tenant_no,
|
||||||
|
BizProtocolSample.sample_code == sample_code,
|
||||||
|
BizProtocolSample.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return (row.collection_protocol_id or "").strip() or None if row else None
|
||||||
|
|
||||||
|
|
||||||
|
class SampleHandleBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
handle_time: Optional[str] = Field(None, alias="handleTime")
|
||||||
|
handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
|
||||||
|
cert_image_urls: Optional[str] = Field(None, alias="certImageUrls")
|
||||||
|
insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
|
||||||
|
remark: Optional[str] = None
|
||||||
|
handle_remark: Optional[str] = Field(None, alias="handleRemark")
|
||||||
|
mail_cert_time: Optional[str] = Field(None, alias="mailCertTime")
|
||||||
|
policy_send_time: Optional[str] = Field(None, alias="policySendTime")
|
||||||
|
collection_protocol_id: Optional[str] = Field(None, alias="collectionProtocolId")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-storage-certs/{sample_code}")
|
||||||
|
async def get_sample_storage_cert(
|
||||||
|
sample_code: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: dict = Depends(_MENU),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
row = cert_crud.get_by_sample_code(db, tenant_no, code)
|
||||||
|
if row is None:
|
||||||
|
return success_response(
|
||||||
|
{
|
||||||
|
"sampleCode": code,
|
||||||
|
"storageCertHandled": False,
|
||||||
|
"handleTime": "",
|
||||||
|
"handlerUserId": "",
|
||||||
|
"certImageUrls": "",
|
||||||
|
"mailCertTime": "",
|
||||||
|
"remark": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return success_response(cert_crud.row_to_dict(row))
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/sample-storage-certs/{sample_code}")
|
||||||
|
async def put_sample_storage_cert(
|
||||||
|
sample_code: str,
|
||||||
|
body: SampleHandleBody,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: dict = Depends(_WRITE_MENU),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
pid = (body.collection_protocol_id or "").strip() or _resolve_protocol_id(
|
||||||
|
db, tenant_no, code
|
||||||
|
)
|
||||||
|
ht = parse_datetime(body.handle_time) if body.handle_time else None
|
||||||
|
mail_raw = body.mail_cert_time
|
||||||
|
update_mail = mail_raw is not None
|
||||||
|
mail_t = parse_datetime(mail_raw) if mail_raw and str(mail_raw).strip() else None
|
||||||
|
row = cert_crud.upsert_handle(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
sample_code=code,
|
||||||
|
collection_protocol_id=pid,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=ht,
|
||||||
|
handler_user_id=body.handler_user_id,
|
||||||
|
cert_image_urls=body.cert_image_urls or "",
|
||||||
|
remark=body.remark or "",
|
||||||
|
mail_cert_time=mail_t,
|
||||||
|
update_mail_cert_time=update_mail,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return success_response(cert_crud.row_to_dict(row), message="保存成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(400, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-insurances/{sample_code}")
|
||||||
|
async def get_sample_insurance(
|
||||||
|
sample_code: str,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: dict = Depends(_MENU),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
row = insurance_crud.get_by_sample_code(db, tenant_no, code)
|
||||||
|
if row is None:
|
||||||
|
return success_response(
|
||||||
|
{
|
||||||
|
"sampleCode": code,
|
||||||
|
"insuranceHandled": False,
|
||||||
|
"handleTime": "",
|
||||||
|
"handlerUserId": "",
|
||||||
|
"insuranceAttachments": "",
|
||||||
|
"policySendTime": "",
|
||||||
|
"handleRemark": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return success_response(insurance_crud.row_to_dict(row))
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/sample-insurances/{sample_code}")
|
||||||
|
async def put_sample_insurance(
|
||||||
|
sample_code: str,
|
||||||
|
body: SampleHandleBody,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: dict = Depends(_WRITE_MENU),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
pid = (body.collection_protocol_id or "").strip() or _resolve_protocol_id(
|
||||||
|
db, tenant_no, code
|
||||||
|
)
|
||||||
|
ht = parse_datetime(body.handle_time) if body.handle_time else None
|
||||||
|
policy_raw = body.policy_send_time
|
||||||
|
update_policy = policy_raw is not None
|
||||||
|
policy_t = (
|
||||||
|
parse_datetime(policy_raw) if policy_raw and str(policy_raw).strip() else None
|
||||||
|
)
|
||||||
|
row = insurance_crud.upsert_handle(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
sample_code=code,
|
||||||
|
collection_protocol_id=pid,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=ht,
|
||||||
|
handler_user_id=body.handler_user_id,
|
||||||
|
insurance_attachments=body.insurance_attachments or "",
|
||||||
|
handle_remark=body.handle_remark or body.remark or "",
|
||||||
|
policy_send_time=policy_t,
|
||||||
|
update_policy_send_time=update_policy,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return success_response(insurance_crud.row_to_dict(row), message="保存成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(400, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(400, str(e))
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""样本编码全链路纠错 API"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.menu_guard import require_admin_or_super_admin
|
||||||
|
from app.core.response import error_response, success_response
|
||||||
|
from app.models.models import SysUser
|
||||||
|
from app.services import sample_code_correction as svc
|
||||||
|
from app.services.sample_code_correction import SampleCodeCorrectionError
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["样本编码纠错"])
|
||||||
|
|
||||||
|
_MENU = require_admin_or_super_admin
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class SampleCodeCorrectionBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
reception_source: str = Field(..., alias="receptionSource")
|
||||||
|
old_sample_code: str = Field(..., alias="oldSampleCode")
|
||||||
|
new_sample_code: str = Field(..., alias="newSampleCode")
|
||||||
|
new_sample_code_id: Optional[str] = Field(None, alias="newSampleCodeId")
|
||||||
|
reason: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-code-corrections/preview")
|
||||||
|
async def preview_sample_code_correction(
|
||||||
|
body: SampleCodeCorrectionBody,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: dict = Depends(_MENU),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
data = svc.preview_correction(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
reception_source=body.reception_source,
|
||||||
|
old_sample_code=body.old_sample_code,
|
||||||
|
new_sample_code=body.new_sample_code,
|
||||||
|
new_sample_code_id=body.new_sample_code_id,
|
||||||
|
)
|
||||||
|
return success_response(data)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
return error_response(f"预览失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-code-corrections/apply")
|
||||||
|
async def apply_sample_code_correction(
|
||||||
|
body: SampleCodeCorrectionBody,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: dict = Depends(_MENU),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_user_id(user)
|
||||||
|
data = svc.apply_correction(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
reception_source=body.reception_source,
|
||||||
|
old_sample_code=body.old_sample_code,
|
||||||
|
new_sample_code=body.new_sample_code,
|
||||||
|
new_sample_code_id=body.new_sample_code_id,
|
||||||
|
reason=body.reason,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
return success_response(data, message="样本编码已更正")
|
||||||
|
except SampleCodeCorrectionError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(str(e))
|
||||||
|
except PermissionError as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
return error_response(f"更正失败: {e}")
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
"""样本运费批次 API"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.menu_guard import require_admin_or_super_admin
|
||||||
|
from app.core.response import error_response, success_response
|
||||||
|
from app.crud import sample_freight as freight_svc
|
||||||
|
from app.crud import sf_express_settlement as sf_crud
|
||||||
|
from app.models.models import SysUser
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["样本运费批次"])
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class SampleFreightItemBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
sample_source: str = Field(..., alias="sampleSource")
|
||||||
|
sample_id: str = Field(..., alias="sampleId")
|
||||||
|
protocol_id: Optional[str] = Field(None, alias="protocolId")
|
||||||
|
sample_code: Optional[str] = Field(None, alias="sampleCode")
|
||||||
|
|
||||||
|
|
||||||
|
class SampleFreightCreateBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
transport_mode: str = Field(..., alias="transportMode")
|
||||||
|
freight_amount: Optional[float] = Field(None, alias="freightAmount")
|
||||||
|
waybill_no: Optional[str] = Field(None, alias="waybillNo")
|
||||||
|
items: Optional[List[SampleFreightItemBody]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SampleFreightUpdateBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
transport_mode: Optional[str] = Field(None, alias="transportMode")
|
||||||
|
freight_amount: Optional[float] = Field(None, alias="freightAmount")
|
||||||
|
waybill_no: Optional[str] = Field(None, alias="waybillNo")
|
||||||
|
|
||||||
|
|
||||||
|
class SampleFreightItemsBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
items: List[SampleFreightItemBody] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class SfSettlementSettleBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
ids: List[str] = Field(default_factory=list)
|
||||||
|
freight_amount: Optional[float] = Field(None, alias="freightAmount")
|
||||||
|
|
||||||
|
|
||||||
|
class SfSettlementFillBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||||
|
|
||||||
|
freight_amount: float = Field(..., alias="freightAmount")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-freights")
|
||||||
|
async def list_sample_freights(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
transportMode: Optional[str] = None,
|
||||||
|
sampleSource: Optional[str] = None,
|
||||||
|
createTimeFrom: Optional[str] = None,
|
||||||
|
createTimeTo: Optional[str] = None,
|
||||||
|
includeItems: bool = Query(False),
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = freight_svc.list_freights(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
page=page,
|
||||||
|
page_size=pageSize,
|
||||||
|
keyword=keyword,
|
||||||
|
transport_mode=transportMode,
|
||||||
|
sample_source=sampleSource,
|
||||||
|
create_time_from=createTimeFrom,
|
||||||
|
create_time_to=createTimeTo,
|
||||||
|
include_items=includeItems,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": pageSize}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-freights/sample-options")
|
||||||
|
async def list_sample_freight_options(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pageSize: int = Query(20, ge=1, le=100, alias="pageSize"),
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
sampleSource: Optional[str] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
items, total = freight_svc.list_sample_options(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
page=page,
|
||||||
|
page_size=pageSize,
|
||||||
|
keyword=keyword,
|
||||||
|
sample_source=sampleSource,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": items, "total": total, "page": page, "pageSize": pageSize}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-freights/sf-settlements")
|
||||||
|
async def list_sf_express_settlements(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||||
|
waybillNo: Optional[str] = None,
|
||||||
|
settleStatus: Optional[str] = None,
|
||||||
|
createTimeFrom: Optional[str] = None,
|
||||||
|
createTimeTo: Optional[str] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
rows, total = sf_crud.list_sf_settlements(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
page=page,
|
||||||
|
page_size=pageSize,
|
||||||
|
waybill_no=waybillNo,
|
||||||
|
settle_status=settleStatus,
|
||||||
|
create_time_from=createTimeFrom,
|
||||||
|
create_time_to=createTimeTo,
|
||||||
|
)
|
||||||
|
return success_response(
|
||||||
|
{"list": rows, "total": total, "page": page, "pageSize": pageSize}
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-freights/sf-settlements/export")
|
||||||
|
async def export_sf_express_settlements(
|
||||||
|
waybillNo: Optional[str] = None,
|
||||||
|
settleStatus: Optional[str] = None,
|
||||||
|
createTimeFrom: Optional[str] = None,
|
||||||
|
createTimeTo: Optional[str] = None,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
rows = sf_crud.list_sf_settlements_for_export(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
waybill_no=waybillNo,
|
||||||
|
settle_status=settleStatus,
|
||||||
|
create_time_from=createTimeFrom,
|
||||||
|
create_time_to=createTimeTo,
|
||||||
|
)
|
||||||
|
return success_response({"list": rows})
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-freights/sf-settlements/settle")
|
||||||
|
async def settle_sf_express_settlements(
|
||||||
|
body: SfSettlementSettleBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
data = sf_crud.settle_sf_freights(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
user_info=user,
|
||||||
|
ids=body.ids,
|
||||||
|
freight_amount=body.freight_amount,
|
||||||
|
)
|
||||||
|
return success_response(data, message="结算成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-freights/sf-settlements/{freight_id}/fill-amount")
|
||||||
|
async def fill_sf_express_settlement_amount(
|
||||||
|
freight_id: str,
|
||||||
|
body: SfSettlementFillBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
data = sf_crud.fill_sf_freight_amount(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
user_info=user,
|
||||||
|
freight_id=freight_id,
|
||||||
|
freight_amount=body.freight_amount,
|
||||||
|
)
|
||||||
|
return success_response(data, message="补填成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-freights/{freight_id}")
|
||||||
|
async def get_sample_freight(
|
||||||
|
freight_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
data = freight_svc.get_freight_detail(db, freight_id, tenant_no, user_info=user)
|
||||||
|
return success_response(data)
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(404, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-freights")
|
||||||
|
async def create_sample_freight(
|
||||||
|
body: SampleFreightCreateBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
items = [it.model_dump(by_alias=True) for it in (body.items or [])]
|
||||||
|
data = freight_svc.create_freight(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
user_id=user_id,
|
||||||
|
transport_mode_raw=body.transport_mode,
|
||||||
|
freight_amount=body.freight_amount,
|
||||||
|
waybill_no=body.waybill_no,
|
||||||
|
waybill_supplied=True,
|
||||||
|
amount_supplied=True,
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
return success_response(data, message="创建成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/sample-freights/{freight_id}")
|
||||||
|
async def update_sample_freight(
|
||||||
|
freight_id: str,
|
||||||
|
body: SampleFreightUpdateBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
data = freight_svc.update_freight(
|
||||||
|
db,
|
||||||
|
freight_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
user_id=user_id,
|
||||||
|
transport_mode_raw=body.transport_mode,
|
||||||
|
freight_amount=body.freight_amount,
|
||||||
|
waybill_no=body.waybill_no,
|
||||||
|
waybill_supplied="waybill_no" in body.model_fields_set,
|
||||||
|
amount_supplied="freight_amount" in body.model_fields_set,
|
||||||
|
)
|
||||||
|
return success_response(data, message="更新成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
msg = str(e)
|
||||||
|
code = 404 if "不存在" in msg else 400
|
||||||
|
return error_response(code, msg)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-freights/{freight_id}/link")
|
||||||
|
async def link_sample_freight(
|
||||||
|
freight_id: str,
|
||||||
|
body: SampleFreightItemsBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
items = [it.model_dump(by_alias=True) for it in (body.items or [])]
|
||||||
|
data = freight_svc.link_samples(
|
||||||
|
db,
|
||||||
|
freight_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
user_id=user_id,
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
return success_response(data, message="关联成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
msg = str(e)
|
||||||
|
code = 404 if "不存在" in msg else 400
|
||||||
|
return error_response(code, msg)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sample-freights/{freight_id}/unlink")
|
||||||
|
async def unlink_sample_freight(
|
||||||
|
freight_id: str,
|
||||||
|
body: SampleFreightItemsBody,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
items = [it.model_dump(by_alias=True) for it in (body.items or [])]
|
||||||
|
data = freight_svc.unlink_samples(
|
||||||
|
db,
|
||||||
|
freight_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
user_id=user_id,
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
return success_response(data, message="已取消关联")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
msg = str(e)
|
||||||
|
code = 404 if "不存在" in msg else 400
|
||||||
|
return error_response(code, msg)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/sample-freights/{freight_id}")
|
||||||
|
async def delete_sample_freight(
|
||||||
|
freight_id: str,
|
||||||
|
user: dict = Depends(require_admin_or_super_admin),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
data = freight_svc.delete_freight(
|
||||||
|
db,
|
||||||
|
freight_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_info=user,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
return success_response(data, message="删除成功")
|
||||||
|
except PermissionError as e:
|
||||||
|
return error_response(403, str(e))
|
||||||
|
except ValueError as e:
|
||||||
|
msg = str(e)
|
||||||
|
code = 404 if "不存在" in msg else 400
|
||||||
|
return error_response(code, msg)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""样本储存证质检信息 API"""
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.menu_guard import require_any_menu_code, require_strict_any_menu_code
|
||||||
|
from app.core.response import error_response, success_response
|
||||||
|
from app.crud import sample_storage_cert_qc as svc
|
||||||
|
from app.models.models import SysUser
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/web/admin", tags=["储存证质检信息"])
|
||||||
|
|
||||||
|
# 市场 / 质检 / 生产三端共用读写(方案 A:完整表后写覆盖)
|
||||||
|
READ_MENUS = (
|
||||||
|
"biz:qcWorkOrder:list",
|
||||||
|
"biz:myQcTask:list",
|
||||||
|
"production:work-order:list",
|
||||||
|
"production:my-task:list",
|
||||||
|
"cell-storage:sample-info:list",
|
||||||
|
)
|
||||||
|
WRITE_MENUS = READ_MENUS
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||||
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||||
|
if not sys_user or not sys_user.tenant_no:
|
||||||
|
raise PermissionError("用户租户信息缺失")
|
||||||
|
return sys_user.tenant_no
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_user_id(user: dict) -> str:
|
||||||
|
user_id = user.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise PermissionError("用户身份信息缺失")
|
||||||
|
return str(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class UpsertBody(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
qc_work_order_id: Optional[str] = Field(None, alias="qcWorkOrderId")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sample-storage-cert-qc/{sample_code}")
|
||||||
|
async def get_sample_storage_cert_qc(
|
||||||
|
sample_code: str,
|
||||||
|
user: dict = Depends(require_any_menu_code(*READ_MENUS)),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
detail = svc.get_detail(db, tenant_no, sample_code)
|
||||||
|
if detail is None:
|
||||||
|
return error_response(404, "样本不存在")
|
||||||
|
return success_response(detail)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/sample-storage-cert-qc/{sample_code}")
|
||||||
|
async def put_sample_storage_cert_qc(
|
||||||
|
sample_code: str,
|
||||||
|
body: UpsertBody,
|
||||||
|
user: dict = Depends(require_strict_any_menu_code(*WRITE_MENUS)),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
user_id = _get_current_user_id(user)
|
||||||
|
try:
|
||||||
|
detail = svc.upsert(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
user_id=user_id,
|
||||||
|
sample_code=sample_code,
|
||||||
|
payload=body.payload,
|
||||||
|
qc_work_order_id=body.qc_work_order_id,
|
||||||
|
)
|
||||||
|
return success_response(detail, message="保存成功")
|
||||||
|
except ValueError as e:
|
||||||
|
return error_response(400, str(e))
|
||||||
@@ -467,8 +467,11 @@ async def approve_task(
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
tenant_no = user.get("tenant_no", "zkxw")
|
tenant_no = user.get("tenant_no", "zkxw")
|
||||||
|
actor_user_id = user.get("user_id") or user.get("id")
|
||||||
engine = WorkflowEngine(db)
|
engine = WorkflowEngine(db)
|
||||||
instance = engine.approve_task(task_id, body.comment or "", tenant_no)
|
instance = engine.approve_task(
|
||||||
|
task_id, body.comment or "", tenant_no, actor_user_id=actor_user_id
|
||||||
|
)
|
||||||
return success_response({"instanceId": instance.id, "status": instance.status}, message="审批成功")
|
return success_response({"instanceId": instance.id, "status": instance.status}, message="审批成功")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
@@ -483,8 +486,11 @@ async def reject_task(
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
tenant_no = user.get("tenant_no", "zkxw")
|
tenant_no = user.get("tenant_no", "zkxw")
|
||||||
|
actor_user_id = user.get("user_id") or user.get("id")
|
||||||
engine = WorkflowEngine(db)
|
engine = WorkflowEngine(db)
|
||||||
instance = engine.reject_task(task_id, body.comment or "", tenant_no)
|
instance = engine.reject_task(
|
||||||
|
task_id, body.comment or "", tenant_no, actor_user_id=actor_user_id
|
||||||
|
)
|
||||||
return success_response({"instanceId": instance.id, "status": instance.status}, message="已驳回")
|
return success_response({"instanceId": instance.id, "status": instance.status}, message="已驳回")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return error_response(400, str(e))
|
return error_response(400, str(e))
|
||||||
|
|||||||
@@ -221,8 +221,11 @@ async def get_current_user_by_token(
|
|||||||
detail="用户已被禁用",
|
detail="用户已被禁用",
|
||||||
)
|
)
|
||||||
|
|
||||||
web_role = resolve_web_role_for_user(db, user.id)
|
tenant_no = (getattr(user, "tenant_no", None) or "").strip() or None
|
||||||
role_names = get_user_role_names(db, user.id)
|
web_role = resolve_web_role_for_user(
|
||||||
|
db, user.id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
role_names = get_user_role_names(db, user.id, tenant_no=tenant_no)
|
||||||
|
|
||||||
phone = (getattr(user, "phone_no", None) or "").strip() or None
|
phone = (getattr(user, "phone_no", None) or "").strip() or None
|
||||||
email = (getattr(user, "email", None) or "").strip() or None
|
email = (getattr(user, "email", None) or "").strip() or None
|
||||||
@@ -244,7 +247,7 @@ async def get_current_user_by_token(
|
|||||||
"real_name": user.nick_name,
|
"real_name": user.nick_name,
|
||||||
"role": web_role,
|
"role": web_role,
|
||||||
"role_names": role_names,
|
"role_names": role_names,
|
||||||
"tenant_no": (getattr(user, "tenant_no", None) or "").strip() or None,
|
"tenant_no": tenant_no,
|
||||||
"region_id": user.dept_id,
|
"region_id": user.dept_id,
|
||||||
"region_name": dept_name,
|
"region_name": dept_name,
|
||||||
"dept_name": dept_name,
|
"dept_name": dept_name,
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
|
from pydantic import field_validator
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# 已知弱/占位 JWT 密钥:启动时一律拒绝(勿再当作默认值)
|
||||||
|
_INSECURE_JWT_SECRET_KEYS = frozenset(
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"secret",
|
||||||
|
"changeme",
|
||||||
|
"change-me",
|
||||||
|
"your-secret-key",
|
||||||
|
"your-secret-key-change-in-production",
|
||||||
|
"your-secret-key-change-in-production-please-use-random-string",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_MIN_JWT_SECRET_KEY_LEN = 32
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
"""应用配置"""
|
"""应用配置"""
|
||||||
@@ -15,11 +30,28 @@ class Settings(BaseSettings):
|
|||||||
# 数据库配置
|
# 数据库配置
|
||||||
DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/sanqiang"
|
DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/sanqiang"
|
||||||
|
|
||||||
# JWT配置
|
# JWT配置(必须通过环境变量配置;禁止弱默认值)
|
||||||
SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string"
|
SECRET_KEY: str = ""
|
||||||
ALGORITHM: str = "HS256"
|
ALGORITHM: str = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30天
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 * 60 # 30天
|
||||||
|
|
||||||
|
@field_validator("SECRET_KEY")
|
||||||
|
@classmethod
|
||||||
|
def secret_key_must_be_strong(cls, value: str) -> str:
|
||||||
|
key = (value or "").strip()
|
||||||
|
if key.lower() in _INSECURE_JWT_SECRET_KEYS or key in _INSECURE_JWT_SECRET_KEYS:
|
||||||
|
raise ValueError(
|
||||||
|
"SECRET_KEY 未配置或仍为弱占位值。"
|
||||||
|
"请在环境文件中设置至少 32 字符的随机密钥,例如:"
|
||||||
|
'python -c "import secrets; print(secrets.token_urlsafe(48))"'
|
||||||
|
)
|
||||||
|
if len(key) < _MIN_JWT_SECRET_KEY_LEN:
|
||||||
|
raise ValueError(
|
||||||
|
f"SECRET_KEY 长度不足(当前 {len(key)},至少 {_MIN_JWT_SECRET_KEY_LEN})。"
|
||||||
|
"请使用 secrets.token_urlsafe(48) 等方式生成。"
|
||||||
|
)
|
||||||
|
return key
|
||||||
|
|
||||||
# CORS配置
|
# CORS配置
|
||||||
BACKEND_CORS_ORIGINS: list = [
|
BACKEND_CORS_ORIGINS: list = [
|
||||||
"http://localhost:5173", # art-design-pro 开发服务器
|
"http://localhost:5173", # art-design-pro 开发服务器
|
||||||
@@ -101,13 +133,23 @@ class Settings(BaseSettings):
|
|||||||
WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID
|
WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID
|
||||||
|
|
||||||
# 微信服务号(OA 审批/抄送通知)
|
# 微信服务号(OA 审批/抄送通知)
|
||||||
# 总开关:false 时不发送审批/抄送/申请人进度模板消息(模板未就绪或微信能力不可用时关闭)
|
# 总开关:false 时不发送审批/抄送/申请人进度/评论@提到/基础实验交付提醒/样本采集通知模板消息
|
||||||
WX_MP_NOTIFY_ENABLED: bool = False
|
WX_MP_NOTIFY_ENABLED: bool = False
|
||||||
WX_MP_APPID: str = ""
|
WX_MP_APPID: str = ""
|
||||||
WX_MP_SECRET: str = ""
|
WX_MP_SECRET: str = ""
|
||||||
WX_MP_TEMPLATE_APPROVAL: str = ""
|
WX_MP_TEMPLATE_APPROVAL: str = ""
|
||||||
WX_MP_TEMPLATE_CC: str = ""
|
WX_MP_TEMPLATE_CC: str = ""
|
||||||
WX_MP_TEMPLATE_APPLICANT_PROGRESS: str = ""
|
WX_MP_TEMPLATE_APPLICANT_PROGRESS: str = ""
|
||||||
|
# 基础实验交付日前一天提醒(出库提醒模板)
|
||||||
|
WX_MP_TEMPLATE_BASIC_EXPERIMENT_DELIVERY: str = (
|
||||||
|
"oQNU3t0Q5kwc4KPJ8Z6yPA-k6qlsH9pikSCIb4eAwgM"
|
||||||
|
)
|
||||||
|
# 协议新增样本采集通知(工单类模板)
|
||||||
|
# 工单标题 thing2 / 发起人 thing8 / 发起时间 time3 / 任务类型 phrase10 / 工单类型 thing17(样本类型名称)
|
||||||
|
WX_MP_TEMPLATE_SAMPLE_COLLECT: str = (
|
||||||
|
"iOnQ9k2mavAOwFbp_6yH4t1SHqpEkaiwNWKueY0kpwE"
|
||||||
|
)
|
||||||
|
WX_MP_SAMPLE_COLLECT_ROLE_NAME: str = "样本采集通知人员"
|
||||||
# 审批模板:申请事项/操作人(申请人)/当前环节/操作时间/申请类型(流程名)
|
# 审批模板:申请事项/操作人(申请人)/当前环节/操作时间/申请类型(流程名)
|
||||||
# 生产字段:thing12 / thing18 / thing7 / time17 / thing8
|
# 生产字段:thing12 / thing18 / thing7 / time17 / thing8
|
||||||
WX_MP_TPL_FIELD_TITLE: str = "thing12"
|
WX_MP_TPL_FIELD_TITLE: str = "thing12"
|
||||||
@@ -126,6 +168,18 @@ class Settings(BaseSettings):
|
|||||||
WX_MP_TPL_PROGRESS_FIELD_APPROVER: str = "thing6"
|
WX_MP_TPL_PROGRESS_FIELD_APPROVER: str = "thing6"
|
||||||
WX_MP_TPL_PROGRESS_FIELD_RESULT: str = "const4"
|
WX_MP_TPL_PROGRESS_FIELD_RESULT: str = "const4"
|
||||||
WX_MP_TPL_PROGRESS_FIELD_TIME: str = "time11"
|
WX_MP_TPL_PROGRESS_FIELD_TIME: str = "time11"
|
||||||
|
# 基础实验交付提醒:客户名称/产品名称/件数/收货联系人/出库时间
|
||||||
|
WX_MP_TPL_BE_DELIVERY_FIELD_CUSTOMER: str = "thing6"
|
||||||
|
WX_MP_TPL_BE_DELIVERY_FIELD_PRODUCT: str = "thing10"
|
||||||
|
WX_MP_TPL_BE_DELIVERY_FIELD_QTY: str = "character_string8"
|
||||||
|
WX_MP_TPL_BE_DELIVERY_FIELD_CONTACT: str = "thing7"
|
||||||
|
WX_MP_TPL_BE_DELIVERY_FIELD_TIME: str = "time14"
|
||||||
|
# 样本采集通知:工单标题/发起人/发起时间/任务类型/工单类型(样本类型名称)
|
||||||
|
WX_MP_TPL_SC_FIELD_TITLE: str = "thing2"
|
||||||
|
WX_MP_TPL_SC_FIELD_APPLICANT: str = "thing8"
|
||||||
|
WX_MP_TPL_SC_FIELD_TIME: str = "time3"
|
||||||
|
WX_MP_TPL_SC_FIELD_TYPE: str = "phrase10"
|
||||||
|
WX_MP_TPL_SC_FIELD_SAMPLE_TYPE: str = "thing17"
|
||||||
|
|
||||||
# 与 docs/sql/zkxw.sql 基线一致:登录按租户 + sys_user.user_no
|
# 与 docs/sql/zkxw.sql 基线一致:登录按租户 + sys_user.user_no
|
||||||
DEFAULT_TENANT_NO: str = "zkxw"
|
DEFAULT_TENANT_NO: str = "zkxw"
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def _resolve_user_role_scopes(
|
|||||||
"""
|
"""
|
||||||
from app.services.web_auth_roles import get_effective_role_ids
|
from app.services.web_auth_roles import get_effective_role_ids
|
||||||
|
|
||||||
role_ids = get_effective_role_ids(db, user_id)
|
role_ids = get_effective_role_ids(db, user_id, tenant_no=tenant_no)
|
||||||
if not role_ids:
|
if not role_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from fastapi import Depends, HTTPException
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.auth import get_current_user
|
from app.core.auth import get_current_user
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.models.models import SysUser
|
||||||
from app.services.web_auth_roles import iter_user_menu_perm_codes
|
from app.services.web_auth_roles import iter_user_menu_perm_codes
|
||||||
|
|
||||||
ADMIN_ROLES = {"admin", "super_admin"}
|
ADMIN_ROLES = {"admin", "super_admin"}
|
||||||
@@ -57,3 +59,92 @@ def require_any_menu_code(*menu_codes: str) -> Callable:
|
|||||||
"""接口菜单权限守卫(已统一放行 menuCode 校验,仅保留运营端登录)。"""
|
"""接口菜单权限守卫(已统一放行 menuCode 校验,仅保留运营端登录)。"""
|
||||||
_ = menu_codes
|
_ = menu_codes
|
||||||
return require_admin_or_super_admin
|
return require_admin_or_super_admin
|
||||||
|
|
||||||
|
|
||||||
|
def _strict_user_identity(db: Session, user: dict) -> tuple[str, str]:
|
||||||
|
"""从用户表读取当前租户,拒绝信任 JWT/请求中的租户覆盖。"""
|
||||||
|
user_id = str((user or {}).get("user_id") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
return "", ""
|
||||||
|
row = (
|
||||||
|
db.query(SysUser.id, SysUser.tenant_no)
|
||||||
|
.filter(
|
||||||
|
SysUser.id == user_id,
|
||||||
|
SysUser.del_flag == 0,
|
||||||
|
SysUser.status == "1",
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return "", ""
|
||||||
|
return str(row[0] or "").strip(), str(row[1] or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def strict_user_has_any_menu_perm(
|
||||||
|
db: Session,
|
||||||
|
user: dict,
|
||||||
|
menu_codes: Iterable[str],
|
||||||
|
) -> bool:
|
||||||
|
"""按用户真实租户严格判断任一菜单权限,含直接与职位继承角色。"""
|
||||||
|
required = {
|
||||||
|
str(code or "").strip() for code in menu_codes if str(code or "").strip()
|
||||||
|
}
|
||||||
|
if not required:
|
||||||
|
return False
|
||||||
|
user_id, tenant_no = _strict_user_identity(db, user)
|
||||||
|
if not user_id or not tenant_no:
|
||||||
|
return False
|
||||||
|
granted = iter_user_menu_perm_codes(db, user_id, tenant_no=tenant_no)
|
||||||
|
return bool(required.intersection(granted))
|
||||||
|
|
||||||
|
|
||||||
|
def strict_user_has_menu_perm(db: Session, user: dict, menu_code: str) -> bool:
|
||||||
|
"""按用户真实租户严格判断单个菜单权限。"""
|
||||||
|
return strict_user_has_any_menu_perm(db, user, (menu_code,))
|
||||||
|
|
||||||
|
|
||||||
|
def assert_strict_menu_code(
|
||||||
|
db: Session,
|
||||||
|
user: dict,
|
||||||
|
menu_code: str,
|
||||||
|
) -> None:
|
||||||
|
"""路由内部字段级权限断言。"""
|
||||||
|
if not strict_user_has_menu_perm(db, user, menu_code):
|
||||||
|
raise HTTPException(status_code=403, detail="无对应菜单权限")
|
||||||
|
|
||||||
|
|
||||||
|
def require_strict_menu_code(menu_code: str) -> Callable:
|
||||||
|
"""租户化的精确菜单权限依赖;与历史宽松守卫并存。"""
|
||||||
|
required = str(menu_code or "").strip()
|
||||||
|
|
||||||
|
async def dependency(
|
||||||
|
user: dict = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
assert_strict_menu_code(db, user, required)
|
||||||
|
return user
|
||||||
|
|
||||||
|
dependency.strict_menu_codes = (required,)
|
||||||
|
return dependency
|
||||||
|
|
||||||
|
|
||||||
|
def require_strict_any_menu_code(*menu_codes: str) -> Callable:
|
||||||
|
"""租户化的任一菜单权限依赖。"""
|
||||||
|
required = tuple(
|
||||||
|
dict.fromkeys(
|
||||||
|
str(code or "").strip()
|
||||||
|
for code in menu_codes
|
||||||
|
if str(code or "").strip()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def dependency(
|
||||||
|
user: dict = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
if not strict_user_has_any_menu_perm(db, user, required):
|
||||||
|
raise HTTPException(status_code=403, detail="无对应菜单权限")
|
||||||
|
return user
|
||||||
|
|
||||||
|
dependency.strict_menu_codes = required
|
||||||
|
return dependency
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""移动市场接口的严格菜单权限守卫。"""
|
||||||
|
from typing import Callable, Iterable
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.auth import get_current_user
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.services.web_auth_roles import (
|
||||||
|
iter_user_menu_perm_codes,
|
||||||
|
resolve_web_role_for_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def require_market_menu_permission(
|
||||||
|
*,
|
||||||
|
user_id: str,
|
||||||
|
tenant_no: str,
|
||||||
|
required: str,
|
||||||
|
granted: Iterable[str],
|
||||||
|
) -> bool:
|
||||||
|
"""严格判断当前租户用户是否拥有指定移动市场权限码。"""
|
||||||
|
uid = (user_id or "").strip()
|
||||||
|
tenant = (tenant_no or "").strip()
|
||||||
|
code = (required or "").strip()
|
||||||
|
if not uid or not tenant or not code:
|
||||||
|
return False
|
||||||
|
return code in {(item or "").strip() for item in granted if item}
|
||||||
|
|
||||||
|
|
||||||
|
def user_has_tenant_menu_perm(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
user_id: str,
|
||||||
|
tenant_no: str,
|
||||||
|
perm_code: str,
|
||||||
|
) -> bool:
|
||||||
|
"""按显式租户读取用户权限,不回退默认租户。"""
|
||||||
|
tenant = (tenant_no or "").strip()
|
||||||
|
uid = (user_id or "").strip()
|
||||||
|
if not tenant or not uid:
|
||||||
|
return False
|
||||||
|
granted = iter_user_menu_perm_codes(db, uid, tenant_no=tenant)
|
||||||
|
return require_market_menu_permission(
|
||||||
|
user_id=uid,
|
||||||
|
tenant_no=tenant,
|
||||||
|
required=perm_code,
|
||||||
|
granted=granted,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def require_market_menu_code(menu_code: str) -> Callable:
|
||||||
|
"""为移动市场 API 提供租户化、精确匹配的菜单权限依赖。"""
|
||||||
|
required = (menu_code or "").strip()
|
||||||
|
|
||||||
|
async def dependency(
|
||||||
|
user: dict = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
tenant_no = str(user.get("tenant_no") or "").strip()
|
||||||
|
user_id = str(user.get("user_id") or "").strip()
|
||||||
|
if not tenant_no or not user_id:
|
||||||
|
raise HTTPException(status_code=403, detail="用户租户或身份信息缺失")
|
||||||
|
tenant_role = resolve_web_role_for_user(
|
||||||
|
db, user_id, tenant_no=tenant_no
|
||||||
|
)
|
||||||
|
if tenant_role != "super_admin" and not user_has_tenant_menu_perm(
|
||||||
|
db,
|
||||||
|
user_id=user_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
perm_code=required,
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=403, detail="无移动端菜单权限")
|
||||||
|
return user
|
||||||
|
|
||||||
|
return dependency
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""协议状态展示(含作废类型后缀)。"""
|
"""协议状态展示(含作废类型后缀)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Dict, Optional
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -12,6 +12,27 @@ VOID_TYPE_DICT = "xyzflx"
|
|||||||
# 协议/捐赠单无效态(与样本接收、生产工单列表过滤口径一致)
|
# 协议/捐赠单无效态(与样本接收、生产工单列表过滤口径一致)
|
||||||
INACTIVE_PROTOCOL_STATUSES = ("yzf", "sdfq")
|
INACTIVE_PROTOCOL_STATUSES = ("yzf", "sdfq")
|
||||||
|
|
||||||
|
# 科研捐赠质检列表/角标默认协议状态:待生效、生效中
|
||||||
|
DEFAULT_QC_DONATION_PROTOCOL_STATUSES = ("dsx", "sxz")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_research_donation_protocol_status_filter(
|
||||||
|
protocol_status: Optional[str],
|
||||||
|
) -> Tuple[str, List[str]]:
|
||||||
|
"""解析科研捐赠质检列表的协议状态筛选语义。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
("in", statuses) — 按所选状态 IN 过滤
|
||||||
|
("none", []) — 显式空选择,无匹配结果
|
||||||
|
("default", DEFAULT_QC_DONATION_PROTOCOL_STATUSES) — 未传参,与列表默认一致
|
||||||
|
"""
|
||||||
|
if protocol_status is None:
|
||||||
|
return ("default", list(DEFAULT_QC_DONATION_PROTOCOL_STATUSES))
|
||||||
|
statuses = [s.strip() for s in str(protocol_status).split(",") if s.strip()]
|
||||||
|
if not statuses:
|
||||||
|
return ("none", [])
|
||||||
|
return ("in", statuses)
|
||||||
|
|
||||||
_PROTOCOL_STATUS_LABELS = {
|
_PROTOCOL_STATUS_LABELS = {
|
||||||
"dsx": "待生效",
|
"dsx": "待生效",
|
||||||
"sxz": "生效中",
|
"sxz": "生效中",
|
||||||
@@ -34,6 +55,16 @@ _VOID_TYPE_FALLBACK_LABELS = {
|
|||||||
"jcbhg": "检测不合格",
|
"jcbhg": "检测不合格",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 非法来源状态视为终态,不允许再转出
|
||||||
|
_PROTOCOL_STATUS_TRANSITIONS = {
|
||||||
|
"dsx": ("sxz", "yzf", "sdfq"),
|
||||||
|
"sxz": ("ygq", "yzf", "zky", "sdfq"),
|
||||||
|
}
|
||||||
|
_DONATION_STATUS_TRANSITIONS = {
|
||||||
|
"dsx": ("sxz", "yzf", "sdfq"),
|
||||||
|
"sxz": ("ygq", "yzf", "zckc", "sdfq"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def resolve_void_type_label(
|
def resolve_void_type_label(
|
||||||
db: Optional[Session],
|
db: Optional[Session],
|
||||||
@@ -59,6 +90,23 @@ def resolve_void_type_label(
|
|||||||
return _VOID_TYPE_FALLBACK_LABELS.get(code, code)
|
return _VOID_TYPE_FALLBACK_LABELS.get(code, code)
|
||||||
|
|
||||||
|
|
||||||
|
def status_transition_denied_message(current: str, target: str, *, kind: str = "protocol") -> str:
|
||||||
|
"""状态流转被拒绝时的提示,状态码展示为中文名称。"""
|
||||||
|
current_label = format_protocol_status_label(current, kind=kind)
|
||||||
|
target_label = format_protocol_status_label(target, kind=kind)
|
||||||
|
return f"状态{current_label}不可转为{target_label}"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_protocol_status_transition(current: str, target: str, *, kind: str = "protocol") -> None:
|
||||||
|
cur = (current or "").strip()
|
||||||
|
nxt = (target or "").strip()
|
||||||
|
if cur == nxt:
|
||||||
|
return
|
||||||
|
table = _DONATION_STATUS_TRANSITIONS if kind == "donation" else _PROTOCOL_STATUS_TRANSITIONS
|
||||||
|
if nxt not in table.get(cur, ()):
|
||||||
|
raise ValueError(status_transition_denied_message(cur, nxt, kind=kind))
|
||||||
|
|
||||||
|
|
||||||
def format_protocol_status_label(
|
def format_protocol_status_label(
|
||||||
status: str,
|
status: str,
|
||||||
void_type: Optional[str] = None,
|
void_type: Optional[str] = None,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
from app.crud.protocol_donation_convert import assert_biz_no_available
|
from app.crud.protocol_donation_convert import assert_biz_no_available
|
||||||
|
from app.models.models import BizBizNoSequence
|
||||||
|
|
||||||
PREFIX_COLLECTION = "XBCC"
|
PREFIX_COLLECTION = "XBCC"
|
||||||
PREFIX_DONATION = "JZCC"
|
PREFIX_DONATION = "JZCC"
|
||||||
@@ -24,6 +25,46 @@ def _format_biz_no(prefix: str, biz_date: str, seq: int) -> str:
|
|||||||
return f"{prefix}{biz_date}{seq:0{SEQ_DIGITS}d}"
|
return f"{prefix}{biz_date}{seq:0{SEQ_DIGITS}d}"
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_sqlite_sequence(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
prefix: str,
|
||||||
|
biz_date: str,
|
||||||
|
user_id: str,
|
||||||
|
now: datetime,
|
||||||
|
) -> int:
|
||||||
|
"""SQLite 测试环境使用 ORM 分配;生产 MySQL 仍走原子 SQL。"""
|
||||||
|
row = db.query(BizBizNoSequence).filter(
|
||||||
|
BizBizNoSequence.tenant_no == tenant_no,
|
||||||
|
BizBizNoSequence.prefix == prefix,
|
||||||
|
BizBizNoSequence.biz_date == biz_date,
|
||||||
|
BizBizNoSequence.del_flag == 0,
|
||||||
|
).first()
|
||||||
|
if row:
|
||||||
|
row.last_seq = int(row.last_seq or 0) + 1
|
||||||
|
row.update_by = user_id
|
||||||
|
row.update_time = now
|
||||||
|
else:
|
||||||
|
row = BizBizNoSequence(
|
||||||
|
id=generate_uuid(),
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
prefix=prefix,
|
||||||
|
biz_date=biz_date,
|
||||||
|
last_seq=1,
|
||||||
|
order_num=0,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
db.flush()
|
||||||
|
return int(row.last_seq)
|
||||||
|
|
||||||
|
|
||||||
def allocate_next_biz_no(
|
def allocate_next_biz_no(
|
||||||
db: Session,
|
db: Session,
|
||||||
*,
|
*,
|
||||||
@@ -41,6 +82,27 @@ def allocate_next_biz_no(
|
|||||||
date_str = now.strftime("%Y%m%d")
|
date_str = now.strftime("%Y%m%d")
|
||||||
uid = user_id or ""
|
uid = user_id or ""
|
||||||
|
|
||||||
|
dialect_name = getattr(getattr(db.get_bind(), "dialect", None), "name", "")
|
||||||
|
if dialect_name == "sqlite":
|
||||||
|
seq_int = _allocate_sqlite_sequence(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
prefix=prefix,
|
||||||
|
biz_date=date_str,
|
||||||
|
user_id=uid,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
if seq_int > MAX_SEQ:
|
||||||
|
raise ValueError(f"当日{prefix}协议号序号已用尽(最大 {MAX_SEQ})")
|
||||||
|
biz_no = _format_biz_no(prefix, date_str, seq_int)
|
||||||
|
assert_biz_no_available(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
biz_no=biz_no,
|
||||||
|
for_donation=(kind == "donation"),
|
||||||
|
)
|
||||||
|
return biz_no
|
||||||
|
|
||||||
db.execute(
|
db.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from app.models.models import (
|
|||||||
BizStorageFacilitySlot,
|
BizStorageFacilitySlot,
|
||||||
)
|
)
|
||||||
from app.services.storage_facility_code import build_full_slot_code
|
from app.services.storage_facility_code import build_full_slot_code
|
||||||
|
from app.crud.storage_facility import search_slot_ids_by_keyword
|
||||||
|
|
||||||
_CELL_PRODUCT_STATUSES = frozenset(
|
_CELL_PRODUCT_STATUSES = frozenset(
|
||||||
{"frozen", "in_production", "sold", "used", "voided", "thawed"}
|
{"frozen", "in_production", "sold", "used", "voided", "thawed"}
|
||||||
@@ -289,20 +290,27 @@ def list_cell_products(
|
|||||||
source_type: Optional[str] = None,
|
source_type: Optional[str] = None,
|
||||||
user_info=None,
|
user_info=None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
kw = (keyword or "").strip()
|
||||||
|
matched_slot_ids: List[str] = (
|
||||||
|
search_slot_ids_by_keyword(db, tenant_no, kw) if kw else []
|
||||||
|
)
|
||||||
|
|
||||||
def _filtered():
|
def _filtered():
|
||||||
q0 = db.query(BizCellProduct).filter(
|
q0 = db.query(BizCellProduct).filter(
|
||||||
BizCellProduct.del_flag == 0,
|
BizCellProduct.del_flag == 0,
|
||||||
BizCellProduct.tenant_no == tenant_no,
|
BizCellProduct.tenant_no == tenant_no,
|
||||||
)
|
)
|
||||||
kw = (keyword or "").strip()
|
|
||||||
if kw:
|
if kw:
|
||||||
like = f"%{kw}%"
|
like = f"%{kw}%"
|
||||||
q0 = q0.filter(
|
keyword_conds = [
|
||||||
or_(
|
BizCellProduct.product_code.ilike(like),
|
||||||
BizCellProduct.product_code.ilike(like),
|
BizCellProduct.product_name.ilike(like),
|
||||||
BizCellProduct.product_name.ilike(like),
|
]
|
||||||
|
if matched_slot_ids:
|
||||||
|
keyword_conds.append(
|
||||||
|
BizCellProduct.storage_slot_id.in_(matched_slot_ids)
|
||||||
)
|
)
|
||||||
)
|
q0 = q0.filter(or_(*keyword_conds))
|
||||||
pc = (product_code or "").strip()
|
pc = (product_code or "").strip()
|
||||||
if pc:
|
if pc:
|
||||||
q0 = q0.filter(BizCellProduct.product_code.like(f"%{pc}%"))
|
q0 = q0.filter(BizCellProduct.product_code.like(f"%{pc}%"))
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
"""按行同步子表。
|
||||||
|
|
||||||
|
带 ID 的行原地修改,没有 ID 时按业务键对上尚未占用的原行,其余才新增。
|
||||||
|
本次提交里没有的活动行才软删除。不要整表软删后再插入,否则外键会指向已删除的行。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
|
||||||
|
|
||||||
|
def _item_id(item: Dict[str, Any]) -> str:
|
||||||
|
return str(item.get("id") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_key(row: Any):
|
||||||
|
order_num = getattr(row, "order_num", None)
|
||||||
|
create_time = getattr(row, "create_time", None)
|
||||||
|
return (
|
||||||
|
order_num if order_num is not None else 0,
|
||||||
|
create_time or datetime.min,
|
||||||
|
str(getattr(row, "id", "") or ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_child_rows(
|
||||||
|
db: Session,
|
||||||
|
model,
|
||||||
|
*,
|
||||||
|
parent_attr: str,
|
||||||
|
parent_id: str,
|
||||||
|
items: List[Dict[str, Any]],
|
||||||
|
build_row: Callable[[str], Any],
|
||||||
|
apply_fields: Callable[[Any, Dict[str, Any], int, datetime], None],
|
||||||
|
tenant_no: Optional[str] = None,
|
||||||
|
tenant_attr: str = "tenant_no",
|
||||||
|
user_id: Optional[str] = None,
|
||||||
|
now: Optional[datetime] = None,
|
||||||
|
natural_key: Optional[Callable[[Dict[str, Any]], str]] = None,
|
||||||
|
row_natural_key: Optional[Callable[[Any], str]] = None,
|
||||||
|
duplicate_message: str = "编号重复",
|
||||||
|
foreign_message: str = "记录不属于当前数据",
|
||||||
|
extra_filters: Optional[Dict[str, Any]] = None,
|
||||||
|
reuse_deleted: bool = False,
|
||||||
|
) -> List[Any]:
|
||||||
|
now = now or datetime.now()
|
||||||
|
payload = list(items or [])
|
||||||
|
seen_ids = set()
|
||||||
|
for item in payload:
|
||||||
|
row_id = _item_id(item)
|
||||||
|
if not row_id:
|
||||||
|
continue
|
||||||
|
if row_id in seen_ids:
|
||||||
|
raise ValueError(duplicate_message)
|
||||||
|
seen_ids.add(row_id)
|
||||||
|
|
||||||
|
def _scoped(query):
|
||||||
|
query = query.filter(getattr(model, parent_attr) == parent_id)
|
||||||
|
if tenant_no is not None and hasattr(model, tenant_attr):
|
||||||
|
query = query.filter(getattr(model, tenant_attr) == tenant_no)
|
||||||
|
for key, value in (extra_filters or {}).items():
|
||||||
|
query = query.filter(getattr(model, key) == value)
|
||||||
|
return query
|
||||||
|
|
||||||
|
active_rows = _scoped(db.query(model).filter(model.del_flag == 0)).all()
|
||||||
|
active_by_id = {row.id: row for row in active_rows}
|
||||||
|
|
||||||
|
for row_id in seen_ids:
|
||||||
|
if row_id in active_by_id:
|
||||||
|
continue
|
||||||
|
other = db.query(model).filter(model.id == row_id).first()
|
||||||
|
if other is None:
|
||||||
|
continue
|
||||||
|
other_parent = getattr(other, parent_attr)
|
||||||
|
other_tenant = getattr(other, tenant_attr, None) if tenant_no is not None else tenant_no
|
||||||
|
if other_parent != parent_id or (
|
||||||
|
tenant_no is not None and (other_tenant or "") != tenant_no
|
||||||
|
):
|
||||||
|
raise ValueError(foreign_message)
|
||||||
|
|
||||||
|
queues: Dict[str, List[Any]] = {}
|
||||||
|
deleted_queues: Dict[str, List[Any]] = {}
|
||||||
|
if natural_key is not None and row_natural_key is not None:
|
||||||
|
for row in sorted(active_rows, key=_sort_key):
|
||||||
|
key = (row_natural_key(row) or "").strip()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
queues.setdefault(key, []).append(row)
|
||||||
|
if reuse_deleted:
|
||||||
|
deleted_rows = _scoped(db.query(model).filter(model.del_flag == 1)).all()
|
||||||
|
for row in sorted(deleted_rows, key=_sort_key):
|
||||||
|
key = (row_natural_key(row) or "").strip()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
deleted_queues.setdefault(key, []).append(row)
|
||||||
|
|
||||||
|
claimed = set()
|
||||||
|
assigned: List[Optional[Any]] = [None] * len(payload)
|
||||||
|
|
||||||
|
def _take(row: Any) -> Any:
|
||||||
|
if row.id in claimed:
|
||||||
|
raise ValueError(duplicate_message)
|
||||||
|
claimed.add(row.id)
|
||||||
|
return row
|
||||||
|
|
||||||
|
for idx, item in enumerate(payload):
|
||||||
|
row_id = _item_id(item)
|
||||||
|
if not row_id:
|
||||||
|
continue
|
||||||
|
if row_id in active_by_id:
|
||||||
|
assigned[idx] = _take(active_by_id[row_id])
|
||||||
|
continue
|
||||||
|
row = db.query(model).filter(model.id == row_id).first()
|
||||||
|
if row is None:
|
||||||
|
row = build_row(row_id)
|
||||||
|
db.add(row)
|
||||||
|
assigned[idx] = _take(row)
|
||||||
|
|
||||||
|
for bucket in queues.values():
|
||||||
|
bucket[:] = [row for row in bucket if row.id not in claimed]
|
||||||
|
|
||||||
|
for idx, item in enumerate(payload):
|
||||||
|
if assigned[idx] is not None:
|
||||||
|
continue
|
||||||
|
row = None
|
||||||
|
if natural_key is not None:
|
||||||
|
key = (natural_key(item) or "").strip()
|
||||||
|
bucket = queues.get(key) if key else None
|
||||||
|
while bucket:
|
||||||
|
candidate = bucket.pop(0)
|
||||||
|
if candidate.id not in claimed:
|
||||||
|
row = candidate
|
||||||
|
break
|
||||||
|
if row is None and reuse_deleted and natural_key is not None:
|
||||||
|
key = (natural_key(item) or "").strip()
|
||||||
|
bucket = deleted_queues.get(key) if key else None
|
||||||
|
while bucket:
|
||||||
|
candidate = bucket.pop(0)
|
||||||
|
if candidate.id not in claimed:
|
||||||
|
row = candidate
|
||||||
|
break
|
||||||
|
if row is None:
|
||||||
|
row = build_row(generate_uuid())
|
||||||
|
db.add(row)
|
||||||
|
assigned[idx] = _take(row)
|
||||||
|
|
||||||
|
result: List[Any] = []
|
||||||
|
for idx, item in enumerate(payload):
|
||||||
|
row = assigned[idx]
|
||||||
|
setattr(row, parent_attr, parent_id)
|
||||||
|
if tenant_no is not None and hasattr(row, tenant_attr):
|
||||||
|
setattr(row, tenant_attr, tenant_no)
|
||||||
|
apply_fields(row, item, idx, now)
|
||||||
|
row.del_flag = 0
|
||||||
|
if hasattr(row, "update_by"):
|
||||||
|
row.update_by = user_id
|
||||||
|
if hasattr(row, "update_time"):
|
||||||
|
row.update_time = now
|
||||||
|
db.add(row)
|
||||||
|
result.append(row)
|
||||||
|
|
||||||
|
if reuse_deleted and row_natural_key is not None:
|
||||||
|
keys_to_clear = set()
|
||||||
|
for row in result:
|
||||||
|
keys_to_clear.add((row_natural_key(row) or "").strip())
|
||||||
|
for row in active_rows:
|
||||||
|
if row.id not in claimed:
|
||||||
|
keys_to_clear.add((row_natural_key(row) or "").strip())
|
||||||
|
removed_tombstone = False
|
||||||
|
for key in keys_to_clear:
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
for tomb in deleted_queues.get(key, []):
|
||||||
|
if tomb.id in claimed:
|
||||||
|
continue
|
||||||
|
db.delete(tomb)
|
||||||
|
removed_tombstone = True
|
||||||
|
if removed_tombstone:
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
for row in active_rows:
|
||||||
|
if row.id in claimed:
|
||||||
|
continue
|
||||||
|
row.del_flag = 1
|
||||||
|
if hasattr(row, "update_by"):
|
||||||
|
row.update_by = user_id
|
||||||
|
if hasattr(row, "update_time"):
|
||||||
|
row.update_time = now
|
||||||
|
db.add(row)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def sync_link_rows(
|
||||||
|
db: Session,
|
||||||
|
model,
|
||||||
|
*,
|
||||||
|
parent_attr: str,
|
||||||
|
parent_id: str,
|
||||||
|
link_attr: str,
|
||||||
|
link_ids: List[str],
|
||||||
|
tenant_no: Optional[str] = None,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
|
duplicate_message: str = "关联重复",
|
||||||
|
foreign_message: str = "关联不属于当前数据",
|
||||||
|
) -> List[Any]:
|
||||||
|
"""按关联 id 更新关系行。重新加回已删除的关联时恢复原行,避免唯一索引冲突。"""
|
||||||
|
now = datetime.now()
|
||||||
|
ordered: List[Dict[str, Any]] = []
|
||||||
|
seen = set()
|
||||||
|
for raw in link_ids or []:
|
||||||
|
link_id = str(raw or "").strip()
|
||||||
|
if not link_id or link_id in seen:
|
||||||
|
continue
|
||||||
|
seen.add(link_id)
|
||||||
|
ordered.append({link_attr: link_id})
|
||||||
|
|
||||||
|
def build_row(row_id: str):
|
||||||
|
data = {
|
||||||
|
"id": row_id,
|
||||||
|
parent_attr: parent_id,
|
||||||
|
link_attr: "",
|
||||||
|
"create_by": user_id,
|
||||||
|
"create_dept_id": "",
|
||||||
|
"create_time": now,
|
||||||
|
}
|
||||||
|
if tenant_no is not None and hasattr(model, "tenant_no"):
|
||||||
|
data["tenant_no"] = tenant_no
|
||||||
|
return model(**data)
|
||||||
|
|
||||||
|
def apply_row(row: Any, item: Dict[str, Any], idx: int, _now: datetime) -> None:
|
||||||
|
setattr(row, link_attr, item[link_attr])
|
||||||
|
if tenant_no is not None and hasattr(row, "tenant_no"):
|
||||||
|
row.tenant_no = tenant_no
|
||||||
|
if hasattr(row, "order_num"):
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
return sync_child_rows(
|
||||||
|
db,
|
||||||
|
model,
|
||||||
|
parent_attr=parent_attr,
|
||||||
|
parent_id=parent_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=ordered,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: item[link_attr],
|
||||||
|
row_natural_key=lambda row: str(getattr(row, link_attr) or "").strip(),
|
||||||
|
duplicate_message=duplicate_message,
|
||||||
|
foreign_message=foreign_message,
|
||||||
|
reuse_deleted=True,
|
||||||
|
)
|
||||||
@@ -405,7 +405,7 @@ def submit_refund(
|
|||||||
) -> BizPilotPayment:
|
) -> BizPilotPayment:
|
||||||
row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no)
|
row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no)
|
||||||
if row.pay_status != "yjf":
|
if row.pay_status != "yjf":
|
||||||
raise ValueError("仅已缴费状态可发起退费")
|
raise ValueError("仅付款完成状态可发起退费")
|
||||||
if refund_amount is None or Decimal(str(refund_amount)) <= 0:
|
if refund_amount is None or Decimal(str(refund_amount)) <= 0:
|
||||||
raise ValueError("请填写有效的退费金额")
|
raise ValueError("请填写有效的退费金额")
|
||||||
row.pay_status = "tfqrz"
|
row.pay_status = "tfqrz"
|
||||||
|
|||||||
+1243
-233
File diff suppressed because it is too large.
Load diff
@@ -24,7 +24,7 @@ def _resolve_issued_user_display_name(
|
|||||||
uid = (user_id or "").strip()
|
uid = (user_id or "").strip()
|
||||||
if not uid:
|
if not uid:
|
||||||
return None
|
return None
|
||||||
u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
|
u = db.query(SysUser).filter(SysUser.id == uid).first()
|
||||||
if not u:
|
if not u:
|
||||||
return None
|
return None
|
||||||
return (u.nick_name or u.user_no or "").strip() or None
|
return (u.nick_name or u.user_no or "").strip() or None
|
||||||
@@ -41,7 +41,7 @@ def enrich_issued_user_names(db: Session, rows: List[Dict[str, Any]]) -> None:
|
|||||||
return
|
return
|
||||||
users = (
|
users = (
|
||||||
db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
|
db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
|
||||||
.filter(SysUser.id.in_(missing_uids), SysUser.del_flag == 0)
|
.filter(SysUser.id.in_(missing_uids))
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
name_map = {(u.id or ""): ((u.nick_name or u.user_no or "").strip()) for u in users}
|
name_map = {(u.id or ""): ((u.nick_name or u.user_no or "").strip()) for u in users}
|
||||||
|
|||||||
@@ -456,6 +456,46 @@ def soft_delete_hospital(db: Session, hospital_id: str, tenant_no: Optional[str]
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def map_staff_region_rows_to_list(rows: List[Tuple[str, str]]) -> List[Dict[str, Any]]:
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
for rid, name in rows:
|
||||||
|
rid = (rid or "").strip()
|
||||||
|
if not rid:
|
||||||
|
continue
|
||||||
|
out.append({"id": rid, "regionName": (name or "").strip()})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def list_regions_by_staff_user(
|
||||||
|
db: Session, user_id: str, tenant_no: Optional[str] = None
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
uid = (user_id or "").strip()
|
||||||
|
if not uid:
|
||||||
|
return []
|
||||||
|
q = (
|
||||||
|
db.query(BizCollectionRegionStaff.region_id, BizCollectionRegion.region_name)
|
||||||
|
.join(
|
||||||
|
BizCollectionRegion,
|
||||||
|
BizCollectionRegion.id == BizCollectionRegionStaff.region_id,
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
BizCollectionRegionStaff.user_id == uid,
|
||||||
|
BizCollectionRegionStaff.del_flag == 0,
|
||||||
|
BizCollectionRegion.del_flag == 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if tenant_no:
|
||||||
|
q = q.filter(
|
||||||
|
BizCollectionRegionStaff.tenant_no == tenant_no,
|
||||||
|
BizCollectionRegion.tenant_no == tenant_no,
|
||||||
|
)
|
||||||
|
q = q.order_by(
|
||||||
|
BizCollectionRegionStaff.order_num.asc(),
|
||||||
|
BizCollectionRegionStaff.create_time.desc(),
|
||||||
|
)
|
||||||
|
return map_staff_region_rows_to_list([(r[0], r[1]) for r in q.all()])
|
||||||
|
|
||||||
|
|
||||||
def list_staff(
|
def list_staff(
|
||||||
db: Session,
|
db: Session,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.models.models import BizConsumablePack, BizConsumablePackItem
|
from app.models.models import BizConsumablePack, BizConsumablePackItem
|
||||||
|
|
||||||
|
|
||||||
@@ -230,37 +231,45 @@ def update_consumable_pack(
|
|||||||
db.add(obj)
|
db.add(obj)
|
||||||
|
|
||||||
if items is not None:
|
if items is not None:
|
||||||
existing = (
|
|
||||||
db.query(BizConsumablePackItem)
|
|
||||||
.filter(and_(BizConsumablePackItem.pack_id == pack_id, BizConsumablePackItem.del_flag == 0))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
for ei in existing:
|
|
||||||
ei.del_flag = 1
|
|
||||||
ei.update_time = now
|
|
||||||
db.add(ei)
|
|
||||||
|
|
||||||
for it in items:
|
def build_item(row_id):
|
||||||
item_obj = BizConsumablePackItem(
|
return BizConsumablePackItem(
|
||||||
id=generate_uuid(),
|
id=row_id,
|
||||||
pack_id=pack_id,
|
pack_id=pack_id,
|
||||||
consumable_code=(it.get("consumable_code") or "").strip(),
|
consumable_code="",
|
||||||
consumable_name=(it.get("consumable_name") or "").strip(),
|
consumable_name="",
|
||||||
spec_model=(it.get("spec_model") or "").strip() or None,
|
|
||||||
standard_usage=(it.get("standard_usage") or "").strip() or None,
|
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
tenant_no=obj.tenant_no,
|
tenant_no=obj.tenant_no,
|
||||||
order_num=0,
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id="",
|
create_dept_id="",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(item_obj)
|
|
||||||
|
def apply_item(row, it, _idx, _now):
|
||||||
|
row.consumable_code = (it.get("consumable_code") or "").strip()
|
||||||
|
row.consumable_name = (it.get("consumable_name") or "").strip()
|
||||||
|
row.spec_model = (it.get("spec_model") or "").strip() or None
|
||||||
|
row.standard_usage = (it.get("standard_usage") or "").strip() or None
|
||||||
|
row.unit = (it.get("unit") or "").strip() or None
|
||||||
|
row.remark = (it.get("remark") or "").strip() or None
|
||||||
|
row.order_num = 0
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizConsumablePackItem,
|
||||||
|
parent_attr="pack_id",
|
||||||
|
parent_id=pack_id,
|
||||||
|
tenant_no=obj.tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_item,
|
||||||
|
apply_fields=apply_item,
|
||||||
|
natural_key=lambda it: (it.get("consumable_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.consumable_code or "").strip(),
|
||||||
|
duplicate_message="耗材包明细重复",
|
||||||
|
foreign_message="耗材包明细不属于当前耗材包",
|
||||||
|
)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(obj)
|
db.refresh(obj)
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ def _row_to_dict(c: BizCustomer) -> Dict[str, Any]:
|
|||||||
"sex": (c.sex or "2").strip() if c.sex else "2",
|
"sex": (c.sex or "2").strip() if c.sex else "2",
|
||||||
"idCardNo": c.id_card_no or "",
|
"idCardNo": c.id_card_no or "",
|
||||||
"age": c.age,
|
"age": c.age,
|
||||||
|
"birthYm": c.birth_ym or "",
|
||||||
|
"ethnic": c.ethnic or "",
|
||||||
|
"email": c.email or "",
|
||||||
|
"address": c.address or "",
|
||||||
"phoneNo": c.phone_no or "",
|
"phoneNo": c.phone_no or "",
|
||||||
"customerTags": c.customer_tags or "",
|
"customerTags": c.customer_tags or "",
|
||||||
"tenantNo": c.tenant_no,
|
"tenantNo": c.tenant_no,
|
||||||
@@ -110,12 +114,18 @@ def create_customer(
|
|||||||
sex: str = "2",
|
sex: str = "2",
|
||||||
id_card_no: Optional[str] = None,
|
id_card_no: Optional[str] = None,
|
||||||
age: Optional[int] = None,
|
age: Optional[int] = None,
|
||||||
|
birth_ym: Optional[str] = None,
|
||||||
|
ethnic: Optional[str] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
address: Optional[str] = None,
|
||||||
phone_no: Optional[str] = None,
|
phone_no: Optional[str] = None,
|
||||||
customer_tags: Optional[str] = None,
|
customer_tags: Optional[str] = None,
|
||||||
tenant_no: str = "zkxw",
|
tenant_no: str = "zkxw",
|
||||||
order_num: Optional[int] = None,
|
order_num: Optional[int] = None,
|
||||||
remark: Optional[str] = None,
|
remark: Optional[str] = None,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
|
commit: bool = True,
|
||||||
|
create_dept_id: str = "",
|
||||||
) -> BizCustomer:
|
) -> BizCustomer:
|
||||||
name = (customer_name or "").strip()
|
name = (customer_name or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
@@ -129,21 +139,28 @@ def create_customer(
|
|||||||
sex=(sex or "2")[:1] if sex else "2",
|
sex=(sex or "2")[:1] if sex else "2",
|
||||||
id_card_no=ic,
|
id_card_no=ic,
|
||||||
age=age,
|
age=age,
|
||||||
|
birth_ym=(birth_ym or "").strip()[:6] or None,
|
||||||
|
ethnic=(ethnic or "").strip() or None,
|
||||||
|
email=(email or "").strip() or None,
|
||||||
|
address=(address or "").strip() or None,
|
||||||
phone_no=(phone_no or "").strip() or None,
|
phone_no=(phone_no or "").strip() or None,
|
||||||
customer_tags=(customer_tags or "").strip() or None,
|
customer_tags=(customer_tags or "").strip() or None,
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
order_num=order_num if order_num is not None else 0,
|
order_num=order_num if order_num is not None else 0,
|
||||||
remark=(remark or "").strip() or None,
|
remark=(remark or "").strip() or None,
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id="",
|
create_dept_id=create_dept_id,
|
||||||
create_time=datetime.now(),
|
create_time=datetime.now(),
|
||||||
update_by=user_id,
|
update_by=user_id,
|
||||||
update_time=datetime.now(),
|
update_time=datetime.now(),
|
||||||
del_flag=0,
|
del_flag=0,
|
||||||
)
|
)
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
db.commit()
|
if commit:
|
||||||
db.refresh(obj)
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
else:
|
||||||
|
db.flush()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +172,10 @@ def update_customer(
|
|||||||
sex: Optional[str] = None,
|
sex: Optional[str] = None,
|
||||||
id_card_no: Optional[str] = None,
|
id_card_no: Optional[str] = None,
|
||||||
age: Optional[int] = None,
|
age: Optional[int] = None,
|
||||||
|
birth_ym: Optional[str] = None,
|
||||||
|
ethnic: Optional[str] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
address: Optional[str] = None,
|
||||||
phone_no: Optional[str] = None,
|
phone_no: Optional[str] = None,
|
||||||
customer_tags: Optional[str] = None,
|
customer_tags: Optional[str] = None,
|
||||||
order_num: Optional[int] = None,
|
order_num: Optional[int] = None,
|
||||||
@@ -179,6 +200,14 @@ def update_customer(
|
|||||||
obj.id_card_no = ic
|
obj.id_card_no = ic
|
||||||
if age is not None:
|
if age is not None:
|
||||||
obj.age = age
|
obj.age = age
|
||||||
|
if birth_ym is not None:
|
||||||
|
obj.birth_ym = birth_ym.strip()[:6] or None
|
||||||
|
if ethnic is not None:
|
||||||
|
obj.ethnic = ethnic.strip() or None
|
||||||
|
if email is not None:
|
||||||
|
obj.email = email.strip() or None
|
||||||
|
if address is not None:
|
||||||
|
obj.address = address.strip() or None
|
||||||
if phone_no is not None:
|
if phone_no is not None:
|
||||||
obj.phone_no = phone_no.strip() or None
|
obj.phone_no = phone_no.strip() or None
|
||||||
if customer_tags is not None:
|
if customer_tags is not None:
|
||||||
@@ -195,14 +224,23 @@ def update_customer(
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def soft_delete_customer(db: Session, customer_id: str, tenant_no: Optional[str] = None) -> None:
|
def soft_delete_customer(
|
||||||
|
db: Session,
|
||||||
|
customer_id: str,
|
||||||
|
tenant_no: Optional[str] = None,
|
||||||
|
*,
|
||||||
|
commit: bool = True,
|
||||||
|
) -> None:
|
||||||
obj = get_customer(db, customer_id, tenant_no=tenant_no)
|
obj = get_customer(db, customer_id, tenant_no=tenant_no)
|
||||||
if not obj:
|
if not obj:
|
||||||
raise ValueError("客户不存在")
|
raise ValueError("客户不存在")
|
||||||
obj.del_flag = 1
|
obj.del_flag = 1
|
||||||
obj.update_time = datetime.now()
|
obj.update_time = datetime.now()
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
db.commit()
|
if commit:
|
||||||
|
db.commit()
|
||||||
|
else:
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
|
||||||
def total_pages(total: int, page_size: int) -> int:
|
def total_pages(total: int, page_size: int) -> int:
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""客户服务字典 CRUD。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
from app.models.models import BizCustomerServiceDict
|
||||||
|
from app.services.sample_customer_service_register import normalize_dict_value
|
||||||
|
|
||||||
|
|
||||||
|
def list_dict_values(db: Session, tenant_no: str, dict_type: str) -> List[str]:
|
||||||
|
dtype = (dict_type or "").strip()
|
||||||
|
if not dtype:
|
||||||
|
return []
|
||||||
|
rows = (
|
||||||
|
db.query(BizCustomerServiceDict)
|
||||||
|
.filter(
|
||||||
|
BizCustomerServiceDict.tenant_no == tenant_no,
|
||||||
|
BizCustomerServiceDict.dict_type == dtype,
|
||||||
|
BizCustomerServiceDict.del_flag == 0,
|
||||||
|
)
|
||||||
|
.order_by(
|
||||||
|
BizCustomerServiceDict.order_num.asc(),
|
||||||
|
BizCustomerServiceDict.dict_value.asc(),
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [row.dict_value for row in rows if row.dict_value]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dict_value(
|
||||||
|
db: Session,
|
||||||
|
tenant_no: str,
|
||||||
|
dict_type: str,
|
||||||
|
value,
|
||||||
|
*,
|
||||||
|
user_id: Optional[str],
|
||||||
|
is_system: bool = False,
|
||||||
|
) -> Optional[str]:
|
||||||
|
normalized = normalize_dict_value(value)
|
||||||
|
if normalized is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dtype = (dict_type or "").strip()
|
||||||
|
if not dtype:
|
||||||
|
return None
|
||||||
|
|
||||||
|
existing = (
|
||||||
|
db.query(BizCustomerServiceDict)
|
||||||
|
.filter(
|
||||||
|
BizCustomerServiceDict.tenant_no == tenant_no,
|
||||||
|
BizCustomerServiceDict.dict_type == dtype,
|
||||||
|
BizCustomerServiceDict.dict_value == normalized,
|
||||||
|
BizCustomerServiceDict.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
return existing.dict_value
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
row = BizCustomerServiceDict(
|
||||||
|
id=generate_uuid(),
|
||||||
|
dict_type=dtype,
|
||||||
|
dict_value=normalized,
|
||||||
|
is_system="1" if is_system else "0",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
db.flush()
|
||||||
|
return normalized
|
||||||
@@ -9,6 +9,7 @@ from sqlalchemy import func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.material_stock_batch import _dec, _fmt_date, _fmt_dt, _parse_date, _require_str
|
from app.crud.material_stock_batch import _dec, _fmt_date, _fmt_dt, _parse_date, _require_str
|
||||||
from app.models.models import SysUser
|
from app.models.models import SysUser
|
||||||
from app.models.material import (
|
from app.models.material import (
|
||||||
@@ -442,6 +443,44 @@ def _resolve_unused_item_fields(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _unused_line_key(usage_id: Optional[str], batch_id: Optional[str]) -> str:
|
||||||
|
usage = (usage_id or "").strip()
|
||||||
|
if usage:
|
||||||
|
return f"usage:{usage}"
|
||||||
|
batch = (batch_id or "").strip()
|
||||||
|
if batch:
|
||||||
|
return f"batch:{batch}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_unused_item(db: Session, it: Dict[str, Any], tenant_no: str) -> Dict[str, Any]:
|
||||||
|
it = _resolve_unused_item_fields(db, it, tenant_no)
|
||||||
|
name = _require_str(it.get("material_name") or it.get("materialName"), "物料名称")
|
||||||
|
qty = it.get("unused_qty") if it.get("unused_qty") is not None else it.get("unusedQty")
|
||||||
|
if qty is None or Decimal(str(qty)) <= 0:
|
||||||
|
raise ValueError("未用完数量必须大于0")
|
||||||
|
unit_price = it.get("unit_price") if it.get("unit_price") is not None else it.get("unitPrice")
|
||||||
|
return {
|
||||||
|
"id": str(it.get("id") or "").strip(),
|
||||||
|
"usage_id": (it.get("usage_id") or it.get("usageId") or "").strip() or None,
|
||||||
|
"batch_id": (it.get("batch_id") or it.get("batchId") or "").strip() or None,
|
||||||
|
"material_code": (it.get("material_code") or it.get("materialCode") or "").strip() or None,
|
||||||
|
"material_name": name,
|
||||||
|
"spec_model": (it.get("spec_model") or it.get("specModel") or "").strip() or None,
|
||||||
|
"unit": (it.get("unit") or "").strip() or None,
|
||||||
|
"stock_in_no": (it.get("stock_in_no") or it.get("stockInNo") or "").strip() or None,
|
||||||
|
"stock_in_date": _parse_optional_date(it.get("stock_in_date") or it.get("stockInDate")),
|
||||||
|
"expire_date": _parse_optional_date(it.get("expire_date") or it.get("expireDate")),
|
||||||
|
"catalog_no": (it.get("catalog_no") or it.get("catalogNo") or "").strip() or None,
|
||||||
|
"batch_no": (it.get("batch_no") or it.get("batchNo") or "").strip() or None,
|
||||||
|
"manufacturer": (it.get("manufacturer") or "").strip() or None,
|
||||||
|
"unit_price": Decimal(str(unit_price)) if unit_price is not None else None,
|
||||||
|
"holder": (it.get("holder") or "").strip() or None,
|
||||||
|
"unused_qty": Decimal(str(qty)),
|
||||||
|
"remark": (it.get("remark") or "").strip() or None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def save_unused_lines(
|
def save_unused_lines(
|
||||||
db: Session,
|
db: Session,
|
||||||
check_id: str,
|
check_id: str,
|
||||||
@@ -458,72 +497,61 @@ def save_unused_lines(
|
|||||||
raise ValueError("仅进行中的盘点单可登记未用完物料")
|
raise ValueError("仅进行中的盘点单可登记未用完物料")
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
old_rows = (
|
prepared = [_prepare_unused_item(db, it, tenant_no) for it in (items or [])]
|
||||||
db.query(BizMaterialStockCheckUnusedLine)
|
|
||||||
.filter(
|
|
||||||
BizMaterialStockCheckUnusedLine.check_id == check_id,
|
|
||||||
BizMaterialStockCheckUnusedLine.del_flag == 0,
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for r in old_rows:
|
|
||||||
r.del_flag = 1
|
|
||||||
r.update_by = user_id
|
|
||||||
r.update_time = now
|
|
||||||
db.add(r)
|
|
||||||
|
|
||||||
count = 0
|
def build_row(row_id: str):
|
||||||
for it in items or []:
|
return BizMaterialStockCheckUnusedLine(
|
||||||
it = _resolve_unused_item_fields(db, it, tenant_no)
|
id=row_id,
|
||||||
name = _require_str(it.get("material_name") or it.get("materialName"), "物料名称")
|
|
||||||
qty = it.get("unused_qty") if it.get("unused_qty") is not None else it.get("unusedQty")
|
|
||||||
if qty is None or Decimal(str(qty)) <= 0:
|
|
||||||
raise ValueError("未用完数量必须大于0")
|
|
||||||
qty_dec = Decimal(str(qty))
|
|
||||||
stock_in_date = _parse_optional_date(it.get("stock_in_date") or it.get("stockInDate"))
|
|
||||||
expire_date = _parse_optional_date(it.get("expire_date") or it.get("expireDate"))
|
|
||||||
usage_id = (it.get("usage_id") or it.get("usageId") or "").strip() or None
|
|
||||||
batch_id = (it.get("batch_id") or it.get("batchId") or "").strip() or None
|
|
||||||
|
|
||||||
row = BizMaterialStockCheckUnusedLine(
|
|
||||||
id=generate_uuid(),
|
|
||||||
check_id=check_id,
|
check_id=check_id,
|
||||||
usage_id=usage_id,
|
material_name="",
|
||||||
batch_id=batch_id,
|
unused_qty=0,
|
||||||
material_code=(it.get("material_code") or it.get("materialCode") or "").strip() or None,
|
|
||||||
material_name=name,
|
|
||||||
spec_model=(it.get("spec_model") or it.get("specModel") or "").strip() or None,
|
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
|
||||||
stock_in_no=(it.get("stock_in_no") or it.get("stockInNo") or "").strip() or None,
|
|
||||||
stock_in_date=stock_in_date,
|
|
||||||
expire_date=expire_date,
|
|
||||||
catalog_no=(it.get("catalog_no") or it.get("catalogNo") or "").strip() or None,
|
|
||||||
batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
|
|
||||||
manufacturer=(it.get("manufacturer") or "").strip() or None,
|
|
||||||
unit_price=(
|
|
||||||
Decimal(str(it.get("unit_price") or it.get("unitPrice")))
|
|
||||||
if (it.get("unit_price") is not None or it.get("unitPrice") is not None)
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
holder=(it.get("holder") or "").strip() or None,
|
|
||||||
unused_qty=qty_dec,
|
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id=dept_id or "",
|
create_dept_id=dept_id or "",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(row)
|
|
||||||
count += 1
|
def apply_row(row, it, idx, _now):
|
||||||
|
row.usage_id = it["usage_id"]
|
||||||
|
row.batch_id = it["batch_id"]
|
||||||
|
row.material_code = it["material_code"]
|
||||||
|
row.material_name = it["material_name"]
|
||||||
|
row.spec_model = it["spec_model"]
|
||||||
|
row.unit = it["unit"]
|
||||||
|
row.stock_in_no = it["stock_in_no"]
|
||||||
|
row.stock_in_date = it["stock_in_date"]
|
||||||
|
row.expire_date = it["expire_date"]
|
||||||
|
row.catalog_no = it["catalog_no"]
|
||||||
|
row.batch_no = it["batch_no"]
|
||||||
|
row.manufacturer = it["manufacturer"]
|
||||||
|
row.unit_price = it["unit_price"]
|
||||||
|
row.holder = it["holder"]
|
||||||
|
row.unused_qty = it["unused_qty"]
|
||||||
|
row.remark = it["remark"]
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
saved = sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizMaterialStockCheckUnusedLine,
|
||||||
|
parent_attr="check_id",
|
||||||
|
parent_id=check_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=prepared,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: _unused_line_key(it.get("usage_id"), it.get("batch_id")),
|
||||||
|
row_natural_key=lambda row: _unused_line_key(row.usage_id, row.batch_id),
|
||||||
|
duplicate_message="未用完明细重复",
|
||||||
|
foreign_message="未用完明细不属于当前盘点单",
|
||||||
|
)
|
||||||
|
|
||||||
check.update_by = user_id
|
check.update_by = user_id
|
||||||
check.update_time = now
|
check.update_time = now
|
||||||
db.add(check)
|
db.add(check)
|
||||||
db.commit()
|
db.commit()
|
||||||
return count
|
return len(saved)
|
||||||
|
|
||||||
|
|
||||||
def complete_check(
|
def complete_check(
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def _enrich(db: Session, d: OaInstanceDraft, *, include_payload: bool = False) -
|
|||||||
)
|
)
|
||||||
if g:
|
if g:
|
||||||
group_name = g.group_name or ""
|
group_name = g.group_name or ""
|
||||||
return row_to_dict(
|
out = row_to_dict(
|
||||||
d,
|
d,
|
||||||
app_name=(app.app_name if app else "") or "",
|
app_name=(app.app_name if app else "") or "",
|
||||||
version_no=ver.version_no if ver else None,
|
version_no=ver.version_no if ver else None,
|
||||||
@@ -117,6 +117,12 @@ def _enrich(db: Session, d: OaInstanceDraft, *, include_payload: bool = False) -
|
|||||||
group_name=group_name,
|
group_name=group_name,
|
||||||
include_payload=include_payload,
|
include_payload=include_payload,
|
||||||
)
|
)
|
||||||
|
if include_payload and "formData" in out:
|
||||||
|
from app.services.oa_datetime_normalize import normalize_datetime_form_values
|
||||||
|
|
||||||
|
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
|
||||||
|
out["formData"] = normalize_datetime_form_values(schema, out.get("formData") or {})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def count_drafts(db: Session, *, tenant_no: str, owner_user_id: str) -> int:
|
def count_drafts(db: Session, *, tenant_no: str, owner_user_id: str) -> int:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlalchemy import func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
from app.models.models import OaReceiveAccount
|
from app.models.models import OaReceiveAccount, SysUser
|
||||||
|
|
||||||
|
|
||||||
def row_to_dict(r: OaReceiveAccount) -> Dict[str, Any]:
|
def row_to_dict(r: OaReceiveAccount) -> Dict[str, Any]:
|
||||||
@@ -26,9 +26,26 @@ def row_to_dict(r: OaReceiveAccount) -> Dict[str, Any]:
|
|||||||
"remark": r.remark or "",
|
"remark": r.remark or "",
|
||||||
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
|
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
|
||||||
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
|
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
|
||||||
|
"ownerUserId": (r.create_by or "").strip(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _owner_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
|
||||||
|
ids = [item for item in {uid.strip() for uid in user_ids if uid and uid.strip()}]
|
||||||
|
if not ids:
|
||||||
|
return {}
|
||||||
|
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
|
||||||
|
return {row.id: (row.nick_name or "").strip() for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_owner_names(db: Session, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
names = _owner_name_map(db, [item.get("ownerUserId") or "" for item in items])
|
||||||
|
for item in items:
|
||||||
|
uid = item.get("ownerUserId") or ""
|
||||||
|
item["ownerName"] = names.get(uid) or "—"
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
def _filtered_query(
|
def _filtered_query(
|
||||||
db: Session,
|
db: Session,
|
||||||
*,
|
*,
|
||||||
@@ -74,17 +91,24 @@ def list_accounts(
|
|||||||
.limit(page_size)
|
.limit(page_size)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return [row_to_dict(x) for x in rows], int(total)
|
return _attach_owner_names(db, [row_to_dict(x) for x in rows]), int(total)
|
||||||
|
|
||||||
|
|
||||||
def list_options(db: Session, *, tenant_no: str) -> List[Dict[str, Any]]:
|
def list_options(db: Session, *, tenant_no: str, user_id: str) -> List[Dict[str, Any]]:
|
||||||
|
owner_id = (user_id or "").strip()
|
||||||
|
if not owner_id:
|
||||||
|
return []
|
||||||
rows = (
|
rows = (
|
||||||
db.query(OaReceiveAccount)
|
db.query(OaReceiveAccount)
|
||||||
.filter(OaReceiveAccount.tenant_no == tenant_no, OaReceiveAccount.del_flag == 0)
|
.filter(
|
||||||
|
OaReceiveAccount.tenant_no == tenant_no,
|
||||||
|
OaReceiveAccount.del_flag == 0,
|
||||||
|
func.trim(OaReceiveAccount.create_by) == owner_id,
|
||||||
|
)
|
||||||
.order_by(OaReceiveAccount.create_time.desc())
|
.order_by(OaReceiveAccount.create_time.desc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return [row_to_dict(x) for x in rows]
|
return _attach_owner_names(db, [row_to_dict(x) for x in rows])
|
||||||
|
|
||||||
|
|
||||||
def get_account(db: Session, account_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
|
def get_account(db: Session, account_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
|
||||||
@@ -97,7 +121,7 @@ def get_account(db: Session, account_id: str, tenant_no: str) -> Optional[Dict[s
|
|||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
return row_to_dict(row) if row else None
|
return _attach_owner_names(db, [row_to_dict(row)])[0] if row else None
|
||||||
|
|
||||||
|
|
||||||
def create_account(
|
def create_account(
|
||||||
@@ -131,7 +155,7 @@ def create_account(
|
|||||||
db.add(obj)
|
db.add(obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(obj)
|
db.refresh(obj)
|
||||||
return row_to_dict(obj)
|
return _attach_owner_names(db, [row_to_dict(obj)])[0]
|
||||||
|
|
||||||
|
|
||||||
def update_account(
|
def update_account(
|
||||||
@@ -168,7 +192,7 @@ def update_account(
|
|||||||
db.add(obj)
|
db.add(obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(obj)
|
db.refresh(obj)
|
||||||
return row_to_dict(obj)
|
return _attach_owner_names(db, [row_to_dict(obj)])[0]
|
||||||
|
|
||||||
|
|
||||||
def soft_delete_account(
|
def soft_delete_account(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from fastapi import HTTPException
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud import cell_product as cell_product_svc
|
from app.crud import cell_product as cell_product_svc
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizCellProduct,
|
BizCellProduct,
|
||||||
@@ -396,13 +397,6 @@ def save_outbound_material(
|
|||||||
db, schedule_id, tenant_no, incoming_qc_ids, user_id, now
|
db, schedule_id, tenant_no, incoming_qc_ids, user_id, now
|
||||||
)
|
)
|
||||||
|
|
||||||
db.query(BizOutboundReleasePaper).filter(
|
|
||||||
BizOutboundReleasePaper.infusion_schedule_id == schedule_id,
|
|
||||||
BizOutboundReleasePaper.tenant_no == tenant_no,
|
|
||||||
BizOutboundReleasePaper.del_flag == 0,
|
|
||||||
).update({"del_flag": 1, "update_by": user_id, "update_time": now}, synchronize_session=False)
|
|
||||||
db.flush()
|
|
||||||
|
|
||||||
for idx, item in enumerate(cells or []):
|
for idx, item in enumerate(cells or []):
|
||||||
cp_id = (item.get("cellProductId") or item.get("cell_product_id") or "").strip()
|
cp_id = (item.get("cellProductId") or item.get("cell_product_id") or "").strip()
|
||||||
if not cp_id:
|
if not cp_id:
|
||||||
@@ -532,7 +526,8 @@ def save_outbound_material(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for idx, item in enumerate(paper_materials or []):
|
paper_payload = []
|
||||||
|
for item in paper_materials or []:
|
||||||
paper_name = (item.get("paperName") or item.get("paper_name") or "").strip()
|
paper_name = (item.get("paperName") or item.get("paper_name") or "").strip()
|
||||||
attachment_url = (item.get("attachmentUrl") or item.get("attachment_url") or "").strip()
|
attachment_url = (item.get("attachmentUrl") or item.get("attachment_url") or "").strip()
|
||||||
copies_raw = item.get("copies")
|
copies_raw = item.get("copies")
|
||||||
@@ -546,26 +541,53 @@ def save_outbound_material(
|
|||||||
raise HTTPException(status_code=400, detail="纸质材料份数格式无效")
|
raise HTTPException(status_code=400, detail="纸质材料份数格式无效")
|
||||||
if copies <= 0:
|
if copies <= 0:
|
||||||
raise HTTPException(status_code=400, detail="纸质材料份数必须大于0")
|
raise HTTPException(status_code=400, detail="纸质材料份数必须大于0")
|
||||||
|
paper_payload.append(
|
||||||
db.add(
|
{
|
||||||
BizOutboundReleasePaper(
|
"paper_name": paper_name,
|
||||||
id=generate_uuid(),
|
"copies": copies,
|
||||||
material_id=header.id,
|
"attachment_url": attachment_url,
|
||||||
infusion_schedule_id=schedule_id,
|
"remark": (item.get("remark") or "").strip() or None,
|
||||||
paper_name=paper_name,
|
}
|
||||||
copies=copies,
|
|
||||||
attachment_url=attachment_url,
|
|
||||||
tenant_no=tenant_no,
|
|
||||||
order_num=idx + 1,
|
|
||||||
remark=(item.get("remark") or "").strip() or None,
|
|
||||||
create_by=user_id,
|
|
||||||
create_dept_id=create_dept_id or "",
|
|
||||||
create_time=now,
|
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build_paper(row_id):
|
||||||
|
return BizOutboundReleasePaper(
|
||||||
|
id=row_id,
|
||||||
|
material_id=header.id,
|
||||||
|
infusion_schedule_id=schedule_id,
|
||||||
|
paper_name="",
|
||||||
|
copies=1,
|
||||||
|
attachment_url="",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id=create_dept_id or "",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_paper(row, item, idx, _now):
|
||||||
|
row.material_id = header.id
|
||||||
|
row.paper_name = item["paper_name"]
|
||||||
|
row.copies = item["copies"]
|
||||||
|
row.attachment_url = item["attachment_url"]
|
||||||
|
row.order_num = idx + 1
|
||||||
|
row.remark = item["remark"]
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizOutboundReleasePaper,
|
||||||
|
parent_attr="infusion_schedule_id",
|
||||||
|
parent_id=schedule_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=paper_payload,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_paper,
|
||||||
|
apply_fields=apply_paper,
|
||||||
|
natural_key=lambda item: item["attachment_url"],
|
||||||
|
row_natural_key=lambda row: row.attachment_url or "",
|
||||||
|
duplicate_message="纸质材料重复",
|
||||||
|
foreign_message="纸质材料不属于当前输注计划",
|
||||||
|
)
|
||||||
|
|
||||||
db.flush()
|
db.flush()
|
||||||
return get_outbound_material(db, schedule_id, tenant_no)
|
return get_outbound_material(db, schedule_id, tenant_no)
|
||||||
@@ -524,6 +524,8 @@ def list_reviews_paginated(
|
|||||||
"projectName": product.project_name or "",
|
"projectName": product.project_name or "",
|
||||||
"cellTypeName": product.cell_type_name or "",
|
"cellTypeName": product.cell_type_name or "",
|
||||||
"specText": product.spec_text or "",
|
"specText": product.spec_text or "",
|
||||||
|
"packageId": getattr(product, "package_id", None) or "",
|
||||||
|
"packageText": getattr(product, "package_text", None) or "",
|
||||||
"agreementPrice": _dec(product.agreement_price),
|
"agreementPrice": _dec(product.agreement_price),
|
||||||
"actualPrice": _dec(product.actual_price),
|
"actualPrice": _dec(product.actual_price),
|
||||||
"quantity": product.quantity,
|
"quantity": product.quantity,
|
||||||
@@ -604,6 +606,8 @@ def list_reviews_paginated(
|
|||||||
"patientName": (patient.patient_name or "") if patient else "",
|
"patientName": (patient.patient_name or "") if patient else "",
|
||||||
"cellType": s.cell_type or (plan.cell_type or "") if plan else (s.cell_type or ""),
|
"cellType": s.cell_type or (plan.cell_type or "") if plan else (s.cell_type or ""),
|
||||||
"specText": s.spec_text or (plan.spec_text or "") if plan else (s.spec_text or ""),
|
"specText": s.spec_text or (plan.spec_text or "") if plan else (s.spec_text or ""),
|
||||||
|
"packageId": getattr(s, "package_id", None) or "",
|
||||||
|
"packageText": getattr(s, "package_text", None) or "",
|
||||||
"infusionTime": _fmt_dt(s.infusion_time),
|
"infusionTime": _fmt_dt(s.infusion_time),
|
||||||
"scheduleStatus": s.schedule_status or "",
|
"scheduleStatus": s.schedule_status or "",
|
||||||
"scheduleStatusLabel": _SCHEDULE_STATUS_LABELS.get(
|
"scheduleStatusLabel": _SCHEDULE_STATUS_LABELS.get(
|
||||||
|
|||||||
@@ -272,6 +272,8 @@ def _row_to_dict_schedule_product(row) -> Dict[str, Any]:
|
|||||||
"projectName": row.project_name or "",
|
"projectName": row.project_name or "",
|
||||||
"cellTypeName": row.cell_type_name or "",
|
"cellTypeName": row.cell_type_name or "",
|
||||||
"specText": row.spec_text or "",
|
"specText": row.spec_text or "",
|
||||||
|
"packageId": getattr(row, "package_id", None) or "",
|
||||||
|
"packageText": getattr(row, "package_text", None) or "",
|
||||||
"agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
|
"agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
|
||||||
"actualPrice": float(row.actual_price) if row.actual_price is not None else None,
|
"actualPrice": float(row.actual_price) if row.actual_price is not None else None,
|
||||||
"quantity": row.quantity if row.quantity is not None else 1,
|
"quantity": row.quantity if row.quantity is not None else 1,
|
||||||
@@ -289,6 +291,8 @@ def _row_to_dict_infusion_schedule(row, cell_products: Optional[List[Dict[str, A
|
|||||||
"infusionPlanId": row.infusion_plan_id or "",
|
"infusionPlanId": row.infusion_plan_id or "",
|
||||||
"cellType": row.cell_type or "",
|
"cellType": row.cell_type or "",
|
||||||
"specText": row.spec_text or "",
|
"specText": row.spec_text or "",
|
||||||
|
"packageId": getattr(row, "package_id", None) or "",
|
||||||
|
"packageText": getattr(row, "package_text", None) or "",
|
||||||
"infusionTime": infusion_time,
|
"infusionTime": infusion_time,
|
||||||
"infusionLocation": row.infusion_location or "",
|
"infusionLocation": row.infusion_location or "",
|
||||||
"infusionMethod": row.infusion_method or "",
|
"infusionMethod": row.infusion_method or "",
|
||||||
@@ -1218,6 +1222,8 @@ def _create_schedule_products(
|
|||||||
project_name=item.get("projectName") or "",
|
project_name=item.get("projectName") or "",
|
||||||
cell_type_name=item.get("cellTypeName") or "",
|
cell_type_name=item.get("cellTypeName") or "",
|
||||||
spec_text=item.get("specText") or "",
|
spec_text=item.get("specText") or "",
|
||||||
|
package_id=item.get("packageId") or "",
|
||||||
|
package_text=item.get("packageText") or "",
|
||||||
agreement_price=item.get("agreementPrice"),
|
agreement_price=item.get("agreementPrice"),
|
||||||
actual_price=item.get("actualPrice"),
|
actual_price=item.get("actualPrice"),
|
||||||
quantity=item.get("quantity") if item.get("quantity") is not None else 1,
|
quantity=item.get("quantity") if item.get("quantity") is not None else 1,
|
||||||
@@ -1273,6 +1279,8 @@ def create_schedule(
|
|||||||
infusion_plan_id=plan_id,
|
infusion_plan_id=plan_id,
|
||||||
cell_type=data.get("cellType") or first_product.get("cellTypeName") or "",
|
cell_type=data.get("cellType") or first_product.get("cellTypeName") or "",
|
||||||
spec_text=data.get("specText") or first_product.get("specText") or "",
|
spec_text=data.get("specText") or first_product.get("specText") or "",
|
||||||
|
package_id=data.get("packageId") or first_product.get("packageId") or "",
|
||||||
|
package_text=data.get("packageText") or first_product.get("packageText") or "",
|
||||||
infusion_time=_parse_infusion_datetime(data.get("infusionTime")),
|
infusion_time=_parse_infusion_datetime(data.get("infusionTime")),
|
||||||
infusion_location=data.get("infusionLocation") or "",
|
infusion_location=data.get("infusionLocation") or "",
|
||||||
infusion_method=data.get("infusionMethod") or "",
|
infusion_method=data.get("infusionMethod") or "",
|
||||||
@@ -1364,6 +1372,10 @@ def _update_schedule_editable(
|
|||||||
if cell_products:
|
if cell_products:
|
||||||
row.cell_type = data.get("cellType") or first_product.get("cellTypeName") or row.cell_type
|
row.cell_type = data.get("cellType") or first_product.get("cellTypeName") or row.cell_type
|
||||||
row.spec_text = data.get("specText") or first_product.get("specText") or row.spec_text
|
row.spec_text = data.get("specText") or first_product.get("specText") or row.spec_text
|
||||||
|
row.package_id = data.get("packageId") or first_product.get("packageId") or row.package_id
|
||||||
|
row.package_text = (
|
||||||
|
data.get("packageText") or first_product.get("packageText") or row.package_text
|
||||||
|
)
|
||||||
if data.get("remark") is not None:
|
if data.get("remark") is not None:
|
||||||
row.remark = data.get("remark") or ""
|
row.remark = data.get("remark") or ""
|
||||||
row.update_by = user_id
|
row.update_by = user_id
|
||||||
@@ -1407,6 +1419,8 @@ def _upsert_schedule_products(
|
|||||||
prow.project_name = item.get("projectName") or ""
|
prow.project_name = item.get("projectName") or ""
|
||||||
prow.cell_type_name = item.get("cellTypeName") or ""
|
prow.cell_type_name = item.get("cellTypeName") or ""
|
||||||
prow.spec_text = item.get("specText") or ""
|
prow.spec_text = item.get("specText") or ""
|
||||||
|
prow.package_id = item.get("packageId") or ""
|
||||||
|
prow.package_text = item.get("packageText") or ""
|
||||||
if item.get("agreementPrice") is not None:
|
if item.get("agreementPrice") is not None:
|
||||||
prow.agreement_price = item.get("agreementPrice")
|
prow.agreement_price = item.get("agreementPrice")
|
||||||
if item.get("actualPrice") is not None:
|
if item.get("actualPrice") is not None:
|
||||||
@@ -1429,6 +1443,8 @@ def _upsert_schedule_products(
|
|||||||
project_name=item.get("projectName") or "",
|
project_name=item.get("projectName") or "",
|
||||||
cell_type_name=item.get("cellTypeName") or "",
|
cell_type_name=item.get("cellTypeName") or "",
|
||||||
spec_text=item.get("specText") or "",
|
spec_text=item.get("specText") or "",
|
||||||
|
package_id=item.get("packageId") or "",
|
||||||
|
package_text=item.get("packageText") or "",
|
||||||
agreement_price=item.get("agreementPrice"),
|
agreement_price=item.get("agreementPrice"),
|
||||||
actual_price=item.get("actualPrice"),
|
actual_price=item.get("actualPrice"),
|
||||||
quantity=item.get("quantity") if item.get("quantity") is not None else 1,
|
quantity=item.get("quantity") if item.get("quantity") is not None else 1,
|
||||||
@@ -1645,6 +1661,8 @@ def create_basic_experiment_sale_schedules(
|
|||||||
schedule_data = {
|
schedule_data = {
|
||||||
"cellType": line.get("cellTypeName") or "",
|
"cellType": line.get("cellTypeName") or "",
|
||||||
"specText": line.get("specText") or "",
|
"specText": line.get("specText") or "",
|
||||||
|
"packageId": line.get("packageId") or "",
|
||||||
|
"packageText": line.get("packageText") or "",
|
||||||
"infusionTime": line.get("pickupTime"),
|
"infusionTime": line.get("pickupTime"),
|
||||||
"amount": line.get("amount"),
|
"amount": line.get("amount"),
|
||||||
"sortNo": idx + 1,
|
"sortNo": idx + 1,
|
||||||
@@ -1655,6 +1673,8 @@ def create_basic_experiment_sale_schedules(
|
|||||||
"projectName": line.get("projectName"),
|
"projectName": line.get("projectName"),
|
||||||
"cellTypeName": line.get("cellTypeName"),
|
"cellTypeName": line.get("cellTypeName"),
|
||||||
"specText": line.get("specText"),
|
"specText": line.get("specText"),
|
||||||
|
"packageId": line.get("packageId"),
|
||||||
|
"packageText": line.get("packageText"),
|
||||||
"agreementPrice": line.get("agreementPrice"),
|
"agreementPrice": line.get("agreementPrice"),
|
||||||
"actualPrice": line.get("actualPrice"),
|
"actualPrice": line.get("actualPrice"),
|
||||||
"quantity": line.get("quantity") or 1,
|
"quantity": line.get("quantity") or 1,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ from typing import Any, Dict, List, Optional
|
|||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
|
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizCheckupInstitution,
|
BizCheckupInstitution,
|
||||||
BizPilotApplication,
|
BizPilotApplication,
|
||||||
@@ -429,6 +432,52 @@ def list_reserve_items(
|
|||||||
return [_row_to_dict_reserve_item(r) for r in rows]
|
return [_row_to_dict_reserve_item(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _checkup_project_name(item: Dict[str, Any]) -> str:
|
||||||
|
return (item.get("projectName") or item.get("project_name") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _checkup_project_fee(item: Dict[str, Any]):
|
||||||
|
if item.get("projectFee") is not None:
|
||||||
|
return item.get("projectFee")
|
||||||
|
return item.get("project_fee")
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_checkup_items(db, model, checkup_id, items_json, tenant_no, user_id, now):
|
||||||
|
def build_row(row_id):
|
||||||
|
return model(
|
||||||
|
id=row_id,
|
||||||
|
pilot_checkup_id=checkup_id,
|
||||||
|
project_name="",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_row(row, item, idx, _now):
|
||||||
|
row.project_name = _checkup_project_name(item)
|
||||||
|
row.project_fee = _checkup_project_fee(item)
|
||||||
|
row.order_num = idx + 1
|
||||||
|
row.remark = item.get("remark") or ""
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
model,
|
||||||
|
parent_attr="pilot_checkup_id",
|
||||||
|
parent_id=checkup_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=list(items_json or []),
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=_checkup_project_name,
|
||||||
|
row_natural_key=lambda row: (row.project_name or "").strip(),
|
||||||
|
duplicate_message="体检项目重复",
|
||||||
|
foreign_message="体检项目不属于当前体检单",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_reserve_items(
|
def save_reserve_items(
|
||||||
db: Session,
|
db: Session,
|
||||||
checkup_id: str,
|
checkup_id: str,
|
||||||
@@ -437,35 +486,15 @@ def save_reserve_items(
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
_sync_checkup_items(
|
||||||
# Soft-delete existing items
|
db,
|
||||||
db.query(BizPilotCheckupReserveItem).filter(
|
BizPilotCheckupReserveItem,
|
||||||
BizPilotCheckupReserveItem.pilot_checkup_id == checkup_id,
|
checkup_id,
|
||||||
BizPilotCheckupReserveItem.tenant_no == tenant_no,
|
items_json,
|
||||||
BizPilotCheckupReserveItem.del_flag == 0,
|
tenant_no,
|
||||||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
user_id,
|
||||||
|
now,
|
||||||
# Insert new items
|
)
|
||||||
for idx, item in enumerate(items_json):
|
|
||||||
row = BizPilotCheckupReserveItem(
|
|
||||||
id=uuid.uuid4().hex,
|
|
||||||
pilot_checkup_id=checkup_id,
|
|
||||||
project_name=item.get("projectName") or item.get("project_name") or "",
|
|
||||||
project_fee=item.get("projectFee") if item.get("projectFee") is not None else (
|
|
||||||
item.get("project_fee") if item.get("project_fee") is not None else None
|
|
||||||
),
|
|
||||||
tenant_no=tenant_no,
|
|
||||||
order_num=idx + 1,
|
|
||||||
remark=item.get("remark") or "",
|
|
||||||
create_by=user_id,
|
|
||||||
create_dept_id="",
|
|
||||||
create_time=now,
|
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
db.add(row)
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return list_reserve_items(db, checkup_id, tenant_no)
|
return list_reserve_items(db, checkup_id, tenant_no)
|
||||||
|
|
||||||
@@ -520,35 +549,15 @@ def save_actual_items(
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
_sync_checkup_items(
|
||||||
# Soft-delete existing items
|
db,
|
||||||
db.query(BizPilotCheckupActualItem).filter(
|
BizPilotCheckupActualItem,
|
||||||
BizPilotCheckupActualItem.pilot_checkup_id == checkup_id,
|
checkup_id,
|
||||||
BizPilotCheckupActualItem.tenant_no == tenant_no,
|
items_json,
|
||||||
BizPilotCheckupActualItem.del_flag == 0,
|
tenant_no,
|
||||||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
user_id,
|
||||||
|
now,
|
||||||
# Insert new items
|
)
|
||||||
for idx, item in enumerate(items_json):
|
|
||||||
row = BizPilotCheckupActualItem(
|
|
||||||
id=uuid.uuid4().hex,
|
|
||||||
pilot_checkup_id=checkup_id,
|
|
||||||
project_name=item.get("projectName") or item.get("project_name") or "",
|
|
||||||
project_fee=item.get("projectFee") if item.get("projectFee") is not None else (
|
|
||||||
item.get("project_fee") if item.get("project_fee") is not None else None
|
|
||||||
),
|
|
||||||
tenant_no=tenant_no,
|
|
||||||
order_num=idx + 1,
|
|
||||||
remark=item.get("remark") or "",
|
|
||||||
create_by=user_id,
|
|
||||||
create_dept_id="",
|
|
||||||
create_time=now,
|
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
db.add(row)
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return list_actual_items(db, checkup_id, tenant_no)
|
return list_actual_items(db, checkup_id, tenant_no)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from typing import Any, Dict, List, Optional
|
|||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
|
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizChannelAccount,
|
BizChannelAccount,
|
||||||
BizChannelAgreementProductPrice,
|
BizChannelAgreementProductPrice,
|
||||||
@@ -539,7 +541,7 @@ def save_delivery_materials(
|
|||||||
tenant_no: str,
|
tenant_no: str,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Bulk save: soft-delete existing + insert new."""
|
"""按物料名称更新已有行,只插入新物料,只软删去掉的物料。"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
# Validate delivery exists
|
# Validate delivery exists
|
||||||
@@ -555,36 +557,40 @@ def save_delivery_materials(
|
|||||||
if not delivery:
|
if not delivery:
|
||||||
raise ValueError("交付信息不存在")
|
raise ValueError("交付信息不存在")
|
||||||
|
|
||||||
# Soft-delete existing materials
|
def build_row(row_id):
|
||||||
db.query(BizPilotDeliveryMaterial).filter(
|
return BizPilotDeliveryMaterial(
|
||||||
BizPilotDeliveryMaterial.delivery_id == delivery_id,
|
id=row_id,
|
||||||
BizPilotDeliveryMaterial.tenant_no == tenant_no,
|
|
||||||
BizPilotDeliveryMaterial.del_flag == 0,
|
|
||||||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
|
||||||
db.flush()
|
|
||||||
|
|
||||||
# Insert new
|
|
||||||
created = []
|
|
||||||
for idx, item in enumerate(items_json):
|
|
||||||
material = BizPilotDeliveryMaterial(
|
|
||||||
id=str(uuid.uuid4()),
|
|
||||||
delivery_id=delivery_id,
|
delivery_id=delivery_id,
|
||||||
material_name=item.get("material_name") or "",
|
material_name="",
|
||||||
qty=item.get("qty"),
|
|
||||||
unit_id=item.get("unit_id") or "",
|
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
order_num=idx + 1,
|
|
||||||
remark=item.get("remark") or "",
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id="",
|
create_dept_id="",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(material)
|
|
||||||
created.append(material)
|
|
||||||
|
|
||||||
|
def apply_row(row, item, idx, _now):
|
||||||
|
row.material_name = item.get("material_name") or ""
|
||||||
|
row.qty = item.get("qty")
|
||||||
|
row.unit_id = item.get("unit_id") or ""
|
||||||
|
row.order_num = idx + 1
|
||||||
|
row.remark = item.get("remark") or ""
|
||||||
|
|
||||||
|
created = sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizPilotDeliveryMaterial,
|
||||||
|
parent_attr="delivery_id",
|
||||||
|
parent_id=delivery_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=list(items_json or []),
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("material_name") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.material_name or "").strip(),
|
||||||
|
duplicate_message="交付物料重复",
|
||||||
|
foreign_message="交付物料不属于当前交付单",
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
for m in created:
|
for m in created:
|
||||||
db.refresh(m)
|
db.refresh(m)
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def get_confirm_by_schedule(
|
|||||||
if uid:
|
if uid:
|
||||||
user = (
|
user = (
|
||||||
db.query(SysUser)
|
db.query(SysUser)
|
||||||
.filter(SysUser.id == uid, SysUser.del_flag == 0)
|
.filter(SysUser.id == uid)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
data["confirmUserName"] = (user.nick_name or user.user_no or "") if user else ""
|
data["confirmUserName"] = (user.nick_name or user.user_no or "") if user else ""
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@@ -10,6 +9,7 @@ from sqlalchemy import and_, desc, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizChannelAgreementProductPrice,
|
BizChannelAgreementProductPrice,
|
||||||
BizChannelServiceOrder,
|
BizChannelServiceOrder,
|
||||||
@@ -69,6 +69,8 @@ def _row_to_dict_schedule(
|
|||||||
"totalCourseCount": course.get("totalCourseCount"),
|
"totalCourseCount": course.get("totalCourseCount"),
|
||||||
"cellType": row.cell_type or "",
|
"cellType": row.cell_type or "",
|
||||||
"specText": row.spec_text or "",
|
"specText": row.spec_text or "",
|
||||||
|
"packageId": getattr(row, "package_id", None) or "",
|
||||||
|
"packageText": getattr(row, "package_text", None) or "",
|
||||||
"infusionTime": row.infusion_time.strftime("%Y-%m-%d %H:%M:%S") if row.infusion_time else "",
|
"infusionTime": row.infusion_time.strftime("%Y-%m-%d %H:%M:%S") if row.infusion_time else "",
|
||||||
"infusionLocation": row.infusion_location or "",
|
"infusionLocation": row.infusion_location or "",
|
||||||
"infusionMethod": row.infusion_method or "",
|
"infusionMethod": row.infusion_method or "",
|
||||||
@@ -142,6 +144,8 @@ def _row_to_dict_product(row) -> Dict[str, Any]:
|
|||||||
"projectName": row.project_name or "",
|
"projectName": row.project_name or "",
|
||||||
"cellTypeName": row.cell_type_name or "",
|
"cellTypeName": row.cell_type_name or "",
|
||||||
"specText": row.spec_text or "",
|
"specText": row.spec_text or "",
|
||||||
|
"packageId": getattr(row, "package_id", None) or "",
|
||||||
|
"packageText": getattr(row, "package_text", None) or "",
|
||||||
"agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
|
"agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
|
||||||
"actualPrice": float(row.actual_price) if row.actual_price is not None else None,
|
"actualPrice": float(row.actual_price) if row.actual_price is not None else None,
|
||||||
"quantity": row.quantity if row.quantity is not None else 1,
|
"quantity": row.quantity if row.quantity is not None else 1,
|
||||||
@@ -516,6 +520,8 @@ def get_infusion_schedule_detail(
|
|||||||
sched["totalCourseCount"] = flat.get("totalCourseCount")
|
sched["totalCourseCount"] = flat.get("totalCourseCount")
|
||||||
sched["cellType"] = flat.get("cellType") or sched.get("cellType") or ""
|
sched["cellType"] = flat.get("cellType") or sched.get("cellType") or ""
|
||||||
sched["specText"] = flat.get("specText") or sched.get("specText") or ""
|
sched["specText"] = flat.get("specText") or sched.get("specText") or ""
|
||||||
|
sched["packageId"] = flat.get("packageId") or sched.get("packageId") or ""
|
||||||
|
sched["packageText"] = flat.get("packageText") or sched.get("packageText") or ""
|
||||||
sched["infusionTime"] = flat.get("infusionTime") or sched.get("infusionTime") or ""
|
sched["infusionTime"] = flat.get("infusionTime") or sched.get("infusionTime") or ""
|
||||||
sched["infusionLocation"] = flat.get("infusionLocation") or sched.get("infusionLocation") or ""
|
sched["infusionLocation"] = flat.get("infusionLocation") or sched.get("infusionLocation") or ""
|
||||||
sched["infusionMethod"] = flat.get("infusionMethod") or sched.get("infusionMethod") or ""
|
sched["infusionMethod"] = flat.get("infusionMethod") or sched.get("infusionMethod") or ""
|
||||||
@@ -1114,6 +1120,8 @@ def update_infusion_schedule_mgmt(
|
|||||||
fields = {
|
fields = {
|
||||||
"cellType": "cell_type",
|
"cellType": "cell_type",
|
||||||
"specText": "spec_text",
|
"specText": "spec_text",
|
||||||
|
"packageId": "package_id",
|
||||||
|
"packageText": "package_text",
|
||||||
"infusionLocation": "infusion_location",
|
"infusionLocation": "infusion_location",
|
||||||
"infusionMethod": "infusion_method",
|
"infusionMethod": "infusion_method",
|
||||||
}
|
}
|
||||||
@@ -1134,44 +1142,68 @@ def update_infusion_schedule_mgmt(
|
|||||||
row.update_by = user_id
|
row.update_by = user_id
|
||||||
row.update_time = now
|
row.update_time = now
|
||||||
|
|
||||||
# Update products if provided
|
|
||||||
products_data = data.get("products")
|
products_data = data.get("products")
|
||||||
if products_data is not None:
|
if products_data is not None:
|
||||||
# Soft-delete existing products
|
def _price_id(item):
|
||||||
db.query(BizPilotInfusionScheduleProduct).filter(
|
return (item.get("priceId") or item.get("price_id") or "").strip()
|
||||||
BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
|
|
||||||
BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
|
|
||||||
BizPilotInfusionScheduleProduct.del_flag == 0,
|
|
||||||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
|
||||||
|
|
||||||
for idx, item in enumerate(products_data):
|
def _product_key(item):
|
||||||
p_row = BizPilotInfusionScheduleProduct(
|
price_id = _price_id(item)
|
||||||
id=str(uuid.uuid4()),
|
if price_id:
|
||||||
|
return "price:" + price_id
|
||||||
|
project_code = (item.get("projectCode") or item.get("project_code") or "").strip()
|
||||||
|
spec_text = (item.get("specText") or item.get("spec_text") or "").strip()
|
||||||
|
return f"proj:{project_code}|{spec_text}"
|
||||||
|
|
||||||
|
def build_product(row_id):
|
||||||
|
return BizPilotInfusionScheduleProduct(
|
||||||
|
id=row_id,
|
||||||
infusion_schedule_id=schedule_id,
|
infusion_schedule_id=schedule_id,
|
||||||
price_id=item.get("priceId") or item.get("price_id") or "",
|
|
||||||
project_code=item.get("projectCode") or item.get("project_code") or "",
|
|
||||||
project_name=item.get("projectName") or item.get("project_name") or "",
|
|
||||||
cell_type_name=item.get("cellTypeName") or item.get("cell_type_name") or "",
|
|
||||||
spec_text=item.get("specText") or item.get("spec_text") or "",
|
|
||||||
agreement_price=item.get("agreementPrice") if item.get("agreementPrice") is not None else (
|
|
||||||
item.get("agreement_price") if item.get("agreement_price") is not None else None
|
|
||||||
),
|
|
||||||
actual_price=item.get("actualPrice") if item.get("actualPrice") is not None else (
|
|
||||||
item.get("actual_price") if item.get("actual_price") is not None else None
|
|
||||||
),
|
|
||||||
quantity=item.get("quantity") if item.get("quantity") is not None else 1,
|
|
||||||
product_status=status,
|
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
order_num=idx + 1,
|
|
||||||
remark=item.get("remark") or "",
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id="",
|
create_dept_id="",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(p_row)
|
|
||||||
|
def apply_product(row, item, idx, _now):
|
||||||
|
row.price_id = _price_id(item)
|
||||||
|
row.project_code = item.get("projectCode") or item.get("project_code") or ""
|
||||||
|
row.project_name = item.get("projectName") or item.get("project_name") or ""
|
||||||
|
row.cell_type_name = item.get("cellTypeName") or item.get("cell_type_name") or ""
|
||||||
|
row.spec_text = item.get("specText") or item.get("spec_text") or ""
|
||||||
|
row.package_id = item.get("packageId") or item.get("package_id") or ""
|
||||||
|
row.package_text = item.get("packageText") or item.get("package_text") or ""
|
||||||
|
row.agreement_price = item.get("agreementPrice") if item.get("agreementPrice") is not None else (
|
||||||
|
item.get("agreement_price") if item.get("agreement_price") is not None else None
|
||||||
|
)
|
||||||
|
row.actual_price = item.get("actualPrice") if item.get("actualPrice") is not None else (
|
||||||
|
item.get("actual_price") if item.get("actual_price") is not None else None
|
||||||
|
)
|
||||||
|
row.quantity = item.get("quantity") if item.get("quantity") is not None else 1
|
||||||
|
row.product_status = status
|
||||||
|
row.order_num = idx + 1
|
||||||
|
row.remark = item.get("remark") or ""
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizPilotInfusionScheduleProduct,
|
||||||
|
parent_attr="infusion_schedule_id",
|
||||||
|
parent_id=schedule_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=list(products_data or []),
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_product,
|
||||||
|
apply_fields=apply_product,
|
||||||
|
natural_key=_product_key,
|
||||||
|
row_natural_key=lambda row: (
|
||||||
|
"price:" + (row.price_id or "").strip()
|
||||||
|
if (row.price_id or "").strip()
|
||||||
|
else f"proj:{(row.project_code or '').strip()}|{(row.spec_text or '').strip()}"
|
||||||
|
),
|
||||||
|
duplicate_message="输注产品重复",
|
||||||
|
foreign_message="输注产品不属于当前计划",
|
||||||
|
)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(row)
|
db.refresh(row)
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ from sqlalchemy import and_, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.collection_protocol import urls_to_attachment_list
|
from app.crud.collection_protocol import urls_to_attachment_list
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizCellProductType,
|
BizCellProductType,
|
||||||
BizCellProductSpec,
|
BizCellProductSpec,
|
||||||
|
BizCellProductPackage,
|
||||||
BizCellProductPrice,
|
BizCellProductPrice,
|
||||||
BizCellProductProjectSystem,
|
BizCellProductProjectSystem,
|
||||||
BizCellProductProjectSystemItem,
|
BizCellProductProjectSystemItem,
|
||||||
@@ -281,6 +283,141 @@ def soft_delete_cell_product_spec(db, spec_id, tenant_no=None):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────
|
||||||
|
# Entity 2b: BizCellProductPackage 细胞产品包装
|
||||||
|
# ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _row_to_dict_cell_product_package(row) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"packageCode": row.package_code or "",
|
||||||
|
"packageName": row.package_name or "",
|
||||||
|
"tenantNo": row.tenant_no or "",
|
||||||
|
"orderNum": row.order_num if row.order_num is not None else 0,
|
||||||
|
"remark": row.remark or "",
|
||||||
|
"createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
|
||||||
|
"updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_cell_product_packages(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
|
||||||
|
def _filtered():
|
||||||
|
q0 = db.query(BizCellProductPackage).filter(BizCellProductPackage.del_flag == 0)
|
||||||
|
if tenant_no:
|
||||||
|
q0 = q0.filter(BizCellProductPackage.tenant_no == tenant_no)
|
||||||
|
kw = (keyword or "").strip()
|
||||||
|
if kw:
|
||||||
|
like = f"%{kw}%"
|
||||||
|
q0 = q0.filter(
|
||||||
|
or_(
|
||||||
|
BizCellProductPackage.package_code.like(like),
|
||||||
|
BizCellProductPackage.package_name.like(like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if user_info:
|
||||||
|
from app.core.data_scope import apply_data_scope
|
||||||
|
q0 = apply_data_scope(q0, BizCellProductPackage, user_info, db, tenant_no)
|
||||||
|
return q0
|
||||||
|
|
||||||
|
total = _filtered().order_by(None).with_entities(func.count(BizCellProductPackage.id)).scalar() or 0
|
||||||
|
page = max(1, page)
|
||||||
|
page_size = min(max(1, page_size), 100)
|
||||||
|
rows = (
|
||||||
|
_filtered()
|
||||||
|
.order_by(BizCellProductPackage.create_time.desc())
|
||||||
|
.offset((page - 1) * page_size)
|
||||||
|
.limit(page_size)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [_row_to_dict_cell_product_package(x) for x in rows], int(total)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cell_product_package(db, package_id, tenant_no=None):
|
||||||
|
q = db.query(BizCellProductPackage).filter(
|
||||||
|
and_(BizCellProductPackage.id == package_id, BizCellProductPackage.del_flag == 0)
|
||||||
|
)
|
||||||
|
if tenant_no:
|
||||||
|
q = q.filter(BizCellProductPackage.tenant_no == tenant_no)
|
||||||
|
return q.first()
|
||||||
|
|
||||||
|
|
||||||
|
def cell_product_package_code_exists(db, code, tenant_no, exclude_id=None):
|
||||||
|
q = db.query(BizCellProductPackage).filter(
|
||||||
|
and_(BizCellProductPackage.package_code == code, BizCellProductPackage.del_flag == 0)
|
||||||
|
)
|
||||||
|
if tenant_no:
|
||||||
|
q = q.filter(BizCellProductPackage.tenant_no == tenant_no)
|
||||||
|
if exclude_id:
|
||||||
|
q = q.filter(BizCellProductPackage.id != exclude_id)
|
||||||
|
return db.query(q.exists()).scalar()
|
||||||
|
|
||||||
|
|
||||||
|
def create_cell_product_package(
|
||||||
|
db, *, package_code, package_name, tenant_no="zkxw", order_num=None, remark=None, user_id=None
|
||||||
|
):
|
||||||
|
if cell_product_package_code_exists(db, package_code, tenant_no):
|
||||||
|
raise ValueError(f"包装编码 {package_code} 已存在")
|
||||||
|
obj = BizCellProductPackage(
|
||||||
|
id=generate_uuid(),
|
||||||
|
package_code=package_code,
|
||||||
|
package_name=package_name,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
order_num=order_num or 0,
|
||||||
|
remark=remark or "",
|
||||||
|
create_by=user_id,
|
||||||
|
create_time=datetime.now(),
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=datetime.now(),
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def update_cell_product_package(
|
||||||
|
db,
|
||||||
|
package_id,
|
||||||
|
*,
|
||||||
|
package_code=None,
|
||||||
|
package_name=None,
|
||||||
|
order_num=None,
|
||||||
|
remark=None,
|
||||||
|
user_id=None,
|
||||||
|
tenant_no=None,
|
||||||
|
):
|
||||||
|
obj = get_cell_product_package(db, package_id, tenant_no)
|
||||||
|
if not obj:
|
||||||
|
raise ValueError("记录不存在")
|
||||||
|
if package_code is not None and package_code != obj.package_code:
|
||||||
|
if cell_product_package_code_exists(db, package_code, tenant_no, exclude_id=package_id):
|
||||||
|
raise ValueError(f"包装编码 {package_code} 已存在")
|
||||||
|
obj.package_code = package_code
|
||||||
|
if package_name is not None:
|
||||||
|
obj.package_name = package_name
|
||||||
|
if order_num is not None:
|
||||||
|
obj.order_num = order_num
|
||||||
|
if remark is not None:
|
||||||
|
obj.remark = remark
|
||||||
|
obj.update_by = user_id
|
||||||
|
obj.update_time = datetime.now()
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def soft_delete_cell_product_package(db, package_id, tenant_no=None):
|
||||||
|
obj = get_cell_product_package(db, package_id, tenant_no)
|
||||||
|
if not obj:
|
||||||
|
raise ValueError("记录不存在")
|
||||||
|
obj.del_flag = 1
|
||||||
|
obj.update_time = datetime.now()
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
# ────────────────────────────────────────────
|
# ────────────────────────────────────────────
|
||||||
# Entity 3: BizCellProductPrice 细胞产品价格
|
# Entity 3: BizCellProductPrice 细胞产品价格
|
||||||
# ────────────────────────────────────────────
|
# ────────────────────────────────────────────
|
||||||
@@ -2151,7 +2288,9 @@ def _insert_package_items(
|
|||||||
tenant_no,
|
tenant_no,
|
||||||
user_id=None,
|
user_id=None,
|
||||||
):
|
):
|
||||||
for idx, it in enumerate(items):
|
now = datetime.now()
|
||||||
|
prepared = []
|
||||||
|
for idx, it in enumerate(items or []):
|
||||||
pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
|
pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
|
||||||
proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
|
proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
|
||||||
name = (it.get("project_name") or it.get("projectName") or "").strip()
|
name = (it.get("project_name") or it.get("projectName") or "").strip()
|
||||||
@@ -2160,22 +2299,51 @@ def _insert_package_items(
|
|||||||
fee = it.get("project_fee")
|
fee = it.get("project_fee")
|
||||||
if fee is None:
|
if fee is None:
|
||||||
fee = it.get("projectFee")
|
fee = it.get("projectFee")
|
||||||
obj = BizCheckupInstitutionPackageItem(
|
prepared.append(
|
||||||
id=generate_uuid(),
|
{
|
||||||
package_id=package_id,
|
"project_id": pid,
|
||||||
checkup_institution_project_id=pid,
|
"project_name": name,
|
||||||
project_name=name,
|
"project_fee": fee,
|
||||||
project_fee=fee,
|
"order_num": it.get("order_num") if it.get("order_num") is not None else it.get("orderNum") or (idx + 1),
|
||||||
tenant_no=tenant_no,
|
"remark": it.get("remark") or "",
|
||||||
order_num=it.get("order_num") if it.get("order_num") is not None else it.get("orderNum") or (idx + 1),
|
}
|
||||||
remark=it.get("remark") or "",
|
|
||||||
create_by=user_id,
|
|
||||||
create_time=datetime.now(),
|
|
||||||
update_by=user_id,
|
|
||||||
update_time=datetime.now(),
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(obj)
|
|
||||||
|
def build_row(row_id):
|
||||||
|
return BizCheckupInstitutionPackageItem(
|
||||||
|
id=row_id,
|
||||||
|
package_id=package_id,
|
||||||
|
checkup_institution_project_id="",
|
||||||
|
project_name="",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_row(row, item, _idx, _now):
|
||||||
|
row.checkup_institution_project_id = item["project_id"]
|
||||||
|
row.project_name = item["project_name"]
|
||||||
|
row.project_fee = item["project_fee"]
|
||||||
|
row.order_num = item["order_num"]
|
||||||
|
row.remark = item["remark"]
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizCheckupInstitutionPackageItem,
|
||||||
|
parent_attr="package_id",
|
||||||
|
parent_id=package_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=prepared,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: str(item.get("project_id") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.checkup_institution_project_id or "").strip(),
|
||||||
|
duplicate_message="套餐项目重复",
|
||||||
|
foreign_message="套餐项目不属于当前套餐",
|
||||||
|
reuse_deleted=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None, user_info=None):
|
def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None, user_info=None):
|
||||||
@@ -2319,15 +2487,6 @@ def update_checkup_institution_package(
|
|||||||
tenant_no,
|
tenant_no,
|
||||||
existing_item_project_ids=existing_pids,
|
existing_item_project_ids=existing_pids,
|
||||||
)
|
)
|
||||||
# 全量替换:物理删除旧明细,避免 uk(package_id, project_id) 与软删行冲突
|
|
||||||
q_items = db.query(BizCheckupInstitutionPackageItem).filter(
|
|
||||||
BizCheckupInstitutionPackageItem.package_id == package_id
|
|
||||||
)
|
|
||||||
if tenant_no:
|
|
||||||
q_items = q_items.filter(BizCheckupInstitutionPackageItem.tenant_no == tenant_no)
|
|
||||||
for row in q_items.all():
|
|
||||||
db.delete(row)
|
|
||||||
db.flush()
|
|
||||||
_insert_package_items(
|
_insert_package_items(
|
||||||
db,
|
db,
|
||||||
package_id=package_id,
|
package_id=package_id,
|
||||||
@@ -3076,50 +3235,60 @@ def _save_gmcp_product_rows(
|
|||||||
user_id,
|
user_id,
|
||||||
now=None,
|
now=None,
|
||||||
):
|
):
|
||||||
"""仅保存购买订单产品明细,不调整份数库存"""
|
"""仅保存购买订单产品明细,不调整份数库存。已有项目按编码、规格、细胞类型更新。"""
|
||||||
now = now or datetime.now()
|
now = now or datetime.now()
|
||||||
for p in products:
|
tenant = tenant_no or "zkxw"
|
||||||
project_code = p.get("projectCode") or p.get("project_code") or ""
|
|
||||||
project_name = p.get("projectName") or p.get("project_name") or ""
|
|
||||||
cell_type = p.get("cellType") or p.get("cell_type") or ""
|
|
||||||
spec_text = p.get("specText") or p.get("spec_text") or ""
|
|
||||||
agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
|
|
||||||
buy_qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
|
|
||||||
buy_amount = float(p.get("buyAmount") or p.get("buy_amount") or agreement_unit_price * buy_qty)
|
|
||||||
|
|
||||||
prod_row = BizChannelServiceOrderProduct(
|
def _product_key(item):
|
||||||
id=generate_uuid(),
|
project_code = (item.get("projectCode") or item.get("project_code") or "").strip()
|
||||||
|
spec_text = (item.get("specText") or item.get("spec_text") or "").strip()
|
||||||
|
cell_type = (item.get("cellType") or item.get("cell_type") or "").strip()
|
||||||
|
return f"{project_code}|{spec_text}|{cell_type}"
|
||||||
|
|
||||||
|
def build_row(row_id):
|
||||||
|
return BizChannelServiceOrderProduct(
|
||||||
|
id=row_id,
|
||||||
service_order_id=order_id,
|
service_order_id=order_id,
|
||||||
project_code=project_code,
|
tenant_no=tenant,
|
||||||
project_name=project_name,
|
|
||||||
cell_type=cell_type,
|
|
||||||
spec_text=spec_text,
|
|
||||||
agreement_unit_price=agreement_unit_price,
|
|
||||||
buy_qty=buy_qty,
|
|
||||||
buy_amount=buy_amount,
|
|
||||||
tenant_no=tenant_no or "zkxw",
|
|
||||||
order_num=0,
|
|
||||||
remark="",
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id="",
|
create_dept_id="",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(prod_row)
|
|
||||||
|
def apply_row(row, p, _idx, _now):
|
||||||
|
agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
|
||||||
|
buy_qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
|
||||||
|
row.project_code = p.get("projectCode") or p.get("project_code") or ""
|
||||||
|
row.project_name = p.get("projectName") or p.get("project_name") or ""
|
||||||
|
row.cell_type = p.get("cellType") or p.get("cell_type") or ""
|
||||||
|
row.spec_text = p.get("specText") or p.get("spec_text") or ""
|
||||||
|
row.agreement_unit_price = agreement_unit_price
|
||||||
|
row.buy_qty = buy_qty
|
||||||
|
row.buy_amount = float(p.get("buyAmount") or p.get("buy_amount") or agreement_unit_price * buy_qty)
|
||||||
|
row.order_num = 0
|
||||||
|
row.remark = ""
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizChannelServiceOrderProduct,
|
||||||
|
parent_attr="service_order_id",
|
||||||
|
parent_id=order_id,
|
||||||
|
tenant_no=tenant,
|
||||||
|
items=list(products or []),
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=_product_key,
|
||||||
|
row_natural_key=lambda row: f"{(row.project_code or '').strip()}|{(row.spec_text or '').strip()}|{(row.cell_type or '').strip()}",
|
||||||
|
duplicate_message="订单产品重复",
|
||||||
|
foreign_message="订单产品不属于当前订单",
|
||||||
|
)
|
||||||
db.flush()
|
db.flush()
|
||||||
|
|
||||||
|
|
||||||
def _replace_gmcp_product_rows(db, order_id, products, tenant_no, user_id):
|
def _replace_gmcp_product_rows(db, order_id, products, tenant_no, user_id):
|
||||||
now = datetime.now()
|
_save_gmcp_product_rows(db, order_id, products, tenant_no, user_id)
|
||||||
db.query(BizChannelServiceOrderProduct).filter(
|
|
||||||
and_(
|
|
||||||
BizChannelServiceOrderProduct.service_order_id == order_id,
|
|
||||||
BizChannelServiceOrderProduct.del_flag == 0,
|
|
||||||
)
|
|
||||||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
|
||||||
_save_gmcp_product_rows(db, order_id, products, tenant_no, user_id, now=now)
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_gmcp_order_quotas(db, order, tenant_no, user_id):
|
def _apply_gmcp_order_quotas(db, order, tenant_no, user_id):
|
||||||
|
|||||||
+14
-26
@@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_link_rows
|
||||||
from app.models.models import SysPost, SysPostRole, SysRole, SysUser, SysUserPost
|
from app.models.models import SysPost, SysPostRole, SysRole, SysUser, SysUserPost
|
||||||
|
|
||||||
|
|
||||||
@@ -232,7 +233,7 @@ def sync_post_role_bindings(
|
|||||||
role_ids: List[str],
|
role_ids: List[str],
|
||||||
updater_id: Optional[str],
|
updater_id: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""将本职位绑定的角色集合全量替换为 role_ids。"""
|
"""按角色更新本职位的角色关联。"""
|
||||||
ordered = list(dict.fromkeys([rid for rid in (role_ids or []) if rid]))
|
ordered = list(dict.fromkeys([rid for rid in (role_ids or []) if rid]))
|
||||||
for rid in ordered:
|
for rid in ordered:
|
||||||
role = (
|
role = (
|
||||||
@@ -249,31 +250,18 @@ def sync_post_role_bindings(
|
|||||||
if not role:
|
if not role:
|
||||||
raise ValueError("存在无效角色或不属于当前租户")
|
raise ValueError("存在无效角色或不属于当前租户")
|
||||||
|
|
||||||
now = datetime.now()
|
sync_link_rows(
|
||||||
db.query(SysPostRole).filter(
|
db,
|
||||||
and_(
|
SysPostRole,
|
||||||
SysPostRole.post_id == post_id,
|
parent_attr="post_id",
|
||||||
SysPostRole.tenant_no == tenant_no,
|
parent_id=post_id,
|
||||||
SysPostRole.del_flag == 0,
|
link_attr="role_id",
|
||||||
)
|
link_ids=ordered,
|
||||||
).update({"del_flag": 1, "update_time": now, "update_by": updater_id}, synchronize_session=False)
|
tenant_no=tenant_no,
|
||||||
for rid in ordered:
|
user_id=updater_id,
|
||||||
db.add(
|
duplicate_message="角色重复",
|
||||||
SysPostRole(
|
foreign_message="角色关联不属于当前职位",
|
||||||
id=generate_uuid(),
|
)
|
||||||
post_id=post_id,
|
|
||||||
role_id=rid,
|
|
||||||
tenant_no=tenant_no,
|
|
||||||
order_num=0,
|
|
||||||
remark=None,
|
|
||||||
create_by=updater_id,
|
|
||||||
create_dept_id="",
|
|
||||||
create_time=now,
|
|
||||||
update_by=updater_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+132
-12
@@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizProcess,
|
BizProcess,
|
||||||
BizProcessCategory,
|
BizProcessCategory,
|
||||||
@@ -300,6 +301,132 @@ def _create_sub_data_collection(db: Session, process_id: str, tenant_no: str, us
|
|||||||
db.add(obj)
|
db.add(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _new_process_child(model, row_id: str, process_id: str, tenant_no: str, user_id: Optional[str], now: datetime):
|
||||||
|
return model(
|
||||||
|
id=row_id,
|
||||||
|
process_id=process_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_process_reagents(db, process_id, tenant_no, user_id, now, items):
|
||||||
|
def apply_row(row, data, idx, _now):
|
||||||
|
item = _sub_item_with_order(data, idx)
|
||||||
|
row.reagent_code = (item.get("reagent_code") or "").strip()
|
||||||
|
row.reagent_name = (item.get("reagent_name") or "").strip()
|
||||||
|
row.spec = (item.get("spec") or "").strip() or None
|
||||||
|
row.standard_usage = (item.get("standard_usage") or "").strip() or None
|
||||||
|
row.unit = (item.get("unit") or "").strip() or None
|
||||||
|
row.is_key_material = item.get("is_key_material", 0)
|
||||||
|
row.remark = (item.get("remark") or "").strip() or None
|
||||||
|
row.order_num = item.get("order_num", 0) if item.get("order_num") is not None else 0
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessReagent,
|
||||||
|
parent_attr="process_id",
|
||||||
|
parent_id=process_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_process_child(BizProcessReagent, row_id, process_id, tenant_no, user_id, now),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("reagent_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.reagent_code or "").strip(),
|
||||||
|
duplicate_message="试剂编号重复",
|
||||||
|
foreign_message="试剂不属于当前工艺",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_process_consumables(db, process_id, tenant_no, user_id, now, items):
|
||||||
|
def apply_row(row, data, idx, _now):
|
||||||
|
item = _sub_item_with_order(data, idx)
|
||||||
|
row.consumable_code = (item.get("consumable_code") or "").strip()
|
||||||
|
row.consumable_name = (item.get("consumable_name") or "").strip()
|
||||||
|
row.spec_model = (item.get("spec_model") or "").strip() or None
|
||||||
|
row.standard_usage = (item.get("standard_usage") or "").strip() or None
|
||||||
|
row.unit = (item.get("unit") or "").strip() or None
|
||||||
|
row.is_required = item.get("is_required", 0)
|
||||||
|
row.remark = (item.get("remark") or "").strip() or None
|
||||||
|
row.order_num = item.get("order_num", 0) if item.get("order_num") is not None else 0
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessConsumable,
|
||||||
|
parent_attr="process_id",
|
||||||
|
parent_id=process_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_process_child(BizProcessConsumable, row_id, process_id, tenant_no, user_id, now),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("consumable_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.consumable_code or "").strip(),
|
||||||
|
duplicate_message="耗材编号重复",
|
||||||
|
foreign_message="耗材不属于当前工艺",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_process_outputs(db, process_id, tenant_no, user_id, now, items):
|
||||||
|
def apply_row(row, data, idx, _now):
|
||||||
|
item = _sub_item_with_order(data, idx)
|
||||||
|
row.output_type_id = (item.get("output_type_id") or "").strip() or None
|
||||||
|
row.output_name = (item.get("output_name") or "").strip() or None
|
||||||
|
row.output_barcode = (item.get("output_barcode") or "").strip() or None
|
||||||
|
row.remark = (item.get("remark") or "").strip() or None
|
||||||
|
row.order_num = item.get("order_num", 0) if item.get("order_num") is not None else 0
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessOutput,
|
||||||
|
parent_attr="process_id",
|
||||||
|
parent_id=process_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_process_child(BizProcessOutput, row_id, process_id, tenant_no, user_id, now),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("output_barcode") or item.get("output_name") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.output_barcode or row.output_name or "").strip(),
|
||||||
|
duplicate_message="产出物编号重复",
|
||||||
|
foreign_message="产出物不属于当前工艺",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_process_data_collections(db, process_id, tenant_no, user_id, now, items):
|
||||||
|
def apply_row(row, data, idx, _now):
|
||||||
|
row.collection_name = (data.get("collection_name") or "").strip()
|
||||||
|
row.unit = (data.get("unit") or "").strip() or None
|
||||||
|
order_num = data.get("order_num")
|
||||||
|
row.order_num = idx if order_num is None else order_num
|
||||||
|
row.remark = (data.get("remark") or "").strip() or None
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessDataCollection,
|
||||||
|
parent_attr="process_id",
|
||||||
|
parent_id=process_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_process_child(
|
||||||
|
BizProcessDataCollection, row_id, process_id, tenant_no, user_id, now
|
||||||
|
),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("collection_name") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.collection_name or "").strip(),
|
||||||
|
duplicate_message="数据采集编号重复",
|
||||||
|
foreign_message="数据采集不属于当前工艺",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_process(
|
def create_process(
|
||||||
db: Session,
|
db: Session,
|
||||||
*,
|
*,
|
||||||
@@ -434,22 +561,15 @@ def update_process(
|
|||||||
sub_updated = reagents is not None or consumables is not None or outputs is not None or data_collections is not None
|
sub_updated = reagents is not None or consumables is not None or outputs is not None or data_collections is not None
|
||||||
if sub_updated:
|
if sub_updated:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
tenant_no = obj.tenant_no
|
||||||
if reagents is not None:
|
if reagents is not None:
|
||||||
_soft_delete_sub_records(db, BizProcessReagent, process_id, now)
|
_sync_process_reagents(db, process_id, tenant_no, user_id, now, reagents)
|
||||||
for idx, r in enumerate(reagents):
|
|
||||||
_create_sub_reagent(db, process_id, obj.tenant_no, user_id, now, _sub_item_with_order(r, idx))
|
|
||||||
if consumables is not None:
|
if consumables is not None:
|
||||||
_soft_delete_sub_records(db, BizProcessConsumable, process_id, now)
|
_sync_process_consumables(db, process_id, tenant_no, user_id, now, consumables)
|
||||||
for idx, c in enumerate(consumables):
|
|
||||||
_create_sub_consumable(db, process_id, obj.tenant_no, user_id, now, _sub_item_with_order(c, idx))
|
|
||||||
if outputs is not None:
|
if outputs is not None:
|
||||||
_soft_delete_sub_records(db, BizProcessOutput, process_id, now)
|
_sync_process_outputs(db, process_id, tenant_no, user_id, now, outputs)
|
||||||
for idx, o in enumerate(outputs):
|
|
||||||
_create_sub_output(db, process_id, obj.tenant_no, user_id, now, _sub_item_with_order(o, idx))
|
|
||||||
if data_collections is not None:
|
if data_collections is not None:
|
||||||
_soft_delete_sub_records(db, BizProcessDataCollection, process_id, now)
|
_sync_process_data_collections(db, process_id, tenant_no, user_id, now, data_collections)
|
||||||
for d in data_collections:
|
|
||||||
_create_sub_data_collection(db, process_id, obj.tenant_no, user_id, now, d)
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(obj)
|
db.refresh(obj)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
|
from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizProductionProductType,
|
BizProductionProductType,
|
||||||
@@ -652,38 +653,147 @@ def soft_delete_route(db: Session, route_id: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 子表完整替换更新
|
# 子表按行更新:保留原行 ID,去掉的才软删除
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _new_route_step_child(model, row_id, step_id, tenant_no, user_id, now):
|
||||||
|
return model(
|
||||||
|
id=row_id,
|
||||||
|
step_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_step_reagents(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
def update_step_reagents(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
_soft_delete_sub_records(db, BizProcessRouteStepReagent, step_id, now)
|
|
||||||
for it in items:
|
def apply_row(row, data, idx, _now):
|
||||||
_create_step_reagent(db, step_id, tenant_no, user_id, now, it)
|
row.reagent_code = (data.get("reagent_code") or "").strip() or None
|
||||||
|
row.reagent_name = (data.get("reagent_name") or "").strip() or None
|
||||||
|
row.spec = (data.get("spec") or "").strip() or None
|
||||||
|
row.standard_usage = (data.get("standard_usage") or "").strip() or None
|
||||||
|
row.unit = (data.get("unit") or "").strip() or None
|
||||||
|
row.is_key_material = data.get("is_key_material", "0")
|
||||||
|
row.remark = (data.get("remark") or "").strip() or None
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessRouteStepReagent,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_route_step_child(
|
||||||
|
BizProcessRouteStepReagent, row_id, step_id, tenant_no, user_id, now
|
||||||
|
),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("reagent_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.reagent_code or "").strip(),
|
||||||
|
duplicate_message="试剂编号重复",
|
||||||
|
foreign_message="试剂不属于当前步骤",
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def update_step_consumables(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
def update_step_consumables(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
_soft_delete_sub_records(db, BizProcessRouteStepConsumable, step_id, now)
|
|
||||||
for it in items:
|
def apply_row(row, data, idx, _now):
|
||||||
_create_step_consumable(db, step_id, tenant_no, user_id, now, it)
|
row.consumable_code = (data.get("consumable_code") or "").strip() or None
|
||||||
|
row.consumable_name = (data.get("consumable_name") or "").strip() or None
|
||||||
|
row.spec_model = (data.get("spec_model") or "").strip() or None
|
||||||
|
row.standard_usage = (data.get("standard_usage") or "").strip() or None
|
||||||
|
row.unit = (data.get("unit") or "").strip() or None
|
||||||
|
row.is_required = data.get("is_required", "1")
|
||||||
|
row.remark = (data.get("remark") or "").strip() or None
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessRouteStepConsumable,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_route_step_child(
|
||||||
|
BizProcessRouteStepConsumable, row_id, step_id, tenant_no, user_id, now
|
||||||
|
),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("consumable_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.consumable_code or "").strip(),
|
||||||
|
duplicate_message="耗材编号重复",
|
||||||
|
foreign_message="耗材不属于当前步骤",
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def update_step_outputs(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
def update_step_outputs(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
_soft_delete_sub_records(db, BizProcessRouteStepOutput, step_id, now)
|
|
||||||
for it in items:
|
def apply_row(row, data, idx, _now):
|
||||||
_create_step_output(db, step_id, tenant_no, user_id, now, it)
|
row.output_type_id = (data.get("output_type_id") or "").strip() or None
|
||||||
|
row.output_name = (data.get("output_name") or "").strip() or None
|
||||||
|
row.output_barcode = (data.get("output_barcode") or "").strip() or None
|
||||||
|
row.remark = (data.get("remark") or "").strip() or None
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessRouteStepOutput,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_route_step_child(
|
||||||
|
BizProcessRouteStepOutput, row_id, step_id, tenant_no, user_id, now
|
||||||
|
),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("output_barcode") or item.get("output_name") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.output_barcode or row.output_name or "").strip(),
|
||||||
|
duplicate_message="产出物编号重复",
|
||||||
|
foreign_message="产出物不属于当前步骤",
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def update_step_data_collections(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
def update_step_data_collections(db: Session, step_id: str, items: List[Dict], tenant_no: str, user_id: Optional[str] = None):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
_soft_delete_sub_records(db, BizProcessRouteStepDataCollection, step_id, now)
|
|
||||||
for it in items:
|
def apply_row(row, data, idx, _now):
|
||||||
_create_step_data_collection(db, step_id, tenant_no, user_id, now, it)
|
row.collection_name = (data.get("collection_name") or "").strip() or None
|
||||||
|
row.unit = (data.get("unit") or "").strip() or None
|
||||||
|
order_num = data.get("order_num")
|
||||||
|
row.order_num = idx if order_num is None else (order_num or 0)
|
||||||
|
row.remark = (data.get("remark") or "").strip() or None
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProcessRouteStepDataCollection,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=lambda row_id: _new_route_step_child(
|
||||||
|
BizProcessRouteStepDataCollection, row_id, step_id, tenant_no, user_id, now
|
||||||
|
),
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: (item.get("collection_name") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.collection_name or "").strip(),
|
||||||
|
duplicate_message="数据采集编号重复",
|
||||||
|
foreign_message="数据采集不属于当前步骤",
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.production_material import _dec, _fmt_dt
|
from app.crud.production_material import _dec, _fmt_dt
|
||||||
from app.models.production_material import BizProductionMaterial, BizProductionMaterialFormula
|
from app.models.production_material import BizProductionMaterial, BizProductionMaterialFormula
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ def replace_formula(
|
|||||||
raise ValueError("请至少添加一条配方明细")
|
raise ValueError("请至少添加一条配方明细")
|
||||||
|
|
||||||
seen_components: set = set()
|
seen_components: set = set()
|
||||||
new_rows: List[BizProductionMaterialFormula] = []
|
normalized: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
for idx, it in enumerate(lines):
|
for idx, it in enumerate(lines):
|
||||||
comp_id = (
|
comp_id = (
|
||||||
@@ -125,45 +125,58 @@ def replace_formula(
|
|||||||
it.get("usageUnit") or it.get("usage_unit") or comp.unit or ""
|
it.get("usageUnit") or it.get("usage_unit") or comp.unit or ""
|
||||||
).strip()[:32]
|
).strip()[:32]
|
||||||
|
|
||||||
new_rows.append(
|
normalized.append(
|
||||||
BizProductionMaterialFormula(
|
{
|
||||||
id=generate_uuid(),
|
"id": (it.get("id") or "").strip(),
|
||||||
configured_material_id=configured_material_id,
|
"component_material_id": comp_id,
|
||||||
component_material_id=comp_id,
|
"component_material_type": comp_type,
|
||||||
component_material_type=comp_type,
|
"component_material_code": comp.material_code,
|
||||||
component_material_code=comp.material_code,
|
"component_material_name": comp.material_name,
|
||||||
component_material_name=comp.material_name,
|
"standard_qty": standard_qty,
|
||||||
standard_qty=standard_qty,
|
"usage_unit": usage_unit or None,
|
||||||
usage_unit=usage_unit or None,
|
"order_num": idx,
|
||||||
tenant_no=tenant_no,
|
"remark": (it.get("remark") or "").strip()[:512] or None,
|
||||||
order_num=idx,
|
}
|
||||||
remark=(it.get("remark") or "").strip()[:512] or None,
|
|
||||||
create_by=user_id,
|
|
||||||
create_dept_id=dept_id or configured.create_dept_id or "",
|
|
||||||
create_time=now,
|
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
existing = (
|
dept = dept_id or configured.create_dept_id or ""
|
||||||
db.query(BizProductionMaterialFormula)
|
|
||||||
.filter(
|
def build_row(row_id: str) -> BizProductionMaterialFormula:
|
||||||
BizProductionMaterialFormula.configured_material_id == configured_material_id,
|
return BizProductionMaterialFormula(
|
||||||
BizProductionMaterialFormula.tenant_no == tenant_no,
|
id=row_id,
|
||||||
BizProductionMaterialFormula.del_flag == 0,
|
configured_material_id=configured_material_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id=dept,
|
||||||
|
create_time=now,
|
||||||
)
|
)
|
||||||
.all()
|
|
||||||
|
def apply_row(row: BizProductionMaterialFormula, it: Dict[str, Any], idx: int, _now: datetime) -> None:
|
||||||
|
row.component_material_id = it["component_material_id"]
|
||||||
|
row.component_material_type = it["component_material_type"]
|
||||||
|
row.component_material_code = it["component_material_code"]
|
||||||
|
row.component_material_name = it["component_material_name"]
|
||||||
|
row.standard_qty = it["standard_qty"]
|
||||||
|
row.usage_unit = it["usage_unit"]
|
||||||
|
row.order_num = idx
|
||||||
|
row.remark = it["remark"]
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProductionMaterialFormula,
|
||||||
|
parent_attr="configured_material_id",
|
||||||
|
parent_id=configured_material_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=normalized,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: (it.get("component_material_id") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.component_material_id or "").strip(),
|
||||||
|
duplicate_message="配方明细编号重复",
|
||||||
|
foreign_message="配方明细不属于当前配置试剂",
|
||||||
)
|
)
|
||||||
for row in existing:
|
|
||||||
row.del_flag = 1
|
|
||||||
row.update_by = user_id
|
|
||||||
row.update_time = now
|
|
||||||
db.add(row)
|
|
||||||
|
|
||||||
for row in new_rows:
|
|
||||||
db.add(row)
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return get_formula(db, configured_material_id, tenant_no)
|
return get_formula(db, configured_material_id, tenant_no)
|
||||||
@@ -9,6 +9,7 @@ from sqlalchemy import and_, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid, parse_datetime
|
from app.core.utils import generate_uuid, parse_datetime
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.production_material import _dec, _fmt_dt, _is_enabled_status
|
from app.crud.production_material import _dec, _fmt_dt, _is_enabled_status
|
||||||
from app.crud.production_material_stock_batch import (
|
from app.crud.production_material_stock_batch import (
|
||||||
_create_batch_and_usage,
|
_create_batch_and_usage,
|
||||||
@@ -1007,44 +1008,65 @@ def update_prep(
|
|||||||
lines_raw = data.get("lines")
|
lines_raw = data.get("lines")
|
||||||
if lines_raw is not None:
|
if lines_raw is not None:
|
||||||
parsed_lines = _parse_prep_lines(db, lines_raw, tenant_no=tenant_no)
|
parsed_lines = _parse_prep_lines(db, lines_raw, tenant_no=tenant_no)
|
||||||
existing = (
|
line_items = []
|
||||||
db.query(BizProductionMaterialPrepLine)
|
|
||||||
.filter(
|
|
||||||
BizProductionMaterialPrepLine.prep_id == prep_id,
|
|
||||||
BizProductionMaterialPrepLine.del_flag == 0,
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for ln in existing:
|
|
||||||
ln.del_flag = 1
|
|
||||||
ln.update_by = user_id
|
|
||||||
ln.update_time = now
|
|
||||||
db.add(ln)
|
|
||||||
for pl in parsed_lines:
|
for pl in parsed_lines:
|
||||||
comp = pl["component"]
|
comp = pl["component"]
|
||||||
db.add(
|
line_items.append(
|
||||||
BizProductionMaterialPrepLine(
|
{
|
||||||
id=generate_uuid(),
|
"id": (pl.get("id") or "").strip(),
|
||||||
prep_id=prep_id,
|
"component_material_id": comp.id,
|
||||||
component_material_id=comp.id,
|
"component_material_type": pl["component_type"],
|
||||||
component_material_type=pl["component_type"],
|
"component_material_code": comp.material_code,
|
||||||
component_material_code=comp.material_code,
|
"component_material_name": comp.material_name,
|
||||||
component_material_name=comp.material_name,
|
"requisition_usage_id": pl["requisition"].id,
|
||||||
requisition_usage_id=pl["requisition"].id,
|
"actual_qty": pl["actual_qty"],
|
||||||
actual_qty=pl["actual_qty"],
|
"usage_unit": pl["usage_unit"] or None,
|
||||||
usage_unit=pl["usage_unit"] or None,
|
"order_num": pl["idx"],
|
||||||
tenant_no=tenant_no,
|
"remark": pl["remark"],
|
||||||
order_num=pl["idx"],
|
}
|
||||||
remark=pl["remark"],
|
|
||||||
create_by=user_id,
|
|
||||||
create_dept_id=dept_id or prep.create_dept_id or "",
|
|
||||||
create_time=now,
|
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build_line(row_id):
|
||||||
|
return BizProductionMaterialPrepLine(
|
||||||
|
id=row_id,
|
||||||
|
prep_id=prep_id,
|
||||||
|
component_material_id="",
|
||||||
|
component_material_type="",
|
||||||
|
actual_qty=0,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id=dept_id or prep.create_dept_id or "",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_line(row, item, _idx, _now):
|
||||||
|
row.component_material_id = item["component_material_id"]
|
||||||
|
row.component_material_type = item["component_material_type"]
|
||||||
|
row.component_material_code = item["component_material_code"]
|
||||||
|
row.component_material_name = item["component_material_name"]
|
||||||
|
row.requisition_usage_id = item["requisition_usage_id"]
|
||||||
|
row.actual_qty = item["actual_qty"]
|
||||||
|
row.usage_unit = item["usage_unit"]
|
||||||
|
row.order_num = item["order_num"]
|
||||||
|
row.remark = item["remark"]
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProductionMaterialPrepLine,
|
||||||
|
parent_attr="prep_id",
|
||||||
|
parent_id=prep_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=line_items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_line,
|
||||||
|
apply_fields=apply_line,
|
||||||
|
natural_key=lambda item: f"{item['component_material_id']}:{item['requisition_usage_id'] or ''}",
|
||||||
|
row_natural_key=lambda row: f"{row.component_material_id}:{row.requisition_usage_id or ''}",
|
||||||
|
duplicate_message="配制明细重复",
|
||||||
|
foreign_message="配制明细不属于当前配制单",
|
||||||
|
)
|
||||||
|
|
||||||
prep.update_by = user_id
|
prep.update_by = user_id
|
||||||
prep.update_time = now
|
prep.update_time = now
|
||||||
db.add(prep)
|
db.add(prep)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from sqlalchemy import func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.production_material_stock_batch import _dec, _fmt_date, _fmt_dt, _parse_date, _require_str
|
from app.crud.production_material_stock_batch import _dec, _fmt_date, _fmt_dt, _parse_date, _require_str
|
||||||
from app.models.models import SysUser
|
from app.models.models import SysUser
|
||||||
from app.models.production_material import (
|
from app.models.production_material import (
|
||||||
@@ -449,6 +450,47 @@ def _resolve_unused_item_fields(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _unused_line_key(usage_id: Optional[str], batch_id: Optional[str]) -> str:
|
||||||
|
usage = (usage_id or "").strip()
|
||||||
|
if usage:
|
||||||
|
return f"usage:{usage}"
|
||||||
|
batch = (batch_id or "").strip()
|
||||||
|
if batch:
|
||||||
|
return f"batch:{batch}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_unused_item(db: Session, it: Dict[str, Any], tenant_no: str) -> Dict[str, Any]:
|
||||||
|
it = _resolve_unused_item_fields(db, it, tenant_no)
|
||||||
|
name = _require_str(it.get("material_name") or it.get("materialName"), "物料名称")
|
||||||
|
qty = it.get("unused_qty") if it.get("unused_qty") is not None else it.get("unusedQty")
|
||||||
|
if qty is None or Decimal(str(qty)) <= 0:
|
||||||
|
raise ValueError("未用完数量必须大于0")
|
||||||
|
unit_price = it.get("unit_price") if it.get("unit_price") is not None else it.get("unitPrice")
|
||||||
|
material_type = it.get("material_type") if "material_type" in it else it.get("materialType")
|
||||||
|
return {
|
||||||
|
"id": str(it.get("id") or "").strip(),
|
||||||
|
"usage_id": (it.get("usage_id") or it.get("usageId") or "").strip() or None,
|
||||||
|
"batch_id": (it.get("batch_id") or it.get("batchId") or "").strip() or None,
|
||||||
|
"material_type": (material_type or "").strip() or None if material_type is not None else None,
|
||||||
|
"has_material_type": "material_type" in it or "materialType" in it,
|
||||||
|
"material_code": (it.get("material_code") or it.get("materialCode") or "").strip() or None,
|
||||||
|
"material_name": name,
|
||||||
|
"spec_model": (it.get("spec_model") or it.get("specModel") or "").strip() or None,
|
||||||
|
"unit": (it.get("unit") or "").strip() or None,
|
||||||
|
"stock_in_no": (it.get("stock_in_no") or it.get("stockInNo") or "").strip() or None,
|
||||||
|
"stock_in_date": _parse_optional_date(it.get("stock_in_date") or it.get("stockInDate")),
|
||||||
|
"expire_date": _parse_optional_date(it.get("expire_date") or it.get("expireDate")),
|
||||||
|
"catalog_no": (it.get("catalog_no") or it.get("catalogNo") or "").strip() or None,
|
||||||
|
"batch_no": (it.get("batch_no") or it.get("batchNo") or "").strip() or None,
|
||||||
|
"manufacturer": (it.get("manufacturer") or "").strip() or None,
|
||||||
|
"unit_price": Decimal(str(unit_price)) if unit_price is not None else None,
|
||||||
|
"holder": (it.get("holder") or "").strip() or None,
|
||||||
|
"unused_qty": Decimal(str(qty)),
|
||||||
|
"remark": (it.get("remark") or "").strip() or None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def save_unused_lines(
|
def save_unused_lines(
|
||||||
db: Session,
|
db: Session,
|
||||||
check_id: str,
|
check_id: str,
|
||||||
@@ -465,73 +507,63 @@ def save_unused_lines(
|
|||||||
raise ValueError("仅进行中的盘点单可登记未用完物料")
|
raise ValueError("仅进行中的盘点单可登记未用完物料")
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
old_rows = (
|
prepared = [_prepare_unused_item(db, it, tenant_no) for it in (items or [])]
|
||||||
db.query(BizProductionMaterialStockCheckUnusedLine)
|
|
||||||
.filter(
|
|
||||||
BizProductionMaterialStockCheckUnusedLine.check_id == check_id,
|
|
||||||
BizProductionMaterialStockCheckUnusedLine.del_flag == 0,
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for r in old_rows:
|
|
||||||
r.del_flag = 1
|
|
||||||
r.update_by = user_id
|
|
||||||
r.update_time = now
|
|
||||||
db.add(r)
|
|
||||||
|
|
||||||
count = 0
|
def build_row(row_id: str):
|
||||||
for it in items or []:
|
return BizProductionMaterialStockCheckUnusedLine(
|
||||||
it = _resolve_unused_item_fields(db, it, tenant_no)
|
id=row_id,
|
||||||
name = _require_str(it.get("material_name") or it.get("materialName"), "物料名称")
|
|
||||||
qty = it.get("unused_qty") if it.get("unused_qty") is not None else it.get("unusedQty")
|
|
||||||
if qty is None or Decimal(str(qty)) <= 0:
|
|
||||||
raise ValueError("未用完数量必须大于0")
|
|
||||||
qty_dec = Decimal(str(qty))
|
|
||||||
stock_in_date = _parse_optional_date(it.get("stock_in_date") or it.get("stockInDate"))
|
|
||||||
expire_date = _parse_optional_date(it.get("expire_date") or it.get("expireDate"))
|
|
||||||
usage_id = (it.get("usage_id") or it.get("usageId") or "").strip() or None
|
|
||||||
batch_id = (it.get("batch_id") or it.get("batchId") or "").strip() or None
|
|
||||||
|
|
||||||
row = BizProductionMaterialStockCheckUnusedLine(
|
|
||||||
id=generate_uuid(),
|
|
||||||
check_id=check_id,
|
check_id=check_id,
|
||||||
usage_id=usage_id,
|
material_name="",
|
||||||
batch_id=batch_id,
|
unused_qty=0,
|
||||||
material_type=(it.get("material_type") or it.get("materialType") or "").strip() or None,
|
|
||||||
material_code=(it.get("material_code") or it.get("materialCode") or "").strip() or None,
|
|
||||||
material_name=name,
|
|
||||||
spec_model=(it.get("spec_model") or it.get("specModel") or "").strip() or None,
|
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
|
||||||
stock_in_no=(it.get("stock_in_no") or it.get("stockInNo") or "").strip() or None,
|
|
||||||
stock_in_date=stock_in_date,
|
|
||||||
expire_date=expire_date,
|
|
||||||
catalog_no=(it.get("catalog_no") or it.get("catalogNo") or "").strip() or None,
|
|
||||||
batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
|
|
||||||
manufacturer=(it.get("manufacturer") or "").strip() or None,
|
|
||||||
unit_price=(
|
|
||||||
Decimal(str(it.get("unit_price") or it.get("unitPrice")))
|
|
||||||
if (it.get("unit_price") is not None or it.get("unitPrice") is not None)
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
holder=(it.get("holder") or "").strip() or None,
|
|
||||||
unused_qty=qty_dec,
|
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id=dept_id or "",
|
create_dept_id=dept_id or "",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(row)
|
|
||||||
count += 1
|
def apply_row(row, it, idx, _now):
|
||||||
|
row.usage_id = it["usage_id"]
|
||||||
|
row.batch_id = it["batch_id"]
|
||||||
|
if it["has_material_type"]:
|
||||||
|
row.material_type = it["material_type"]
|
||||||
|
row.material_code = it["material_code"]
|
||||||
|
row.material_name = it["material_name"]
|
||||||
|
row.spec_model = it["spec_model"]
|
||||||
|
row.unit = it["unit"]
|
||||||
|
row.stock_in_no = it["stock_in_no"]
|
||||||
|
row.stock_in_date = it["stock_in_date"]
|
||||||
|
row.expire_date = it["expire_date"]
|
||||||
|
row.catalog_no = it["catalog_no"]
|
||||||
|
row.batch_no = it["batch_no"]
|
||||||
|
row.manufacturer = it["manufacturer"]
|
||||||
|
row.unit_price = it["unit_price"]
|
||||||
|
row.holder = it["holder"]
|
||||||
|
row.unused_qty = it["unused_qty"]
|
||||||
|
row.remark = it["remark"]
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
saved = sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProductionMaterialStockCheckUnusedLine,
|
||||||
|
parent_attr="check_id",
|
||||||
|
parent_id=check_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=prepared,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: _unused_line_key(it.get("usage_id"), it.get("batch_id")),
|
||||||
|
row_natural_key=lambda row: _unused_line_key(row.usage_id, row.batch_id),
|
||||||
|
duplicate_message="未用完明细重复",
|
||||||
|
foreign_message="未用完明细不属于当前盘点单",
|
||||||
|
)
|
||||||
|
|
||||||
check.update_by = user_id
|
check.update_by = user_id
|
||||||
check.update_time = now
|
check.update_time = now
|
||||||
db.add(check)
|
db.add(check)
|
||||||
db.commit()
|
db.commit()
|
||||||
return count
|
return len(saved)
|
||||||
|
|
||||||
|
|
||||||
def complete_check(
|
def complete_check(
|
||||||
|
|||||||
+337
-119
@@ -9,7 +9,9 @@ from sqlalchemy import and_, func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import format_date, generate_uuid, parse_datetime
|
from app.core.utils import format_date, generate_uuid, parse_datetime
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
|
from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
|
||||||
|
from app.crud.user_display import user_display_name_map
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizCellProduct,
|
BizCellProduct,
|
||||||
BizCollectionProtocol,
|
BizCollectionProtocol,
|
||||||
@@ -727,12 +729,7 @@ def _enrich_assignee_names(db: Session, items: List[Dict[str, Any]]) -> None:
|
|||||||
user_ids = list({(x.get("currentAssigneeUserId") or "").strip() for x in items if (x.get("currentAssigneeUserId") or "").strip()})
|
user_ids = list({(x.get("currentAssigneeUserId") or "").strip() for x in items if (x.get("currentAssigneeUserId") or "").strip()})
|
||||||
if not user_ids:
|
if not user_ids:
|
||||||
return
|
return
|
||||||
rows = (
|
name_map = user_display_name_map(db, user_ids)
|
||||||
db.query(SysUser.id, SysUser.nick_name)
|
|
||||||
.filter(SysUser.id.in_(user_ids), SysUser.del_flag == 0)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
name_map = {r.id: (r.nick_name or "").strip() for r in rows}
|
|
||||||
for item in items:
|
for item in items:
|
||||||
uid = (item.get("currentAssigneeUserId") or "").strip()
|
uid = (item.get("currentAssigneeUserId") or "").strip()
|
||||||
if uid:
|
if uid:
|
||||||
@@ -2306,10 +2303,17 @@ def replace_step_reagents(
|
|||||||
task_no = task_row.task_no or ""
|
task_no = task_row.task_no or ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
|
def build_row(row_id: str) -> BizProductionTaskStepReagent:
|
||||||
|
return BizProductionTaskStepReagent(
|
||||||
|
id=row_id,
|
||||||
|
step_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
usage_items: List[Dict[str, Any]] = []
|
def apply_row(row: BizProductionTaskStepReagent, it: Dict[str, Any], idx: int, _now: datetime) -> None:
|
||||||
for idx, it in enumerate(items):
|
|
||||||
req_id = (
|
req_id = (
|
||||||
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||||||
)
|
)
|
||||||
@@ -2321,20 +2325,39 @@ def replace_step_reagents(
|
|||||||
it.get("actual_usage"),
|
it.get("actual_usage"),
|
||||||
req_id,
|
req_id,
|
||||||
)
|
)
|
||||||
db.add(BizProductionTaskStepReagent(
|
row.reagent_code = (it.get("reagent_code") or "").strip() or None
|
||||||
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
|
row.reagent_name = (it.get("reagent_name") or "").strip() or None
|
||||||
reagent_code=(it.get("reagent_code") or "").strip() or None,
|
row.spec = (it.get("spec") or "").strip() or None
|
||||||
reagent_name=(it.get("reagent_name") or "").strip() or None,
|
row.standard_usage = (it.get("standard_usage") or "").strip() or None
|
||||||
spec=(it.get("spec") or "").strip() or None,
|
row.actual_usage = (it.get("actual_usage") or "").strip() or None
|
||||||
standard_usage=(it.get("standard_usage") or "").strip() or None,
|
row.unit = (it.get("unit") or "").strip() or None
|
||||||
actual_usage=(it.get("actual_usage") or "").strip() or None,
|
row.requisition_usage_id = req_id or None
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
row.is_key_material = it.get("is_key_material", "0")
|
||||||
requisition_usage_id=req_id or None,
|
row.remark = (it.get("remark") or "").strip() or None
|
||||||
is_key_material=it.get("is_key_material", "0"),
|
row.order_num = idx
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
order_num=idx, create_by=user_id, create_dept_id="",
|
sync_child_rows(
|
||||||
create_time=now, update_by=user_id, update_time=now, del_flag=0,
|
db,
|
||||||
))
|
BizProductionTaskStepReagent,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: (it.get("reagent_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.reagent_code or "").strip(),
|
||||||
|
duplicate_message="试剂编号重复",
|
||||||
|
foreign_message="试剂不属于当前步骤",
|
||||||
|
)
|
||||||
|
|
||||||
|
usage_items: List[Dict[str, Any]] = []
|
||||||
|
for it in items:
|
||||||
|
req_id = (
|
||||||
|
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||||||
|
)
|
||||||
if req_id:
|
if req_id:
|
||||||
usage_items.append({
|
usage_items.append({
|
||||||
"requisitionUsageId": req_id,
|
"requisitionUsageId": req_id,
|
||||||
@@ -2376,10 +2399,17 @@ def replace_step_consumables(
|
|||||||
task_no = task_row.task_no or ""
|
task_no = task_row.task_no or ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
|
def build_row(row_id: str) -> BizProductionTaskStepConsumable:
|
||||||
|
return BizProductionTaskStepConsumable(
|
||||||
|
id=row_id,
|
||||||
|
step_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
usage_items: List[Dict[str, Any]] = []
|
def apply_row(row: BizProductionTaskStepConsumable, it: Dict[str, Any], idx: int, _now: datetime) -> None:
|
||||||
for idx, it in enumerate(items):
|
|
||||||
req_id = (
|
req_id = (
|
||||||
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||||||
)
|
)
|
||||||
@@ -2391,20 +2421,39 @@ def replace_step_consumables(
|
|||||||
it.get("actual_usage"),
|
it.get("actual_usage"),
|
||||||
req_id,
|
req_id,
|
||||||
)
|
)
|
||||||
db.add(BizProductionTaskStepConsumable(
|
row.consumable_code = (it.get("consumable_code") or "").strip() or None
|
||||||
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
|
row.consumable_name = (it.get("consumable_name") or "").strip() or None
|
||||||
consumable_code=(it.get("consumable_code") or "").strip() or None,
|
row.spec_model = (it.get("spec_model") or "").strip() or None
|
||||||
consumable_name=(it.get("consumable_name") or "").strip() or None,
|
row.standard_usage = (it.get("standard_usage") or "").strip() or None
|
||||||
spec_model=(it.get("spec_model") or "").strip() or None,
|
row.actual_usage = (it.get("actual_usage") or "").strip() or None
|
||||||
standard_usage=(it.get("standard_usage") or "").strip() or None,
|
row.unit = (it.get("unit") or "").strip() or None
|
||||||
actual_usage=(it.get("actual_usage") or "").strip() or None,
|
row.requisition_usage_id = req_id or None
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
row.is_required = it.get("is_required", "1")
|
||||||
requisition_usage_id=req_id or None,
|
row.remark = (it.get("remark") or "").strip() or None
|
||||||
is_required=it.get("is_required", "1"),
|
row.order_num = idx
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
order_num=idx, create_by=user_id, create_dept_id="",
|
sync_child_rows(
|
||||||
create_time=now, update_by=user_id, update_time=now, del_flag=0,
|
db,
|
||||||
))
|
BizProductionTaskStepConsumable,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: (it.get("consumable_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.consumable_code or "").strip(),
|
||||||
|
duplicate_message="耗材编号重复",
|
||||||
|
foreign_message="耗材不属于当前步骤",
|
||||||
|
)
|
||||||
|
|
||||||
|
usage_items: List[Dict[str, Any]] = []
|
||||||
|
for it in items:
|
||||||
|
req_id = (
|
||||||
|
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||||||
|
)
|
||||||
if req_id:
|
if req_id:
|
||||||
usage_items.append({
|
usage_items.append({
|
||||||
"requisitionUsageId": req_id,
|
"requisitionUsageId": req_id,
|
||||||
@@ -2445,10 +2494,22 @@ def replace_step_configured_reagents(
|
|||||||
task_no = task_row.task_no or ""
|
task_no = task_row.task_no or ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_soft_delete_sub(db, BizProductionTaskStepConfiguredReagent, "step_id", step_id, now)
|
def build_row(row_id: str) -> BizProductionTaskStepConfiguredReagent:
|
||||||
|
return BizProductionTaskStepConfiguredReagent(
|
||||||
|
id=row_id,
|
||||||
|
step_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
usage_items: List[Dict[str, Any]] = []
|
def apply_row(
|
||||||
for idx, it in enumerate(items):
|
row: BizProductionTaskStepConfiguredReagent,
|
||||||
|
it: Dict[str, Any],
|
||||||
|
idx: int,
|
||||||
|
_now: datetime,
|
||||||
|
) -> None:
|
||||||
req_id = (
|
req_id = (
|
||||||
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||||||
)
|
)
|
||||||
@@ -2460,19 +2521,38 @@ def replace_step_configured_reagents(
|
|||||||
it.get("actual_usage"),
|
it.get("actual_usage"),
|
||||||
req_id,
|
req_id,
|
||||||
)
|
)
|
||||||
db.add(BizProductionTaskStepConfiguredReagent(
|
row.configured_reagent_code = (it.get("configured_reagent_code") or "").strip() or None
|
||||||
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
|
row.configured_reagent_name = (it.get("configured_reagent_name") or "").strip() or None
|
||||||
configured_reagent_code=(it.get("configured_reagent_code") or "").strip() or None,
|
row.spec = (it.get("spec") or "").strip() or None
|
||||||
configured_reagent_name=(it.get("configured_reagent_name") or "").strip() or None,
|
row.standard_usage = (it.get("standard_usage") or "").strip() or None
|
||||||
spec=(it.get("spec") or "").strip() or None,
|
row.actual_usage = (it.get("actual_usage") or "").strip() or None
|
||||||
standard_usage=(it.get("standard_usage") or "").strip() or None,
|
row.unit = (it.get("unit") or "").strip() or None
|
||||||
actual_usage=(it.get("actual_usage") or "").strip() or None,
|
row.requisition_usage_id = req_id or None
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
row.remark = (it.get("remark") or "").strip() or None
|
||||||
requisition_usage_id=req_id or None,
|
row.order_num = idx
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
order_num=idx, create_by=user_id, create_dept_id="",
|
sync_child_rows(
|
||||||
create_time=now, update_by=user_id, update_time=now, del_flag=0,
|
db,
|
||||||
))
|
BizProductionTaskStepConfiguredReagent,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: (it.get("configured_reagent_code") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.configured_reagent_code or "").strip(),
|
||||||
|
duplicate_message="配置试剂编号重复",
|
||||||
|
foreign_message="配置试剂不属于当前步骤",
|
||||||
|
)
|
||||||
|
|
||||||
|
usage_items: List[Dict[str, Any]] = []
|
||||||
|
for it in items:
|
||||||
|
req_id = (
|
||||||
|
(it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
|
||||||
|
)
|
||||||
if req_id:
|
if req_id:
|
||||||
usage_items.append({
|
usage_items.append({
|
||||||
"requisitionUsageId": req_id,
|
"requisitionUsageId": req_id,
|
||||||
@@ -2492,23 +2572,62 @@ def replace_step_configured_reagents(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_step_output_fields(
|
||||||
|
row: BizProductionTaskStepOutput,
|
||||||
|
item: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
order_num: int,
|
||||||
|
user_id: Optional[str],
|
||||||
|
now: datetime,
|
||||||
|
) -> None:
|
||||||
|
row.output_type_id = (item.get("output_type_id") or "").strip() or None
|
||||||
|
row.output_name = (item.get("output_name") or "").strip() or None
|
||||||
|
row.output_barcode = (item.get("output_barcode") or "").strip() or None
|
||||||
|
row.remark = (item.get("remark") or "").strip() or None
|
||||||
|
row.order_num = order_num
|
||||||
|
row.update_by = user_id
|
||||||
|
row.update_time = now
|
||||||
|
row.del_flag = 0
|
||||||
|
|
||||||
|
|
||||||
def replace_step_outputs(
|
def replace_step_outputs(
|
||||||
db: Session, step_id: str, tenant_no: str,
|
db: Session, step_id: str, tenant_no: str,
|
||||||
user_id: Optional[str], items: List[Dict[str, Any]],
|
user_id: Optional[str], items: List[Dict[str, Any]],
|
||||||
) -> None:
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""按产出物 ID 更新原行。带 ID 的行原地修改,没有 ID 的才新增,提交列表里没有的才软删除。
|
||||||
|
|
||||||
|
质检工单用 production_step_output_id 指向这里。整表软删再插入会换掉 ID,关联就断了。
|
||||||
|
"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
|
|
||||||
for idx, it in enumerate(items):
|
def build_row(row_id: str) -> BizProductionTaskStepOutput:
|
||||||
db.add(BizProductionTaskStepOutput(
|
return BizProductionTaskStepOutput(
|
||||||
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
|
id=row_id,
|
||||||
output_type_id=(it.get("output_type_id") or "").strip() or None,
|
step_id=step_id,
|
||||||
output_name=(it.get("output_name") or "").strip() or None,
|
tenant_no=tenant_no,
|
||||||
output_barcode=(it.get("output_barcode") or "").strip() or None,
|
create_by=user_id,
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
create_dept_id="",
|
||||||
order_num=idx, create_by=user_id, create_dept_id="",
|
create_time=now,
|
||||||
create_time=now, update_by=user_id, update_time=now, del_flag=0,
|
)
|
||||||
))
|
|
||||||
|
result_rows = sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProductionTaskStepOutput,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=lambda row, item, idx, now_: _apply_step_output_fields(
|
||||||
|
row, item, order_num=idx, user_id=user_id, now=now_
|
||||||
|
),
|
||||||
|
duplicate_message="产出物编号重复",
|
||||||
|
foreign_message="产出物不属于当前步骤",
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return [_step_output_to_dict(row) for row in result_rows]
|
||||||
|
|
||||||
|
|
||||||
def replace_step_data_collections(
|
def replace_step_data_collections(
|
||||||
@@ -2516,17 +2635,45 @@ def replace_step_data_collections(
|
|||||||
user_id: Optional[str], items: List[Dict[str, Any]],
|
user_id: Optional[str], items: List[Dict[str, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
|
|
||||||
for idx, it in enumerate(items):
|
def build_row(row_id: str) -> BizProductionTaskStepDataCollection:
|
||||||
db.add(BizProductionTaskStepDataCollection(
|
return BizProductionTaskStepDataCollection(
|
||||||
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
|
id=row_id,
|
||||||
collection_name=(it.get("collection_name") or "").strip() or None,
|
step_id=step_id,
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
tenant_no=tenant_no,
|
||||||
collection_value=(it.get("collection_value") or "").strip() or None,
|
create_by=user_id,
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
create_dept_id="",
|
||||||
order_num=idx, create_by=user_id, create_dept_id="",
|
create_time=now,
|
||||||
create_time=now, update_by=user_id, update_time=now, del_flag=0,
|
)
|
||||||
))
|
|
||||||
|
def apply_row(
|
||||||
|
row: BizProductionTaskStepDataCollection,
|
||||||
|
it: Dict[str, Any],
|
||||||
|
idx: int,
|
||||||
|
_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
row.collection_name = (it.get("collection_name") or "").strip() or None
|
||||||
|
row.unit = (it.get("unit") or "").strip() or None
|
||||||
|
row.collection_value = (it.get("collection_value") or "").strip() or None
|
||||||
|
row.remark = (it.get("remark") or "").strip() or None
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProductionTaskStepDataCollection,
|
||||||
|
parent_attr="step_id",
|
||||||
|
parent_id=step_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: (it.get("collection_name") or "").strip(),
|
||||||
|
row_natural_key=lambda row: (row.collection_name or "").strip(),
|
||||||
|
duplicate_message="数据采集编号重复",
|
||||||
|
foreign_message="数据采集不属于当前步骤",
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -2604,58 +2751,129 @@ def spawn_temporary_work_order_from_task(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _task_input_identity(input_type: Optional[str], input_id: Optional[str], input_code: Optional[str]) -> str:
|
||||||
|
kind = (input_type or "").strip()
|
||||||
|
ident = (input_id or "").strip() or (input_code or "").strip()
|
||||||
|
if not kind or not ident:
|
||||||
|
return ""
|
||||||
|
return f"{kind}:{ident}"
|
||||||
|
|
||||||
|
|
||||||
|
def _task_input_item_identity(it: Dict[str, Any]) -> str:
|
||||||
|
return _task_input_identity(
|
||||||
|
it.get("input_type") or it.get("inputType"),
|
||||||
|
it.get("input_id") or it.get("inputId"),
|
||||||
|
it.get("input_code") or it.get("inputCode"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_cell_product_status(
|
||||||
|
db: Session,
|
||||||
|
product_id: Optional[str],
|
||||||
|
*,
|
||||||
|
user_id: Optional[str],
|
||||||
|
now: datetime,
|
||||||
|
to_status: str,
|
||||||
|
from_statuses: tuple,
|
||||||
|
) -> None:
|
||||||
|
pid = (product_id or "").strip()
|
||||||
|
if not pid:
|
||||||
|
return
|
||||||
|
product = db.query(BizCellProduct).filter(
|
||||||
|
BizCellProduct.id == pid, BizCellProduct.del_flag == 0
|
||||||
|
).first()
|
||||||
|
if product and product.product_status in from_statuses:
|
||||||
|
product.product_status = to_status
|
||||||
|
product.update_by = user_id
|
||||||
|
product.update_time = now
|
||||||
|
db.add(product)
|
||||||
|
|
||||||
|
|
||||||
def set_task_inputs(
|
def set_task_inputs(
|
||||||
db: Session, task_id: str, tenant_no: str,
|
db: Session, task_id: str, tenant_no: str,
|
||||||
user_id: Optional[str], items: List[Dict[str, Any]],
|
user_id: Optional[str], items: List[Dict[str, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
# --- 1. 加载旧 inputs 并按 cell_product 恢复状态 ---
|
|
||||||
old_rows = db.query(BizProductionTaskInput).filter(
|
old_rows = db.query(BizProductionTaskInput).filter(
|
||||||
and_(
|
and_(
|
||||||
BizProductionTaskInput.task_id == task_id,
|
BizProductionTaskInput.task_id == task_id,
|
||||||
|
BizProductionTaskInput.tenant_no == tenant_no,
|
||||||
BizProductionTaskInput.del_flag == 0,
|
BizProductionTaskInput.del_flag == 0,
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
old_identity = {
|
||||||
|
row.id: (row.input_type or "", row.input_id or "")
|
||||||
|
for row in old_rows
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_row(row_id: str) -> BizProductionTaskInput:
|
||||||
|
return BizProductionTaskInput(
|
||||||
|
id=row_id,
|
||||||
|
task_id=task_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_row(row: BizProductionTaskInput, it: Dict[str, Any], idx: int, _now: datetime) -> None:
|
||||||
|
row.input_type = (it.get("input_type") or it.get("inputType") or "").strip() or None
|
||||||
|
row.input_id = (it.get("input_id") or it.get("inputId") or "").strip() or None
|
||||||
|
row.input_code = (it.get("input_code") or it.get("inputCode") or "").strip() or None
|
||||||
|
row.input_name = (it.get("input_name") or it.get("inputName") or "").strip() or None
|
||||||
|
row.remark = (it.get("remark") or "").strip() or None
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
saved = sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizProductionTaskInput,
|
||||||
|
parent_attr="task_id",
|
||||||
|
parent_id=task_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=items,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=_task_input_item_identity,
|
||||||
|
row_natural_key=lambda row: _task_input_identity(row.input_type, row.input_id, row.input_code),
|
||||||
|
duplicate_message="原材料编号重复",
|
||||||
|
foreign_message="原材料不属于当前任务",
|
||||||
|
)
|
||||||
|
saved_ids = {row.id for row in saved}
|
||||||
for old_row in old_rows:
|
for old_row in old_rows:
|
||||||
# 如果是细胞产品,恢复状态
|
if old_row.id in saved_ids:
|
||||||
if old_row.input_type == "cell_product" and old_row.input_id:
|
continue
|
||||||
product = db.query(BizCellProduct).filter(
|
if old_row.input_type == "cell_product":
|
||||||
BizCellProduct.id == old_row.input_id, BizCellProduct.del_flag == 0
|
_set_cell_product_status(
|
||||||
).first()
|
db,
|
||||||
if product and product.product_status == "in_production":
|
old_row.input_id,
|
||||||
product.product_status = "frozen"
|
user_id=user_id,
|
||||||
product.update_by = user_id
|
now=now,
|
||||||
product.update_time = now
|
to_status="frozen",
|
||||||
db.add(product)
|
from_statuses=("in_production",),
|
||||||
|
)
|
||||||
# --- 2. 全量软删除旧行 ---
|
for row in saved:
|
||||||
_soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
|
previous = old_identity.get(row.id)
|
||||||
|
current = (row.input_type or "", row.input_id or "")
|
||||||
# --- 3. 按传入顺序批量新建 ---
|
if previous != current and previous and previous[0] == "cell_product":
|
||||||
for idx, it in enumerate(items):
|
_set_cell_product_status(
|
||||||
inp_type = (it.get("input_type") or it.get("inputType") or "").strip()
|
db,
|
||||||
inp_id = (it.get("input_id") or it.get("inputId") or "").strip()
|
previous[1],
|
||||||
# 如果是细胞产品,设置状态为 in_production
|
user_id=user_id,
|
||||||
if inp_type == "cell_product" and inp_id:
|
now=now,
|
||||||
product = db.query(BizCellProduct).filter(
|
to_status="frozen",
|
||||||
BizCellProduct.id == inp_id, BizCellProduct.del_flag == 0
|
from_statuses=("in_production",),
|
||||||
).first()
|
)
|
||||||
if product and product.product_status in ("frozen", "thawed"):
|
if row.input_type == "cell_product":
|
||||||
product.product_status = "in_production"
|
_set_cell_product_status(
|
||||||
product.update_by = user_id
|
db,
|
||||||
product.update_time = now
|
row.input_id,
|
||||||
db.add(product)
|
user_id=user_id,
|
||||||
db.add(BizProductionTaskInput(
|
now=now,
|
||||||
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
|
to_status="in_production",
|
||||||
input_type=inp_type or None,
|
from_statuses=("frozen", "thawed"),
|
||||||
input_id=inp_id or None,
|
)
|
||||||
input_code=(it.get("input_code") or it.get("inputCode") or "").strip() or None,
|
|
||||||
input_name=(it.get("input_name") or it.get("inputName") or "").strip() or None,
|
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
order_num=idx, create_by=user_id, create_dept_id="",
|
|
||||||
create_time=now, update_by=user_id, update_time=now, del_flag=0,
|
|
||||||
))
|
|
||||||
|
|
||||||
db.flush()
|
db.flush()
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from sqlalchemy import func
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizProcessRoute,
|
BizProcessRoute,
|
||||||
|
BizProcessRouteStep,
|
||||||
BizProductionTask,
|
BizProductionTask,
|
||||||
BizProductionTaskSet,
|
BizProductionTaskSet,
|
||||||
BizProductionTaskSetItem,
|
BizProductionTaskSetItem,
|
||||||
@@ -110,6 +112,39 @@ def collect_missing_route_ids(items: List[Dict[str, Any]], existing_ids: Set[str
|
|||||||
return missing
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
def collect_route_ids_without_steps(
|
||||||
|
items: List[Dict[str, Any]],
|
||||||
|
route_ids_with_steps: Set[str],
|
||||||
|
) -> List[str]:
|
||||||
|
"""返回条目中已绑定路线但无有效工序步骤的 route_id(去重保序)。"""
|
||||||
|
empty: List[str] = []
|
||||||
|
seen: Set[str] = set()
|
||||||
|
with_steps = route_ids_with_steps or set()
|
||||||
|
for item in items or []:
|
||||||
|
rid = _strip_or_none(item.get("route_id"))
|
||||||
|
if not rid or rid in with_steps or rid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(rid)
|
||||||
|
empty.append(rid)
|
||||||
|
return empty
|
||||||
|
|
||||||
|
|
||||||
|
def _load_route_ids_with_steps(db: Session, route_ids: List[str]) -> Set[str]:
|
||||||
|
ids = [rid for rid in route_ids if rid]
|
||||||
|
if not ids:
|
||||||
|
return set()
|
||||||
|
rows = (
|
||||||
|
db.query(BizProcessRouteStep.route_id)
|
||||||
|
.filter(
|
||||||
|
BizProcessRouteStep.route_id.in_(ids),
|
||||||
|
BizProcessRouteStep.del_flag == 0,
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return {r[0] for r in rows if r and r[0]}
|
||||||
|
|
||||||
|
|
||||||
def total_pages(total: int, page_size: int) -> int:
|
def total_pages(total: int, page_size: int) -> int:
|
||||||
if page_size <= 0:
|
if page_size <= 0:
|
||||||
return 0
|
return 0
|
||||||
@@ -222,41 +257,46 @@ def _replace_items(
|
|||||||
create_dept_id: str,
|
create_dept_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
existing = (
|
|
||||||
db.query(BizProductionTaskSetItem)
|
def _set_item_key(item):
|
||||||
.filter(
|
return f"{(item.get('route_id') or '').strip()}|{(item.get('task_name') or '').strip()}"
|
||||||
BizProductionTaskSetItem.set_id == set_id,
|
|
||||||
BizProductionTaskSetItem.tenant_no == tenant_no,
|
def build_item(row_id):
|
||||||
BizProductionTaskSetItem.del_flag == 0,
|
return BizProductionTaskSetItem(
|
||||||
|
id=row_id,
|
||||||
|
set_id=set_id,
|
||||||
|
task_name="",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id=create_dept_id or "",
|
||||||
|
create_time=now,
|
||||||
)
|
)
|
||||||
.all()
|
|
||||||
)
|
def apply_item(row, item, idx, _now):
|
||||||
for row in existing:
|
|
||||||
row.del_flag = 1
|
|
||||||
row.update_by = user_id
|
|
||||||
row.update_time = now
|
|
||||||
db.add(row)
|
|
||||||
for idx, item in enumerate(items, start=1):
|
|
||||||
rid = item.get("route_id")
|
rid = item.get("route_id")
|
||||||
db.add(
|
row.task_name = item["task_name"]
|
||||||
BizProductionTaskSetItem(
|
row.route_id = rid
|
||||||
id=generate_uuid(),
|
row.route_name = route_names.get(rid or "") if rid else None
|
||||||
set_id=set_id,
|
row.description = item.get("description")
|
||||||
task_name=item["task_name"],
|
row.remark = item.get("remark")
|
||||||
route_id=rid,
|
row.order_num = idx + 1
|
||||||
route_name=route_names.get(rid or "") if rid else None,
|
|
||||||
description=item.get("description"),
|
sync_child_rows(
|
||||||
remark=item.get("remark"),
|
db,
|
||||||
tenant_no=tenant_no,
|
BizProductionTaskSetItem,
|
||||||
order_num=idx,
|
parent_attr="set_id",
|
||||||
create_by=user_id,
|
parent_id=set_id,
|
||||||
create_dept_id=create_dept_id or "",
|
tenant_no=tenant_no,
|
||||||
create_time=now,
|
items=items,
|
||||||
update_by=user_id,
|
user_id=user_id,
|
||||||
update_time=now,
|
now=now,
|
||||||
del_flag=0,
|
build_row=build_item,
|
||||||
)
|
apply_fields=apply_item,
|
||||||
)
|
natural_key=_set_item_key,
|
||||||
|
row_natural_key=lambda row: f"{(row.route_id or '').strip()}|{(row.task_name or '').strip()}",
|
||||||
|
duplicate_message="任务集合条目重复",
|
||||||
|
foreign_message="任务集合条目不属于当前集合",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _item_count_map(db: Session, tenant_no: str, set_ids: List[str]) -> Dict[str, int]:
|
def _item_count_map(db: Session, tenant_no: str, set_ids: List[str]) -> Dict[str, int]:
|
||||||
@@ -664,12 +704,16 @@ def create_tasks_from_set(
|
|||||||
}
|
}
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
name_map = _load_route_name_map(
|
route_ids = [p["route_id"] for p in payloads if p.get("route_id")]
|
||||||
db, tenant_no, [p["route_id"] for p in payloads if p.get("route_id")]
|
name_map = _load_route_name_map(db, tenant_no, route_ids)
|
||||||
)
|
|
||||||
missing = collect_missing_route_ids(payloads, set(name_map.keys()))
|
missing = collect_missing_route_ids(payloads, set(name_map.keys()))
|
||||||
if missing:
|
if missing:
|
||||||
raise ValueError("工艺路线已删除,无法派发:" + "、".join(missing))
|
raise ValueError("工艺路线已删除,无法派发:" + "、".join(missing))
|
||||||
|
with_steps = _load_route_ids_with_steps(db, route_ids)
|
||||||
|
empty_steps = collect_route_ids_without_steps(payloads, with_steps)
|
||||||
|
if empty_steps:
|
||||||
|
labels = [name_map.get(rid) or rid for rid in empty_steps]
|
||||||
|
raise ValueError("工艺路线无工序步骤,无法派发:" + "、".join(labels))
|
||||||
lab_storage_code = resolve_work_order_lab_storage_code(
|
lab_storage_code = resolve_work_order_lab_storage_code(
|
||||||
db, tenant_no=tenant_no, wo=wo
|
db, tenant_no=tenant_no, wo=wo
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ def enrich_step_completed_by_names(db: Session, steps: List[Dict]) -> None:
|
|||||||
return
|
return
|
||||||
users = {
|
users = {
|
||||||
u.id: u
|
u.id: u
|
||||||
for u in db.query(SysUser).filter(SysUser.id.in_(uids), SysUser.del_flag == 0).all()
|
for u in db.query(SysUser).filter(SysUser.id.in_(uids)).all()
|
||||||
}
|
}
|
||||||
for step in steps:
|
for step in steps:
|
||||||
uid = (step.get("completedBy") or "").strip()
|
uid = (step.get("completedBy") or "").strip()
|
||||||
|
|||||||
@@ -347,6 +347,8 @@ def _enrich_outbound_items(
|
|||||||
"projectName": product.project_name or "",
|
"projectName": product.project_name or "",
|
||||||
"cellTypeName": product.cell_type_name or "",
|
"cellTypeName": product.cell_type_name or "",
|
||||||
"specText": product.spec_text or "",
|
"specText": product.spec_text or "",
|
||||||
|
"packageId": getattr(product, "package_id", None) or "",
|
||||||
|
"packageText": getattr(product, "package_text", None) or "",
|
||||||
"agreementPrice": _dec_price(product.agreement_price),
|
"agreementPrice": _dec_price(product.agreement_price),
|
||||||
"actualPrice": _dec_price(product.actual_price),
|
"actualPrice": _dec_price(product.actual_price),
|
||||||
"quantity": product.quantity,
|
"quantity": product.quantity,
|
||||||
@@ -435,6 +437,8 @@ def _enrich_outbound_items(
|
|||||||
it["totalCourseCount"] = course.get("totalCourseCount")
|
it["totalCourseCount"] = course.get("totalCourseCount")
|
||||||
it["cellType"] = sched.cell_type or ""
|
it["cellType"] = sched.cell_type or ""
|
||||||
it["specText"] = sched.spec_text or ""
|
it["specText"] = sched.spec_text or ""
|
||||||
|
it["packageId"] = getattr(sched, "package_id", None) or ""
|
||||||
|
it["packageText"] = getattr(sched, "package_text", None) or ""
|
||||||
it["infusionTime"] = _fmt_dt(sched.infusion_time) if sched.infusion_time else ""
|
it["infusionTime"] = _fmt_dt(sched.infusion_time) if sched.infusion_time else ""
|
||||||
it["infusionLocation"] = sched.infusion_location or ""
|
it["infusionLocation"] = sched.infusion_location or ""
|
||||||
it["products"] = products_map.get(it["infusionScheduleId"], [])
|
it["products"] = products_map.get(it["infusionScheduleId"], [])
|
||||||
@@ -583,6 +587,8 @@ def _attach_outbound_detail(
|
|||||||
out["infusionSchedule"] = {
|
out["infusionSchedule"] = {
|
||||||
"cellType": sched.cell_type if sched else "",
|
"cellType": sched.cell_type if sched else "",
|
||||||
"specText": sched.spec_text if sched else "",
|
"specText": sched.spec_text if sched else "",
|
||||||
|
"packageId": getattr(sched, "package_id", None) or "" if sched else "",
|
||||||
|
"packageText": getattr(sched, "package_text", None) or "" if sched else "",
|
||||||
"infusionTime": _fmt_dt(sched.infusion_time) if sched and sched.infusion_time else "",
|
"infusionTime": _fmt_dt(sched.infusion_time) if sched and sched.infusion_time else "",
|
||||||
"infusionLocation": sched.infusion_location if sched else "",
|
"infusionLocation": sched.infusion_location if sched else "",
|
||||||
"infusionMethod": sched.infusion_method if sched else "",
|
"infusionMethod": sched.infusion_method if sched else "",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlalchemy import and_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
from app.models.models import BizCollectionProtocol, BizProtocolSample
|
from app.models.models import BizCollectionProtocol, BizPresignOrder, BizProtocolSample
|
||||||
from app.models.research_donation import (
|
from app.models.research_donation import (
|
||||||
BizProtocolDonationConvert,
|
BizProtocolDonationConvert,
|
||||||
BizResearchDonation,
|
BizResearchDonation,
|
||||||
@@ -193,10 +193,11 @@ def assert_biz_no_available(
|
|||||||
biz_no: str,
|
biz_no: str,
|
||||||
exclude_protocol_id: Optional[str] = None,
|
exclude_protocol_id: Optional[str] = None,
|
||||||
exclude_donation_id: Optional[str] = None,
|
exclude_donation_id: Optional[str] = None,
|
||||||
|
exclude_presign_order_id: Optional[str] = None,
|
||||||
for_donation: bool = False,
|
for_donation: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
校验业务编号在采集协议与科研捐赠间可用。
|
校验业务编号在采集协议、科研捐赠与预签约订单间可用。
|
||||||
若当前单存在转移配对,则豁免配对单据上的编号占用。
|
若当前单存在转移配对,则豁免配对单据上的编号占用。
|
||||||
"""
|
"""
|
||||||
pn = (biz_no or "").strip()
|
pn = (biz_no or "").strip()
|
||||||
@@ -259,6 +260,22 @@ def assert_biz_no_available(
|
|||||||
raise ValueError("捐赠协议号已存在")
|
raise ValueError("捐赠协议号已存在")
|
||||||
raise ValueError("协议编号已被捐赠协议单使用")
|
raise ValueError("协议编号已被捐赠协议单使用")
|
||||||
|
|
||||||
|
presign_rows = (
|
||||||
|
db.query(BizPresignOrder)
|
||||||
|
.filter(
|
||||||
|
and_(
|
||||||
|
BizPresignOrder.protocol_no == pn,
|
||||||
|
BizPresignOrder.tenant_no == tenant_no,
|
||||||
|
BizPresignOrder.del_flag == 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for row in presign_rows:
|
||||||
|
if exclude_presign_order_id and row.id == exclude_presign_order_id:
|
||||||
|
continue
|
||||||
|
raise ValueError("协议编号已存在")
|
||||||
|
|
||||||
|
|
||||||
def upsert_convert_link_for_to_storage(
|
def upsert_convert_link_for_to_storage(
|
||||||
db: Session,
|
db: Session,
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""采集协议单 — 协议级退费登记 CRUD(不改转账笔 pay_status)"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid, parse_datetime
|
||||||
|
from app.crud.collection_protocol import urls_to_attachment_list
|
||||||
|
from app.models.models import BizProtocolRefundRecord
|
||||||
|
from app.services.protocol_refund_record import validate_refund_amount
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> datetime:
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
def _record_to_dict(
|
||||||
|
db: Session, r: BizProtocolRefundRecord, tenant_no: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": r.id,
|
||||||
|
"collectionProtocolId": r.collection_protocol_id,
|
||||||
|
"refundAmount": float(r.refund_amount) if r.refund_amount is not None else None,
|
||||||
|
"refundTime": r.refund_time.strftime("%Y-%m-%d %H:%M:%S") if r.refund_time else "",
|
||||||
|
"refundRemark": r.refund_remark or "",
|
||||||
|
"refundAttachments": r.refund_attachments or "",
|
||||||
|
"refundAttachmentFiles": urls_to_attachment_list(
|
||||||
|
r.refund_attachments, db, tenant_no
|
||||||
|
),
|
||||||
|
"workflowInstanceId": r.workflow_instance_id or "",
|
||||||
|
"remark": r.remark or "",
|
||||||
|
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
|
||||||
|
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_refund_records(
|
||||||
|
db: Session, *, collection_protocol_id: str, tenant_no: str
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
rows = (
|
||||||
|
db.query(BizProtocolRefundRecord)
|
||||||
|
.filter(
|
||||||
|
BizProtocolRefundRecord.collection_protocol_id == collection_protocol_id,
|
||||||
|
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||||
|
BizProtocolRefundRecord.del_flag == 0,
|
||||||
|
)
|
||||||
|
.order_by(BizProtocolRefundRecord.create_time.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [_record_to_dict(db, r, tenant_no) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def create_refund_record(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
collection_protocol_id: str,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: Optional[str],
|
||||||
|
refund_amount: Any,
|
||||||
|
refund_time: Any = None,
|
||||||
|
refund_remark: Optional[str] = None,
|
||||||
|
refund_attachments: Optional[str] = None,
|
||||||
|
workflow_instance_id: Optional[str] = None,
|
||||||
|
remark: Optional[str] = None,
|
||||||
|
) -> BizProtocolRefundRecord:
|
||||||
|
amt = validate_refund_amount(refund_amount)
|
||||||
|
now = _now()
|
||||||
|
rt = None
|
||||||
|
if refund_time not in (None, ""):
|
||||||
|
rt = (
|
||||||
|
refund_time
|
||||||
|
if isinstance(refund_time, datetime)
|
||||||
|
else parse_datetime(str(refund_time))
|
||||||
|
)
|
||||||
|
obj = BizProtocolRefundRecord(
|
||||||
|
id=generate_uuid(),
|
||||||
|
collection_protocol_id=collection_protocol_id,
|
||||||
|
refund_amount=amt,
|
||||||
|
refund_time=rt or now,
|
||||||
|
refund_remark=(refund_remark or "").strip() or None,
|
||||||
|
refund_attachments=(refund_attachments or "").strip() or None,
|
||||||
|
workflow_instance_id=(workflow_instance_id or "").strip() or None,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
order_num=0,
|
||||||
|
remark=(remark or "").strip() or None,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def update_refund_record(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
record_id: str,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: Optional[str],
|
||||||
|
refund_amount: Any = None,
|
||||||
|
refund_time: Any = None,
|
||||||
|
refund_remark: Optional[str] = None,
|
||||||
|
refund_attachments: Optional[str] = None,
|
||||||
|
workflow_instance_id: Optional[str] = None,
|
||||||
|
remark: Optional[str] = None,
|
||||||
|
fields_set: Optional[set] = None,
|
||||||
|
) -> BizProtocolRefundRecord:
|
||||||
|
obj = (
|
||||||
|
db.query(BizProtocolRefundRecord)
|
||||||
|
.filter(
|
||||||
|
BizProtocolRefundRecord.id == record_id,
|
||||||
|
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||||
|
BizProtocolRefundRecord.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not obj:
|
||||||
|
raise ValueError("退费登记不存在")
|
||||||
|
fs = fields_set or {
|
||||||
|
"refund_amount",
|
||||||
|
"refund_time",
|
||||||
|
"refund_remark",
|
||||||
|
"refund_attachments",
|
||||||
|
"workflow_instance_id",
|
||||||
|
"remark",
|
||||||
|
}
|
||||||
|
if "refund_amount" in fs and refund_amount is not None:
|
||||||
|
obj.refund_amount = validate_refund_amount(refund_amount)
|
||||||
|
if "refund_time" in fs:
|
||||||
|
if refund_time in (None, ""):
|
||||||
|
obj.refund_time = None
|
||||||
|
elif isinstance(refund_time, datetime):
|
||||||
|
obj.refund_time = refund_time
|
||||||
|
else:
|
||||||
|
obj.refund_time = parse_datetime(str(refund_time))
|
||||||
|
if "refund_remark" in fs:
|
||||||
|
obj.refund_remark = (refund_remark or "").strip() or None
|
||||||
|
if "refund_attachments" in fs:
|
||||||
|
obj.refund_attachments = (refund_attachments or "").strip() or None
|
||||||
|
if "workflow_instance_id" in fs:
|
||||||
|
obj.workflow_instance_id = (workflow_instance_id or "").strip() or None
|
||||||
|
if "remark" in fs:
|
||||||
|
obj.remark = (remark or "").strip() or None
|
||||||
|
obj.update_by = user_id
|
||||||
|
obj.update_time = _now()
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def soft_delete_refund_record(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
record_id: str,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: Optional[str],
|
||||||
|
) -> None:
|
||||||
|
obj = (
|
||||||
|
db.query(BizProtocolRefundRecord)
|
||||||
|
.filter(
|
||||||
|
BizProtocolRefundRecord.id == record_id,
|
||||||
|
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||||
|
BizProtocolRefundRecord.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not obj:
|
||||||
|
raise ValueError("退费登记不存在")
|
||||||
|
obj.del_flag = 1
|
||||||
|
obj.update_by = user_id
|
||||||
|
obj.update_time = _now()
|
||||||
|
db.add(obj)
|
||||||
|
db.commit()
|
||||||
@@ -126,9 +126,113 @@ def _apply_sample_status_filter(q, sample_status: Optional[str]):
|
|||||||
BizProtocolSample.sample_status == "",
|
BizProtocolSample.sample_status == "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if st == "collected":
|
||||||
|
return q.filter(BizProtocolSample.sample_status.in_(["ycj", "jcz", "yrk"]))
|
||||||
|
if st == "voided":
|
||||||
|
return q.filter(BizProtocolSample.sample_status == "yzf")
|
||||||
return q.filter(BizProtocolSample.sample_status == st)
|
return q.filter(BizProtocolSample.sample_status == st)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_reception_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str):
|
||||||
|
from sqlalchemy import exists
|
||||||
|
|
||||||
|
from app.models.models import BizSampleReception
|
||||||
|
|
||||||
|
f = (flag or "").strip()
|
||||||
|
if not f:
|
||||||
|
return q
|
||||||
|
base = and_(
|
||||||
|
BizSampleReception.tenant_no == tenant_no,
|
||||||
|
BizSampleReception.del_flag == 0,
|
||||||
|
BizSampleReception.reception_source == source,
|
||||||
|
BizSampleReception.sample_code == sample_code_col,
|
||||||
|
)
|
||||||
|
if f == "js":
|
||||||
|
return q.filter(exists().where(and_(base, BizSampleReception.accept_conclusion == "js")))
|
||||||
|
if f == "bj":
|
||||||
|
return q.filter(exists().where(and_(base, BizSampleReception.accept_conclusion == "bj")))
|
||||||
|
if f == "none":
|
||||||
|
return q.filter(
|
||||||
|
~exists().where(and_(base, BizSampleReception.accept_conclusion.in_(["js", "bj"])))
|
||||||
|
)
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_qc_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str):
|
||||||
|
from sqlalchemy import exists
|
||||||
|
|
||||||
|
from app.models.models import BizQcWorkOrder
|
||||||
|
|
||||||
|
f = (flag or "").strip()
|
||||||
|
if not f:
|
||||||
|
return q
|
||||||
|
base = and_(
|
||||||
|
BizQcWorkOrder.tenant_no == tenant_no,
|
||||||
|
BizQcWorkOrder.del_flag == 0,
|
||||||
|
BizQcWorkOrder.reception_source == source,
|
||||||
|
BizQcWorkOrder.trigger_source == "reception",
|
||||||
|
BizQcWorkOrder.sample_code == sample_code_col,
|
||||||
|
)
|
||||||
|
if f == "complete":
|
||||||
|
return q.filter(exists().where(and_(base, BizQcWorkOrder.conclusion.in_(["tg", "btg"]))))
|
||||||
|
if f == "voided":
|
||||||
|
return q.filter(exists().where(and_(base, BizQcWorkOrder.work_order_status == "yzf")))
|
||||||
|
if f == "incomplete":
|
||||||
|
return q.filter(
|
||||||
|
~exists().where(
|
||||||
|
and_(
|
||||||
|
base,
|
||||||
|
or_(
|
||||||
|
BizQcWorkOrder.conclusion.in_(["tg", "btg"]),
|
||||||
|
BizQcWorkOrder.work_order_status == "yzf",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_storage_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str):
|
||||||
|
from sqlalchemy import exists
|
||||||
|
|
||||||
|
from app.models.models import BizStorageReleaseReview
|
||||||
|
|
||||||
|
f = (flag or "").strip()
|
||||||
|
if not f:
|
||||||
|
return q
|
||||||
|
base = and_(
|
||||||
|
BizStorageReleaseReview.tenant_no == tenant_no,
|
||||||
|
BizStorageReleaseReview.del_flag == 0,
|
||||||
|
BizStorageReleaseReview.reception_source == source,
|
||||||
|
BizStorageReleaseReview.sample_code == sample_code_col,
|
||||||
|
)
|
||||||
|
if f == "yrk":
|
||||||
|
return q.filter(exists().where(and_(base, BizStorageReleaseReview.storage_status == "yrk")))
|
||||||
|
if f == "rejected":
|
||||||
|
return q.filter(
|
||||||
|
exists().where(
|
||||||
|
and_(
|
||||||
|
base,
|
||||||
|
BizStorageReleaseReview.storage_status != "yrk",
|
||||||
|
BizStorageReleaseReview.review_conclusion == "btg",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if f == "none":
|
||||||
|
return q.filter(
|
||||||
|
~exists().where(
|
||||||
|
and_(
|
||||||
|
base,
|
||||||
|
or_(
|
||||||
|
BizStorageReleaseReview.storage_status == "yrk",
|
||||||
|
BizStorageReleaseReview.review_conclusion == "btg",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
def list_protocol_sample_collections(
|
def list_protocol_sample_collections(
|
||||||
db: Session,
|
db: Session,
|
||||||
*,
|
*,
|
||||||
@@ -137,13 +241,64 @@ def list_protocol_sample_collections(
|
|||||||
tenant_no: str,
|
tenant_no: str,
|
||||||
keyword: Optional[str] = None,
|
keyword: Optional[str] = None,
|
||||||
sample_status: Optional[str] = None,
|
sample_status: Optional[str] = None,
|
||||||
|
collection_flag: Optional[str] = None,
|
||||||
|
reception_flag: Optional[str] = None,
|
||||||
|
qc_flag: Optional[str] = None,
|
||||||
|
storage_flag: Optional[str] = None,
|
||||||
|
storage_cert_handled: Optional[str] = None,
|
||||||
|
insurance_handled: Optional[str] = None,
|
||||||
user_info=None,
|
user_info=None,
|
||||||
) -> Tuple[List[Dict[str, Any]], int]:
|
) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
if not tenant_no:
|
if not tenant_no:
|
||||||
return [], 0
|
return [], 0
|
||||||
|
|
||||||
q = _base_query(db, tenant_no)
|
q = _base_query(db, tenant_no)
|
||||||
q = _apply_sample_status_filter(q, sample_status)
|
q = _apply_sample_status_filter(q, collection_flag or sample_status)
|
||||||
|
q = _apply_reception_flag_filter(
|
||||||
|
q, reception_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage"
|
||||||
|
)
|
||||||
|
q = _apply_qc_flag_filter(
|
||||||
|
q, qc_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage"
|
||||||
|
)
|
||||||
|
q = _apply_storage_flag_filter(
|
||||||
|
q, storage_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage"
|
||||||
|
)
|
||||||
|
from app.models.models import BizProtocolFullmoonTodo, BizProtocolStorageCert
|
||||||
|
|
||||||
|
sch = (storage_cert_handled or "").strip()
|
||||||
|
if sch in ("1", "0", "true", "false"):
|
||||||
|
want = sch in ("1", "true")
|
||||||
|
handled_pred = or_(
|
||||||
|
BizProtocolStorageCert.handle_time.isnot(None),
|
||||||
|
BizProtocolStorageCert.make_status == "1",
|
||||||
|
and_(BizProtocolStorageCert.make_date.isnot(None), BizProtocolStorageCert.make_date != ""),
|
||||||
|
)
|
||||||
|
q = q.outerjoin(
|
||||||
|
BizProtocolStorageCert,
|
||||||
|
and_(
|
||||||
|
BizProtocolStorageCert.collection_protocol_id == BizCollectionProtocol.id,
|
||||||
|
BizProtocolStorageCert.tenant_no == tenant_no,
|
||||||
|
BizProtocolStorageCert.del_flag == 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
q = q.filter(handled_pred) if want else q.filter(~handled_pred)
|
||||||
|
ih = (insurance_handled or "").strip()
|
||||||
|
if ih in ("1", "0", "true", "false"):
|
||||||
|
want = ih in ("1", "true")
|
||||||
|
handled_pred = or_(
|
||||||
|
BizProtocolFullmoonTodo.handle_time.isnot(None),
|
||||||
|
BizProtocolFullmoonTodo.todo_status == "1",
|
||||||
|
and_(BizProtocolFullmoonTodo.handle_date.isnot(None), BizProtocolFullmoonTodo.handle_date != ""),
|
||||||
|
)
|
||||||
|
q = q.outerjoin(
|
||||||
|
BizProtocolFullmoonTodo,
|
||||||
|
and_(
|
||||||
|
BizProtocolFullmoonTodo.collection_protocol_id == BizCollectionProtocol.id,
|
||||||
|
BizProtocolFullmoonTodo.tenant_no == tenant_no,
|
||||||
|
BizProtocolFullmoonTodo.del_flag == 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
q = q.filter(handled_pred) if want else q.filter(~handled_pred)
|
||||||
|
|
||||||
if keyword and keyword.strip():
|
if keyword and keyword.strip():
|
||||||
kw = f"%{keyword.strip()}%"
|
kw = f"%{keyword.strip()}%"
|
||||||
@@ -244,15 +399,58 @@ def list_protocol_sample_collections(
|
|||||||
maternal_blood_collect_time=_fmt_dt(ci.maternal_blood_collect_time) if ci else "",
|
maternal_blood_collect_time=_fmt_dt(ci.maternal_blood_collect_time) if ci else "",
|
||||||
)
|
)
|
||||||
item.update(sample_special_to_dict(psample))
|
item.update(sample_special_to_dict(psample))
|
||||||
|
# 客户服务宽表:当事人 / 协议附件 / 备注 / 市场专员 id
|
||||||
|
item["motherPhone"] = (party.mother_phone or "") if party else ""
|
||||||
|
item["motherIdCard"] = (party.mother_id_card or "") if party else ""
|
||||||
|
item["fatherName"] = (party.father_name or "") if party else ""
|
||||||
|
item["fatherPhone"] = (party.father_phone or "") if party else ""
|
||||||
|
item["fatherIdCard"] = (party.father_id_card or "") if party else ""
|
||||||
|
item["newbornIdCard"] = (party.newborn_id_card or "") if party else ""
|
||||||
|
item["newbornSex"] = (party.newborn_sex or "") if party else ""
|
||||||
|
item["newbornDeliveryTime"] = _fmt_dt(party.newborn_delivery_time) if party else ""
|
||||||
|
item["protocolRemark"] = (proto.remark or "") if proto else ""
|
||||||
|
item["protocolAttachments"] = (proto.protocol_attachments or "") if proto else ""
|
||||||
|
item["consentFormUrls"] = (proto.consent_form_urls or "") if proto else ""
|
||||||
|
item["healthSurveyFormUrls"] = (proto.health_survey_form_urls or "") if proto else ""
|
||||||
|
item["salesUserId"] = (ci.sales_user_id or "") if ci else ""
|
||||||
items.append(item)
|
items.append(item)
|
||||||
if items and tenant_no:
|
if items and tenant_no:
|
||||||
from app.crud import sample_reception as reception_svc
|
from app.crud import sample_reception as reception_svc
|
||||||
|
from app.crud.storage_release_review import _get_user_name
|
||||||
|
from app.models.models import BizProtocolSamplePayment
|
||||||
|
|
||||||
codes = [i["sampleCode"] for i in items if i.get("sampleCode")]
|
codes = [i["sampleCode"] for i in items if i.get("sampleCode")]
|
||||||
code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
|
code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
|
||||||
db, tenant_no, codes, reception_source="cell_storage"
|
db, tenant_no, codes, reception_source="cell_storage"
|
||||||
)
|
)
|
||||||
|
pay_mode_map: Dict[str, str] = {}
|
||||||
|
if codes:
|
||||||
|
for sp in (
|
||||||
|
db.query(BizProtocolSamplePayment)
|
||||||
|
.filter(
|
||||||
|
BizProtocolSamplePayment.tenant_no == tenant_no,
|
||||||
|
BizProtocolSamplePayment.del_flag == 0,
|
||||||
|
BizProtocolSamplePayment.sample_code.in_(codes),
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
):
|
||||||
|
sc = (sp.sample_code or "").strip()
|
||||||
|
if sc and sc not in pay_mode_map:
|
||||||
|
pay_mode_map[sc] = (sp.pay_mode or "").strip()
|
||||||
|
sales_ids = list({(i.get("salesUserId") or "").strip() for i in items if i.get("salesUserId")})
|
||||||
|
sales_name_map = {uid: _get_user_name(db, uid) for uid in sales_ids if uid}
|
||||||
|
pay_mode_label = {"dj": "趸交", "fq": "分期"}
|
||||||
for item in items:
|
for item in items:
|
||||||
sc = (item.get("sampleCode") or "").strip()
|
sc = (item.get("sampleCode") or "").strip()
|
||||||
item["labStorageCode"] = code_map.get(sc, "") if sc else ""
|
item["labStorageCode"] = code_map.get(sc, "") if sc else ""
|
||||||
|
mode = pay_mode_map.get(sc, "")
|
||||||
|
item["payMode"] = mode
|
||||||
|
item["payModeLabel"] = pay_mode_label.get(mode, mode or "")
|
||||||
|
sid = (item.get("salesUserId") or "").strip()
|
||||||
|
item["marketSpecialistName"] = sales_name_map.get(sid, "") if sid else ""
|
||||||
|
from app.services.sample_info_list_enrich import enrich_sample_info_items
|
||||||
|
|
||||||
|
enrich_sample_info_items(
|
||||||
|
db, tenant_no, items, reception_source="cell_storage", include_handle_flags=True
|
||||||
|
)
|
||||||
return items, total
|
return items, total
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -13,6 +13,7 @@ from app.crud import qc_task as task_svc
|
|||||||
from app.crud import qc_task_assignee as assignee_svc
|
from app.crud import qc_task_assignee as assignee_svc
|
||||||
from app.crud import qc_third_party_item as third_item_svc
|
from app.crud import qc_third_party_item as third_item_svc
|
||||||
from app.crud.qc_third_party_company import get_company
|
from app.crud.qc_third_party_company import get_company
|
||||||
|
from app.crud.user_display import user_display_name_map
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizQcBatchAssignee,
|
BizQcBatchAssignee,
|
||||||
BizQcTask,
|
BizQcTask,
|
||||||
@@ -20,7 +21,6 @@ from app.models.models import (
|
|||||||
BizQcWorkOrderInspectionBatch,
|
BizQcWorkOrderInspectionBatch,
|
||||||
BizQcWorkOrderLabItem,
|
BizQcWorkOrderLabItem,
|
||||||
BizQcWorkOrderThirdItem,
|
BizQcWorkOrderThirdItem,
|
||||||
SysUser,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_CONFIGURABLE_WORK_ORDER_STATUSES = frozenset({"dpz", "ypz", "ywc", "yzf"})
|
_CONFIGURABLE_WORK_ORDER_STATUSES = frozenset({"dpz", "ypz", "ywc", "yzf"})
|
||||||
@@ -180,13 +180,8 @@ def _batch_assignee_user_ids(db: Session, batch_id: str, tenant_no: str) -> List
|
|||||||
def _assignee_names(db: Session, user_ids: List[str]) -> str:
|
def _assignee_names(db: Session, user_ids: List[str]) -> str:
|
||||||
if not user_ids:
|
if not user_ids:
|
||||||
return ""
|
return ""
|
||||||
rows = (
|
name_map = user_display_name_map(db, user_ids)
|
||||||
db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
|
return "、".join(name_map.get(uid) or uid for uid in user_ids)
|
||||||
.filter(SysUser.id.in_(user_ids), SysUser.del_flag == 0)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
name_map = {r.id: (r.nick_name or r.user_no or r.id) for r in rows}
|
|
||||||
return "、".join(name_map.get(uid, uid) for uid in user_ids)
|
|
||||||
|
|
||||||
|
|
||||||
def _prefetch_batch_side_data(
|
def _prefetch_batch_side_data(
|
||||||
@@ -248,12 +243,7 @@ def _prefetch_batch_side_data(
|
|||||||
[uid for ids in assignees_by_batch.values() for uid in ids]
|
[uid for ids in assignees_by_batch.values() for uid in ids]
|
||||||
)
|
)
|
||||||
if all_user_ids:
|
if all_user_ids:
|
||||||
for row in (
|
name_map = user_display_name_map(db, all_user_ids)
|
||||||
db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
|
|
||||||
.filter(SysUser.id.in_(all_user_ids), SysUser.del_flag == 0)
|
|
||||||
.all()
|
|
||||||
):
|
|
||||||
name_map[row.id] = row.nick_name or row.user_no or row.id
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"assignees_by_batch": assignees_by_batch,
|
"assignees_by_batch": assignees_by_batch,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.models.qc_param import BizQcItemParam
|
from app.models.qc_param import BizQcItemParam
|
||||||
|
|
||||||
|
|
||||||
@@ -80,18 +80,23 @@ def save_params_for_item(
|
|||||||
_validate_param(p, idx)
|
_validate_param(p, idx)
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
db.query(BizQcItemParam).filter(
|
|
||||||
BizQcItemParam.item_source == item_source,
|
|
||||||
BizQcItemParam.item_id == item_id,
|
|
||||||
BizQcItemParam.tenant_no == tenant_no,
|
|
||||||
BizQcItemParam.del_flag == 0,
|
|
||||||
).update(
|
|
||||||
{"del_flag": 1, "update_by": user_id, "update_time": now},
|
|
||||||
synchronize_session="fetch",
|
|
||||||
)
|
|
||||||
db.flush()
|
|
||||||
|
|
||||||
for idx, p in enumerate(params):
|
def build_row(row_id):
|
||||||
|
return BizQcItemParam(
|
||||||
|
id=row_id,
|
||||||
|
item_source=item_source,
|
||||||
|
item_id=item_id,
|
||||||
|
param_name="",
|
||||||
|
param_code="",
|
||||||
|
param_type="text",
|
||||||
|
required_flag="0",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_row(row, p, idx, _now):
|
||||||
param_type = (p.get("paramType") or "text").strip()
|
param_type = (p.get("paramType") or "text").strip()
|
||||||
options_val = None
|
options_val = None
|
||||||
default_val = None
|
default_val = None
|
||||||
@@ -99,28 +104,32 @@ def save_params_for_item(
|
|||||||
opts = _parse_options_list(p.get("options"))
|
opts = _parse_options_list(p.get("options"))
|
||||||
options_val = json.dumps(opts, ensure_ascii=False)
|
options_val = json.dumps(opts, ensure_ascii=False)
|
||||||
default_val = opts[0] if opts else None
|
default_val = opts[0] if opts else None
|
||||||
|
row.param_name = p.get("paramName", "")
|
||||||
|
row.param_code = p.get("paramCode", "")
|
||||||
|
row.param_unit = p.get("paramUnit") or None
|
||||||
|
row.param_type = param_type
|
||||||
|
row.options = options_val
|
||||||
|
row.default_value = default_val
|
||||||
|
row.required_flag = p.get("requiredFlag", "0")
|
||||||
|
row.order_num = p.get("orderNum", idx)
|
||||||
|
|
||||||
obj = BizQcItemParam(
|
sync_child_rows(
|
||||||
id=generate_uuid(),
|
db,
|
||||||
item_source=item_source,
|
BizQcItemParam,
|
||||||
item_id=item_id,
|
parent_attr="item_id",
|
||||||
param_name=p.get("paramName", ""),
|
parent_id=item_id,
|
||||||
param_code=p.get("paramCode", ""),
|
tenant_no=tenant_no,
|
||||||
param_unit=p.get("paramUnit") or None,
|
items=params,
|
||||||
param_type=param_type,
|
user_id=user_id,
|
||||||
options=options_val,
|
now=now,
|
||||||
default_value=default_val,
|
build_row=build_row,
|
||||||
required_flag=p.get("requiredFlag", "0"),
|
apply_fields=apply_row,
|
||||||
tenant_no=tenant_no,
|
natural_key=lambda item: (item.get("paramCode") or "").strip(),
|
||||||
order_num=p.get("orderNum", idx),
|
row_natural_key=lambda row: (row.param_code or "").strip(),
|
||||||
create_by=user_id,
|
duplicate_message="参数编码重复",
|
||||||
create_dept_id="",
|
foreign_message="参数不属于当前检测项目",
|
||||||
create_time=now,
|
extra_filters={"item_source": item_source},
|
||||||
update_by=user_id,
|
reuse_deleted=True,
|
||||||
update_time=now,
|
)
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
db.add(obj)
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return list_params_by_item(db, item_source, item_id, tenant_no)
|
return list_params_by_item(db, item_source, item_id, tenant_no)
|
||||||
@@ -9,6 +9,7 @@ from sqlalchemy import func, or_
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.qc_material_stock_batch import _dec, _fmt_date, _fmt_dt, _parse_date, _require_str
|
from app.crud.qc_material_stock_batch import _dec, _fmt_date, _fmt_dt, _parse_date, _require_str
|
||||||
from app.models.models import SysUser
|
from app.models.models import SysUser
|
||||||
from app.models.qc_material import (
|
from app.models.qc_material import (
|
||||||
@@ -439,6 +440,44 @@ def _resolve_unused_item_fields(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _unused_line_key(usage_id: Optional[str], batch_id: Optional[str]) -> str:
|
||||||
|
usage = (usage_id or "").strip()
|
||||||
|
if usage:
|
||||||
|
return f"usage:{usage}"
|
||||||
|
batch = (batch_id or "").strip()
|
||||||
|
if batch:
|
||||||
|
return f"batch:{batch}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_unused_item(db: Session, it: Dict[str, Any], tenant_no: str) -> Dict[str, Any]:
|
||||||
|
it = _resolve_unused_item_fields(db, it, tenant_no)
|
||||||
|
name = _require_str(it.get("material_name") or it.get("materialName"), "物料名称")
|
||||||
|
qty = it.get("unused_qty") if it.get("unused_qty") is not None else it.get("unusedQty")
|
||||||
|
if qty is None or Decimal(str(qty)) <= 0:
|
||||||
|
raise ValueError("未用完数量必须大于0")
|
||||||
|
unit_price = it.get("unit_price") if it.get("unit_price") is not None else it.get("unitPrice")
|
||||||
|
return {
|
||||||
|
"id": str(it.get("id") or "").strip(),
|
||||||
|
"usage_id": (it.get("usage_id") or it.get("usageId") or "").strip() or None,
|
||||||
|
"batch_id": (it.get("batch_id") or it.get("batchId") or "").strip() or None,
|
||||||
|
"material_code": (it.get("material_code") or it.get("materialCode") or "").strip() or None,
|
||||||
|
"material_name": name,
|
||||||
|
"spec_model": (it.get("spec_model") or it.get("specModel") or "").strip() or None,
|
||||||
|
"unit": (it.get("unit") or "").strip() or None,
|
||||||
|
"stock_in_no": (it.get("stock_in_no") or it.get("stockInNo") or "").strip() or None,
|
||||||
|
"stock_in_date": _parse_optional_date(it.get("stock_in_date") or it.get("stockInDate")),
|
||||||
|
"expire_date": _parse_optional_date(it.get("expire_date") or it.get("expireDate")),
|
||||||
|
"catalog_no": (it.get("catalog_no") or it.get("catalogNo") or "").strip() or None,
|
||||||
|
"batch_no": (it.get("batch_no") or it.get("batchNo") or "").strip() or None,
|
||||||
|
"manufacturer": (it.get("manufacturer") or "").strip() or None,
|
||||||
|
"unit_price": Decimal(str(unit_price)) if unit_price is not None else None,
|
||||||
|
"holder": (it.get("holder") or "").strip() or None,
|
||||||
|
"unused_qty": Decimal(str(qty)),
|
||||||
|
"remark": (it.get("remark") or "").strip() or None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def save_unused_lines(
|
def save_unused_lines(
|
||||||
db: Session,
|
db: Session,
|
||||||
check_id: str,
|
check_id: str,
|
||||||
@@ -455,72 +494,61 @@ def save_unused_lines(
|
|||||||
raise ValueError("仅进行中的盘点单可登记未用完物料")
|
raise ValueError("仅进行中的盘点单可登记未用完物料")
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
old_rows = (
|
prepared = [_prepare_unused_item(db, it, tenant_no) for it in (items or [])]
|
||||||
db.query(BizQcMaterialStockCheckUnusedLine)
|
|
||||||
.filter(
|
|
||||||
BizQcMaterialStockCheckUnusedLine.check_id == check_id,
|
|
||||||
BizQcMaterialStockCheckUnusedLine.del_flag == 0,
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for r in old_rows:
|
|
||||||
r.del_flag = 1
|
|
||||||
r.update_by = user_id
|
|
||||||
r.update_time = now
|
|
||||||
db.add(r)
|
|
||||||
|
|
||||||
count = 0
|
def build_row(row_id: str):
|
||||||
for it in items or []:
|
return BizQcMaterialStockCheckUnusedLine(
|
||||||
it = _resolve_unused_item_fields(db, it, tenant_no)
|
id=row_id,
|
||||||
name = _require_str(it.get("material_name") or it.get("materialName"), "物料名称")
|
|
||||||
qty = it.get("unused_qty") if it.get("unused_qty") is not None else it.get("unusedQty")
|
|
||||||
if qty is None or Decimal(str(qty)) <= 0:
|
|
||||||
raise ValueError("未用完数量必须大于0")
|
|
||||||
qty_dec = Decimal(str(qty))
|
|
||||||
stock_in_date = _parse_optional_date(it.get("stock_in_date") or it.get("stockInDate"))
|
|
||||||
expire_date = _parse_optional_date(it.get("expire_date") or it.get("expireDate"))
|
|
||||||
usage_id = (it.get("usage_id") or it.get("usageId") or "").strip() or None
|
|
||||||
batch_id = (it.get("batch_id") or it.get("batchId") or "").strip() or None
|
|
||||||
|
|
||||||
row = BizQcMaterialStockCheckUnusedLine(
|
|
||||||
id=generate_uuid(),
|
|
||||||
check_id=check_id,
|
check_id=check_id,
|
||||||
usage_id=usage_id,
|
material_name="",
|
||||||
batch_id=batch_id,
|
unused_qty=0,
|
||||||
material_code=(it.get("material_code") or it.get("materialCode") or "").strip() or None,
|
|
||||||
material_name=name,
|
|
||||||
spec_model=(it.get("spec_model") or it.get("specModel") or "").strip() or None,
|
|
||||||
unit=(it.get("unit") or "").strip() or None,
|
|
||||||
stock_in_no=(it.get("stock_in_no") or it.get("stockInNo") or "").strip() or None,
|
|
||||||
stock_in_date=stock_in_date,
|
|
||||||
expire_date=expire_date,
|
|
||||||
catalog_no=(it.get("catalog_no") or it.get("catalogNo") or "").strip() or None,
|
|
||||||
batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
|
|
||||||
manufacturer=(it.get("manufacturer") or "").strip() or None,
|
|
||||||
unit_price=(
|
|
||||||
Decimal(str(it.get("unit_price") or it.get("unitPrice")))
|
|
||||||
if (it.get("unit_price") is not None or it.get("unitPrice") is not None)
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
holder=(it.get("holder") or "").strip() or None,
|
|
||||||
unused_qty=qty_dec,
|
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
remark=(it.get("remark") or "").strip() or None,
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id=dept_id or "",
|
create_dept_id=dept_id or "",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(row)
|
|
||||||
count += 1
|
def apply_row(row, it, idx, _now):
|
||||||
|
row.usage_id = it["usage_id"]
|
||||||
|
row.batch_id = it["batch_id"]
|
||||||
|
row.material_code = it["material_code"]
|
||||||
|
row.material_name = it["material_name"]
|
||||||
|
row.spec_model = it["spec_model"]
|
||||||
|
row.unit = it["unit"]
|
||||||
|
row.stock_in_no = it["stock_in_no"]
|
||||||
|
row.stock_in_date = it["stock_in_date"]
|
||||||
|
row.expire_date = it["expire_date"]
|
||||||
|
row.catalog_no = it["catalog_no"]
|
||||||
|
row.batch_no = it["batch_no"]
|
||||||
|
row.manufacturer = it["manufacturer"]
|
||||||
|
row.unit_price = it["unit_price"]
|
||||||
|
row.holder = it["holder"]
|
||||||
|
row.unused_qty = it["unused_qty"]
|
||||||
|
row.remark = it["remark"]
|
||||||
|
row.order_num = idx
|
||||||
|
|
||||||
|
saved = sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizQcMaterialStockCheckUnusedLine,
|
||||||
|
parent_attr="check_id",
|
||||||
|
parent_id=check_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=prepared,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda it: _unused_line_key(it.get("usage_id"), it.get("batch_id")),
|
||||||
|
row_natural_key=lambda row: _unused_line_key(row.usage_id, row.batch_id),
|
||||||
|
duplicate_message="未用完明细重复",
|
||||||
|
foreign_message="未用完明细不属于当前盘点单",
|
||||||
|
)
|
||||||
|
|
||||||
check.update_by = user_id
|
check.update_by = user_id
|
||||||
check.update_time = now
|
check.update_time = now
|
||||||
db.add(check)
|
db.add(check)
|
||||||
db.commit()
|
db.commit()
|
||||||
return count
|
return len(saved)
|
||||||
|
|
||||||
|
|
||||||
def complete_check(
|
def complete_check(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
|
||||||
from app.crud import qc_item_param as param_svc
|
from app.crud import qc_item_param as param_svc
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.crud.qc_task import get_task
|
from app.crud.qc_task import get_task
|
||||||
from app.models.models import BizQcWorkOrderLabItem, BizQcWorkOrderThirdItem
|
from app.models.models import BizQcWorkOrderLabItem, BizQcWorkOrderThirdItem
|
||||||
from app.models.qc_param import BizQcTaskResult
|
from app.models.qc_param import BizQcTaskResult
|
||||||
@@ -152,36 +152,53 @@ def save_results_for_task(
|
|||||||
if not task:
|
if not task:
|
||||||
raise ValueError("任务不存在")
|
raise ValueError("任务不存在")
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
db.query(BizQcTaskResult).filter(
|
|
||||||
BizQcTaskResult.task_id == task_id,
|
|
||||||
BizQcTaskResult.tenant_no == tenant_no,
|
|
||||||
BizQcTaskResult.del_flag == 0,
|
|
||||||
).update(
|
|
||||||
{"del_flag": 1, "update_by": user_id, "update_time": now},
|
|
||||||
synchronize_session="fetch",
|
|
||||||
)
|
|
||||||
db.flush()
|
|
||||||
|
|
||||||
for res in results:
|
def _result_key(item: Dict[str, Any]) -> str:
|
||||||
obj = BizQcTaskResult(
|
param_id = (item.get("paramId") or "").strip()
|
||||||
id=generate_uuid(),
|
if param_id:
|
||||||
|
return "id:" + param_id
|
||||||
|
return "code:" + (item.get("paramCode") or "").strip()
|
||||||
|
|
||||||
|
def build_row(row_id):
|
||||||
|
return BizQcTaskResult(
|
||||||
|
id=row_id,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
param_id=res.get("paramId") or None,
|
param_name="",
|
||||||
param_name=res.get("paramName", ""),
|
param_type="text",
|
||||||
param_code=res.get("paramCode") or None,
|
|
||||||
param_unit=res.get("paramUnit") or None,
|
|
||||||
param_type=res.get("paramType", "text"),
|
|
||||||
param_value=res.get("paramValue") or None,
|
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id="",
|
create_dept_id="",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(obj)
|
|
||||||
|
|
||||||
|
def apply_row(row, res, _idx, _now):
|
||||||
|
row.param_id = res.get("paramId") or None
|
||||||
|
row.param_name = res.get("paramName", "")
|
||||||
|
row.param_code = res.get("paramCode") or None
|
||||||
|
row.param_unit = res.get("paramUnit") or None
|
||||||
|
row.param_type = res.get("paramType", "text")
|
||||||
|
row.param_value = res.get("paramValue") or None
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizQcTaskResult,
|
||||||
|
parent_attr="task_id",
|
||||||
|
parent_id=task_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=results,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=_result_key,
|
||||||
|
row_natural_key=lambda row: (
|
||||||
|
"id:" + row.param_id.strip()
|
||||||
|
if (row.param_id or "").strip()
|
||||||
|
else "code:" + (row.param_code or "").strip()
|
||||||
|
),
|
||||||
|
duplicate_message="检测结果重复",
|
||||||
|
foreign_message="检测结果不属于当前任务",
|
||||||
|
)
|
||||||
if commit:
|
if commit:
|
||||||
db.commit()
|
db.commit()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""临时质检列表:同生产工单成组,组位置按组内最新创建时间倒序。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import cmp_to_key
|
||||||
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
from sqlalchemy import and_, func
|
||||||
|
from sqlalchemy.orm import Query
|
||||||
|
|
||||||
|
from app.models.models import BizQcWorkOrder
|
||||||
|
|
||||||
|
|
||||||
|
def uses_temporary_production_cluster_list_sort(work_order_type: Optional[str]) -> bool:
|
||||||
|
return (work_order_type or "").strip() == "temporary"
|
||||||
|
|
||||||
|
|
||||||
|
def temporary_production_cluster_key(
|
||||||
|
production_work_order_id: Optional[str], row_id: str
|
||||||
|
) -> str:
|
||||||
|
pwo = (production_work_order_id or "").strip()
|
||||||
|
if pwo:
|
||||||
|
return f"pwo:{pwo}"
|
||||||
|
return f"id:{(row_id or '').strip()}"
|
||||||
|
|
||||||
|
|
||||||
|
def _max_create_time_by_cluster(
|
||||||
|
rows: Sequence[Tuple[str, Optional[str], str]],
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""rows: (id, production_work_order_id, create_time_str)"""
|
||||||
|
group_max: Dict[str, str] = {}
|
||||||
|
for row_id, pwo_id, create_time in rows:
|
||||||
|
key = temporary_production_cluster_key(pwo_id, row_id)
|
||||||
|
t = (create_time or "").strip()
|
||||||
|
prev = group_max.get(key, "")
|
||||||
|
if t > prev:
|
||||||
|
group_max[key] = t
|
||||||
|
return group_max
|
||||||
|
|
||||||
|
|
||||||
|
def compare_temporary_production_cluster_desc(
|
||||||
|
id_a: str,
|
||||||
|
pwo_a: Optional[str],
|
||||||
|
time_a: Optional[str],
|
||||||
|
group_max_a: str,
|
||||||
|
id_b: str,
|
||||||
|
pwo_b: Optional[str],
|
||||||
|
time_b: Optional[str],
|
||||||
|
group_max_b: str,
|
||||||
|
) -> int:
|
||||||
|
"""负值表示 a 应排在 b 前。"""
|
||||||
|
ga = (group_max_a or "").strip()
|
||||||
|
gb = (group_max_b or "").strip()
|
||||||
|
if ga != gb:
|
||||||
|
return -1 if ga > gb else 1
|
||||||
|
key_a = temporary_production_cluster_key(pwo_a, id_a)
|
||||||
|
key_b = temporary_production_cluster_key(pwo_b, id_b)
|
||||||
|
if key_a != key_b:
|
||||||
|
return -1 if key_a < key_b else 1
|
||||||
|
ta = (time_a or "").strip()
|
||||||
|
tb = (time_b or "").strip()
|
||||||
|
if ta != tb:
|
||||||
|
return -1 if ta > tb else 1
|
||||||
|
ia = (id_a or "").strip()
|
||||||
|
ib = (id_b or "").strip()
|
||||||
|
if ia != ib:
|
||||||
|
return -1 if ia > ib else 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def sort_dicts_by_temporary_production_cluster(
|
||||||
|
items: List[Dict[str, Any]],
|
||||||
|
*,
|
||||||
|
id_key: str = "id",
|
||||||
|
pwo_key: str = "productionWorkOrderId",
|
||||||
|
time_key: str = "createTime",
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""按设计规则排序质检工单 dict 列表(供单测 / 内存排序)。"""
|
||||||
|
triples: List[Tuple[str, Optional[str], str]] = []
|
||||||
|
for item in items:
|
||||||
|
rid = str(item.get(id_key) or "")
|
||||||
|
pwo = item.get(pwo_key)
|
||||||
|
pwo_s: Optional[str]
|
||||||
|
if pwo is None:
|
||||||
|
pwo_s = None
|
||||||
|
elif isinstance(pwo, str):
|
||||||
|
pwo_s = pwo
|
||||||
|
else:
|
||||||
|
pwo_s = str(pwo)
|
||||||
|
triples.append((rid, pwo_s, str(item.get(time_key) or "")))
|
||||||
|
group_max = _max_create_time_by_cluster(triples)
|
||||||
|
|
||||||
|
def cmp(a: Dict[str, Any], b: Dict[str, Any]) -> int:
|
||||||
|
id_a = str(a.get(id_key) or "")
|
||||||
|
id_b = str(b.get(id_key) or "")
|
||||||
|
pwo_a = a.get(pwo_key)
|
||||||
|
pwo_b = b.get(pwo_key)
|
||||||
|
if pwo_a is not None and not isinstance(pwo_a, str):
|
||||||
|
pwo_a = str(pwo_a)
|
||||||
|
if pwo_b is not None and not isinstance(pwo_b, str):
|
||||||
|
pwo_b = str(pwo_b)
|
||||||
|
time_a = str(a.get(time_key) or "")
|
||||||
|
time_b = str(b.get(time_key) or "")
|
||||||
|
key_a = temporary_production_cluster_key(pwo_a, id_a)
|
||||||
|
key_b = temporary_production_cluster_key(pwo_b, id_b)
|
||||||
|
return compare_temporary_production_cluster_desc(
|
||||||
|
id_a,
|
||||||
|
pwo_a,
|
||||||
|
time_a,
|
||||||
|
group_max.get(key_a, ""),
|
||||||
|
id_b,
|
||||||
|
pwo_b,
|
||||||
|
time_b,
|
||||||
|
group_max.get(key_b, ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted(items, key=cmp_to_key(cmp))
|
||||||
|
|
||||||
|
|
||||||
|
def apply_temporary_production_cluster_order(q: Query) -> Query:
|
||||||
|
"""对已过滤的 BizQcWorkOrder Query 施加成组排序(MySQL 5.7 友好)。"""
|
||||||
|
session = q.session
|
||||||
|
base = (
|
||||||
|
q.order_by(None)
|
||||||
|
.with_entities(
|
||||||
|
BizQcWorkOrder.id.label("wo_id"),
|
||||||
|
BizQcWorkOrder.production_work_order_id.label("pwo_id"),
|
||||||
|
BizQcWorkOrder.create_time.label("ct"),
|
||||||
|
)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
pwo_max = (
|
||||||
|
session.query(
|
||||||
|
base.c.pwo_id.label("pwo_id"),
|
||||||
|
func.max(base.c.ct).label("group_ct"),
|
||||||
|
)
|
||||||
|
.select_from(base)
|
||||||
|
.filter(
|
||||||
|
base.c.pwo_id.isnot(None),
|
||||||
|
base.c.pwo_id != "",
|
||||||
|
)
|
||||||
|
.group_by(base.c.pwo_id)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
q.outerjoin(
|
||||||
|
pwo_max,
|
||||||
|
and_(
|
||||||
|
BizQcWorkOrder.production_work_order_id == pwo_max.c.pwo_id,
|
||||||
|
BizQcWorkOrder.production_work_order_id.isnot(None),
|
||||||
|
BizQcWorkOrder.production_work_order_id != "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order_by(
|
||||||
|
func.coalesce(pwo_max.c.group_ct, BizQcWorkOrder.create_time).desc(),
|
||||||
|
func.coalesce(BizQcWorkOrder.production_work_order_id, BizQcWorkOrder.id).asc(),
|
||||||
|
BizQcWorkOrder.create_time.desc(),
|
||||||
|
BizQcWorkOrder.id.desc(),
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -6,10 +6,14 @@ from datetime import datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from sqlalchemy import and_, func, or_
|
from sqlalchemy import and_, exists, func, or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.db_collation import eq_unicode, like_unicode, unicode_ci
|
from app.core.db_collation import eq_unicode, like_unicode, unicode_ci
|
||||||
|
from app.core.protocol_status_display import (
|
||||||
|
DEFAULT_QC_DONATION_PROTOCOL_STATUSES,
|
||||||
|
resolve_research_donation_protocol_status_filter,
|
||||||
|
)
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
from app.crud import qc_lab_test_item as lab_item_svc
|
from app.crud import qc_lab_test_item as lab_item_svc
|
||||||
from app.crud import qc_task as task_svc
|
from app.crud import qc_task as task_svc
|
||||||
@@ -20,6 +24,10 @@ from app.crud.qc_inbound_lab_storage_sort import (
|
|||||||
inbound_lab_storage_order_by_clauses,
|
inbound_lab_storage_order_by_clauses,
|
||||||
uses_inbound_lab_storage_list_sort,
|
uses_inbound_lab_storage_list_sort,
|
||||||
)
|
)
|
||||||
|
from app.crud.qc_temporary_production_cluster_sort import (
|
||||||
|
apply_temporary_production_cluster_order,
|
||||||
|
uses_temporary_production_cluster_list_sort,
|
||||||
|
)
|
||||||
from app.crud.collection_protocol import urls_to_attachment_list
|
from app.crud.collection_protocol import urls_to_attachment_list
|
||||||
from app.utils.sample_special import apply_special_to_item_from_samples
|
from app.utils.sample_special import apply_special_to_item_from_samples
|
||||||
from app.crud import sample_reception as sample_reception_svc
|
from app.crud import sample_reception as sample_reception_svc
|
||||||
@@ -219,10 +227,51 @@ def _enrich_work_order_cell_products(
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def _step_output_identity(row: BizProductionTaskStepOutput) -> Tuple[str, str, str, str]:
|
||||||
|
return (
|
||||||
|
(row.step_id or "").strip(),
|
||||||
|
(row.output_name or "").strip(),
|
||||||
|
(row.output_barcode or "").strip(),
|
||||||
|
(row.output_type_id or "").strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _live_output_replacements(
|
||||||
|
db: Session,
|
||||||
|
tenant_no: str,
|
||||||
|
deleted_rows: List[BizProductionTaskStepOutput],
|
||||||
|
) -> Dict[str, BizProductionTaskStepOutput]:
|
||||||
|
"""已软删产出物 → 同一步骤上名称、条码、产出类型都一致的唯一有效行。"""
|
||||||
|
step_ids = list({(row.step_id or "").strip() for row in deleted_rows if (row.step_id or "").strip()})
|
||||||
|
if not step_ids:
|
||||||
|
return {}
|
||||||
|
live_rows = (
|
||||||
|
db.query(BizProductionTaskStepOutput)
|
||||||
|
.filter(
|
||||||
|
BizProductionTaskStepOutput.step_id.in_(step_ids),
|
||||||
|
BizProductionTaskStepOutput.tenant_no == tenant_no,
|
||||||
|
BizProductionTaskStepOutput.del_flag == 0,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
grouped: Dict[Tuple[str, str, str, str], List[BizProductionTaskStepOutput]] = defaultdict(list)
|
||||||
|
for row in live_rows:
|
||||||
|
grouped[_step_output_identity(row)].append(row)
|
||||||
|
replacements: Dict[str, BizProductionTaskStepOutput] = {}
|
||||||
|
for old in deleted_rows:
|
||||||
|
matches = [row for row in grouped.get(_step_output_identity(old), []) if row.id != old.id]
|
||||||
|
if len(matches) == 1:
|
||||||
|
replacements[old.id] = matches[0]
|
||||||
|
return replacements
|
||||||
|
|
||||||
|
|
||||||
def _enrich_work_order_step_outputs(
|
def _enrich_work_order_step_outputs(
|
||||||
db: Session, tenant_no: str, items: List[Dict[str, Any]]
|
db: Session, tenant_no: str, items: List[Dict[str, Any]]
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""步骤产出物触发的质检:无细胞产品时用工单条码/名称展示质检对象。"""
|
"""步骤产出物触发的质检:无细胞产品时用有效产出物的条码和名称展示质检对象。
|
||||||
|
|
||||||
|
工单若仍指向已软删产出物,改用同一步骤上的那条有效产出物,不再拿软删行当质检对象。
|
||||||
|
"""
|
||||||
if not items:
|
if not items:
|
||||||
return items
|
return items
|
||||||
step_output_ids = list(
|
step_output_ids = list(
|
||||||
@@ -240,17 +289,23 @@ def _enrich_work_order_step_outputs(
|
|||||||
.filter(
|
.filter(
|
||||||
BizProductionTaskStepOutput.id.in_(step_output_ids),
|
BizProductionTaskStepOutput.id.in_(step_output_ids),
|
||||||
BizProductionTaskStepOutput.tenant_no == tenant_no,
|
BizProductionTaskStepOutput.tenant_no == tenant_no,
|
||||||
BizProductionTaskStepOutput.del_flag == 0,
|
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
row_map = {r.id: r for r in rows}
|
deleted_rows = [row for row in rows if int(row.del_flag or 0) == 1]
|
||||||
|
live_by_deleted_id = (
|
||||||
|
_live_output_replacements(db, tenant_no, deleted_rows) if deleted_rows else {}
|
||||||
|
)
|
||||||
|
row_map = {row.id: row for row in rows}
|
||||||
for item in items:
|
for item in items:
|
||||||
if (item.get("cellProductId") or "").strip():
|
if (item.get("cellProductId") or "").strip():
|
||||||
continue
|
continue
|
||||||
so = row_map.get(item.get("productionStepOutputId") or "")
|
so = row_map.get(item.get("productionStepOutputId") or "")
|
||||||
if not so:
|
if so is not None and int(so.del_flag or 0) == 1:
|
||||||
|
so = live_by_deleted_id.get(so.id)
|
||||||
|
if so is None or int(so.del_flag or 0) == 1:
|
||||||
continue
|
continue
|
||||||
|
item["productionStepOutputId"] = so.id
|
||||||
if not (item.get("cellProductCode") or "").strip():
|
if not (item.get("cellProductCode") or "").strip():
|
||||||
item["cellProductCode"] = (so.output_barcode or "").strip()
|
item["cellProductCode"] = (so.output_barcode or "").strip()
|
||||||
if not (item.get("cellProductName") or "").strip():
|
if not (item.get("cellProductName") or "").strip():
|
||||||
@@ -298,6 +353,8 @@ def _enrich_outbound_work_orders(
|
|||||||
"projectName": product.project_name or "",
|
"projectName": product.project_name or "",
|
||||||
"cellTypeName": product.cell_type_name or "",
|
"cellTypeName": product.cell_type_name or "",
|
||||||
"specText": product.spec_text or "",
|
"specText": product.spec_text or "",
|
||||||
|
"packageId": getattr(product, "package_id", None) or "",
|
||||||
|
"packageText": getattr(product, "package_text", None) or "",
|
||||||
"agreementPrice": _dec(product.agreement_price),
|
"agreementPrice": _dec(product.agreement_price),
|
||||||
"actualPrice": _dec(product.actual_price),
|
"actualPrice": _dec(product.actual_price),
|
||||||
"quantity": product.quantity,
|
"quantity": product.quantity,
|
||||||
@@ -381,6 +438,8 @@ def _enrich_outbound_work_orders(
|
|||||||
item["totalCourseCount"] = course.get("totalCourseCount")
|
item["totalCourseCount"] = course.get("totalCourseCount")
|
||||||
item["cellType"] = sched.cell_type or ""
|
item["cellType"] = sched.cell_type or ""
|
||||||
item["specText"] = sched.spec_text or ""
|
item["specText"] = sched.spec_text or ""
|
||||||
|
item["packageId"] = getattr(sched, "package_id", None) or ""
|
||||||
|
item["packageText"] = getattr(sched, "package_text", None) or ""
|
||||||
item["infusionTime"] = _fmt_dt(sched.infusion_time)
|
item["infusionTime"] = _fmt_dt(sched.infusion_time)
|
||||||
item["products"] = products_map.get(sid, [])
|
item["products"] = products_map.get(sid, [])
|
||||||
return items
|
return items
|
||||||
@@ -1566,6 +1625,52 @@ def _protocol_sample_ids_for_sample_type(
|
|||||||
return ids
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def _research_donation_status_exists(statuses: List[str]):
|
||||||
|
"""质检工单关联科研捐赠单且 donation_status 命中 statuses。"""
|
||||||
|
return exists().where(
|
||||||
|
and_(
|
||||||
|
eq_unicode(BizResearchDonation.id, BizQcWorkOrder.collection_protocol_id),
|
||||||
|
BizResearchDonation.del_flag == 0,
|
||||||
|
BizResearchDonation.donation_status.in_(statuses),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_research_donation_protocol_status_filter(
|
||||||
|
query,
|
||||||
|
protocol_status: Optional[str],
|
||||||
|
*,
|
||||||
|
reception_source: Optional[str] = None,
|
||||||
|
force_research_donation_scope: bool = False,
|
||||||
|
):
|
||||||
|
"""科研捐赠质检:按协议/捐赠状态过滤。
|
||||||
|
|
||||||
|
- force_research_donation_scope=True:用于全量 Tab 统计,仅剔除 RD 废弃行,其它来源不动。
|
||||||
|
- 否则仅当 reception_source == research_donation 时生效。
|
||||||
|
"""
|
||||||
|
rs = (reception_source or "").strip()
|
||||||
|
if force_research_donation_scope:
|
||||||
|
# 角标与列表默认一致:科研捐赠仅计待生效/生效中
|
||||||
|
not_default = and_(
|
||||||
|
BizQcWorkOrder.reception_source == "research_donation",
|
||||||
|
~_research_donation_status_exists(list(DEFAULT_QC_DONATION_PROTOCOL_STATUSES)),
|
||||||
|
)
|
||||||
|
return query.filter(~not_default)
|
||||||
|
|
||||||
|
if rs != "research_donation":
|
||||||
|
return query
|
||||||
|
|
||||||
|
mode, statuses = resolve_research_donation_protocol_status_filter(protocol_status)
|
||||||
|
if mode == "none":
|
||||||
|
return query.filter(BizQcWorkOrder.id.in_([]))
|
||||||
|
if mode == "in":
|
||||||
|
return query.filter(_research_donation_status_exists(statuses))
|
||||||
|
# default:与前端默认勾选一致
|
||||||
|
return query.filter(
|
||||||
|
_research_donation_status_exists(list(DEFAULT_QC_DONATION_PROTOCOL_STATUSES))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_work_orders(
|
def list_work_orders(
|
||||||
db: Session,
|
db: Session,
|
||||||
*,
|
*,
|
||||||
@@ -1578,6 +1683,7 @@ def list_work_orders(
|
|||||||
reception_source: Optional[str] = None,
|
reception_source: Optional[str] = None,
|
||||||
trigger_source: Optional[str] = None,
|
trigger_source: Optional[str] = None,
|
||||||
sample_type_id: Optional[str] = None,
|
sample_type_id: Optional[str] = None,
|
||||||
|
protocol_status: Optional[str] = None,
|
||||||
create_time_start: Optional[str] = None,
|
create_time_start: Optional[str] = None,
|
||||||
create_time_end: Optional[str] = None,
|
create_time_end: Optional[str] = None,
|
||||||
infusion_time_start: Optional[str] = None,
|
infusion_time_start: Optional[str] = None,
|
||||||
@@ -1708,6 +1814,11 @@ def list_work_orders(
|
|||||||
if user_info:
|
if user_info:
|
||||||
from app.core.data_scope import apply_data_scope
|
from app.core.data_scope import apply_data_scope
|
||||||
q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)
|
q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)
|
||||||
|
q0 = apply_research_donation_protocol_status_filter(
|
||||||
|
q0,
|
||||||
|
protocol_status,
|
||||||
|
reception_source=rs,
|
||||||
|
)
|
||||||
return q0
|
return q0
|
||||||
|
|
||||||
total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
|
total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
|
||||||
@@ -1716,6 +1827,8 @@ def list_work_orders(
|
|||||||
q_page = _filtered()
|
q_page = _filtered()
|
||||||
if uses_inbound_lab_storage_list_sort(reception_source):
|
if uses_inbound_lab_storage_list_sort(reception_source):
|
||||||
q_page = q_page.order_by(*inbound_lab_storage_order_by_clauses())
|
q_page = q_page.order_by(*inbound_lab_storage_order_by_clauses())
|
||||||
|
elif uses_temporary_production_cluster_list_sort(work_order_type):
|
||||||
|
q_page = apply_temporary_production_cluster_order(q_page)
|
||||||
else:
|
else:
|
||||||
q_page = q_page.order_by(BizQcWorkOrder.create_time.desc())
|
q_page = q_page.order_by(BizQcWorkOrder.create_time.desc())
|
||||||
rows = (
|
rows = (
|
||||||
@@ -1941,6 +2054,9 @@ def count_unfinished_work_orders_by_tab(
|
|||||||
BizQcWorkOrder.tenant_no == tenant_no,
|
BizQcWorkOrder.tenant_no == tenant_no,
|
||||||
)
|
)
|
||||||
q = _unfinished_work_order_filter(q)
|
q = _unfinished_work_order_filter(q)
|
||||||
|
q = apply_research_donation_protocol_status_filter(
|
||||||
|
q, None, force_research_donation_scope=True
|
||||||
|
)
|
||||||
if user_info:
|
if user_info:
|
||||||
from app.core.data_scope import apply_data_scope
|
from app.core.data_scope import apply_data_scope
|
||||||
|
|
||||||
@@ -1997,6 +2113,9 @@ def count_mine_unfinished_work_orders_by_tab(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
q = _unfinished_work_order_filter(q)
|
q = _unfinished_work_order_filter(q)
|
||||||
|
q = apply_research_donation_protocol_status_filter(
|
||||||
|
q, None, force_research_donation_scope=True
|
||||||
|
)
|
||||||
rows = (
|
rows = (
|
||||||
q.group_by(
|
q.group_by(
|
||||||
BizQcWorkOrder.work_order_type,
|
BizQcWorkOrder.work_order_type,
|
||||||
@@ -3215,9 +3334,9 @@ def list_temp_qc_work_orders(
|
|||||||
total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
|
total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
|
||||||
page = max(1, page)
|
page = max(1, page)
|
||||||
page_size = min(max(1, page_size), 100)
|
page_size = min(max(1, page_size), 100)
|
||||||
|
q_page = apply_temporary_production_cluster_order(_filtered())
|
||||||
rows = (
|
rows = (
|
||||||
_filtered()
|
q_page
|
||||||
.order_by(BizQcWorkOrder.create_time.desc())
|
|
||||||
.offset((page - 1) * page_size)
|
.offset((page - 1) * page_size)
|
||||||
.limit(page_size)
|
.limit(page_size)
|
||||||
.all()
|
.all()
|
||||||
@@ -3422,6 +3541,8 @@ def build_outbound_pilot_context_by_schedule(
|
|||||||
"infusionMethod": sched.infusion_method or "",
|
"infusionMethod": sched.infusion_method or "",
|
||||||
"cellType": sched.cell_type or "",
|
"cellType": sched.cell_type or "",
|
||||||
"specText": sched.spec_text or "",
|
"specText": sched.spec_text or "",
|
||||||
|
"packageId": getattr(sched, "package_id", None) or "",
|
||||||
|
"packageText": getattr(sched, "package_text", None) or "",
|
||||||
"scheduleStatus": sched.schedule_status or "",
|
"scheduleStatus": sched.schedule_status or "",
|
||||||
"infusionStatus": sched.infusion_status or "",
|
"infusionStatus": sched.infusion_status or "",
|
||||||
"qcStatus": sched.qc_status or "",
|
"qcStatus": sched.qc_status or "",
|
||||||
@@ -3510,6 +3631,8 @@ def build_outbound_pilot_context_by_schedule(
|
|||||||
"projectName": p.project_name or "",
|
"projectName": p.project_name or "",
|
||||||
"cellTypeName": p.cell_type_name or "",
|
"cellTypeName": p.cell_type_name or "",
|
||||||
"specText": p.spec_text or "",
|
"specText": p.spec_text or "",
|
||||||
|
"packageId": getattr(p, "package_id", None) or "",
|
||||||
|
"packageText": getattr(p, "package_text", None) or "",
|
||||||
"agreementPrice": _dec(p.agreement_price),
|
"agreementPrice": _dec(p.agreement_price),
|
||||||
"actualPrice": _dec(p.actual_price),
|
"actualPrice": _dec(p.actual_price),
|
||||||
"quantity": p.quantity,
|
"quantity": p.quantity,
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -128,6 +128,10 @@ def _apply_sample_status_filter(q, sample_status: Optional[str]):
|
|||||||
BizResearchDonationSample.sample_status == "",
|
BizResearchDonationSample.sample_status == "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if st == "collected":
|
||||||
|
return q.filter(BizResearchDonationSample.sample_status.in_(["ycj", "jcz", "yrk"]))
|
||||||
|
if st == "voided":
|
||||||
|
return q.filter(BizResearchDonationSample.sample_status == "yzf")
|
||||||
return q.filter(BizResearchDonationSample.sample_status == st)
|
return q.filter(BizResearchDonationSample.sample_status == st)
|
||||||
|
|
||||||
|
|
||||||
@@ -139,13 +143,44 @@ def list_donation_sample_collections(
|
|||||||
tenant_no: str,
|
tenant_no: str,
|
||||||
keyword: Optional[str] = None,
|
keyword: Optional[str] = None,
|
||||||
sample_status: Optional[str] = None,
|
sample_status: Optional[str] = None,
|
||||||
|
collection_flag: Optional[str] = None,
|
||||||
|
reception_flag: Optional[str] = None,
|
||||||
|
qc_flag: Optional[str] = None,
|
||||||
|
storage_flag: Optional[str] = None,
|
||||||
user_info=None,
|
user_info=None,
|
||||||
) -> Tuple[List[Dict[str, Any]], int]:
|
) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
if not tenant_no:
|
if not tenant_no:
|
||||||
return [], 0
|
return [], 0
|
||||||
|
|
||||||
|
from app.crud.protocol_sample_collection import (
|
||||||
|
_apply_qc_flag_filter,
|
||||||
|
_apply_reception_flag_filter,
|
||||||
|
_apply_storage_flag_filter,
|
||||||
|
)
|
||||||
|
|
||||||
q = _base_query(db, tenant_no)
|
q = _base_query(db, tenant_no)
|
||||||
q = _apply_sample_status_filter(q, sample_status)
|
q = _apply_sample_status_filter(q, collection_flag or sample_status)
|
||||||
|
q = _apply_reception_flag_filter(
|
||||||
|
q,
|
||||||
|
reception_flag,
|
||||||
|
sample_code_col=BizResearchDonationSample.sample_code,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
source="research_donation",
|
||||||
|
)
|
||||||
|
q = _apply_qc_flag_filter(
|
||||||
|
q,
|
||||||
|
qc_flag,
|
||||||
|
sample_code_col=BizResearchDonationSample.sample_code,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
source="research_donation",
|
||||||
|
)
|
||||||
|
q = _apply_storage_flag_filter(
|
||||||
|
q,
|
||||||
|
storage_flag,
|
||||||
|
sample_code_col=BizResearchDonationSample.sample_code,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
source="research_donation",
|
||||||
|
)
|
||||||
|
|
||||||
if keyword and keyword.strip():
|
if keyword and keyword.strip():
|
||||||
kw = f"%{keyword.strip()}%"
|
kw = f"%{keyword.strip()}%"
|
||||||
@@ -257,4 +292,13 @@ def list_donation_sample_collections(
|
|||||||
for item in items:
|
for item in items:
|
||||||
sc = (item.get("sampleCode") or "").strip()
|
sc = (item.get("sampleCode") or "").strip()
|
||||||
item["labStorageCode"] = code_map.get(sc, "") if sc else ""
|
item["labStorageCode"] = code_map.get(sc, "") if sc else ""
|
||||||
|
from app.services.sample_info_list_enrich import enrich_sample_info_items
|
||||||
|
|
||||||
|
enrich_sample_info_items(
|
||||||
|
db,
|
||||||
|
tenant_no,
|
||||||
|
items,
|
||||||
|
reception_source="research_donation",
|
||||||
|
include_handle_flags=False,
|
||||||
|
)
|
||||||
return items, total
|
return items, total
|
||||||
+49
-43
@@ -9,6 +9,7 @@ from sqlalchemy import and_, func
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_child_rows, sync_link_rows
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
SysPost,
|
SysPost,
|
||||||
SysPostRole,
|
SysPostRole,
|
||||||
@@ -377,26 +378,18 @@ def update_role_menus(
|
|||||||
tenant_no: str,
|
tenant_no: str,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# uk_role_menu 为 (role_id, menu_id, del_flag):先全量软删再插入会在再次保存时
|
sync_link_rows(
|
||||||
# 出现两条 (role_id, menu_id, del_flag=1),触发 1062。关联表采用整角色物理清理后重建。
|
db,
|
||||||
unique_ids = list(dict.fromkeys(menu_ids or []))
|
SysRoleMenu,
|
||||||
db.query(SysRoleMenu).filter(SysRoleMenu.role_id == role_id).delete(synchronize_session=False)
|
parent_attr="role_id",
|
||||||
now = datetime.now()
|
parent_id=role_id,
|
||||||
for mid in unique_ids:
|
link_attr="menu_id",
|
||||||
obj = SysRoleMenu(
|
link_ids=menu_ids,
|
||||||
id=generate_uuid(),
|
tenant_no=tenant_no,
|
||||||
role_id=role_id,
|
user_id=user_id,
|
||||||
menu_id=mid,
|
duplicate_message="菜单重复",
|
||||||
tenant_no=tenant_no,
|
foreign_message="菜单关联不属于当前角色",
|
||||||
order_num=0,
|
)
|
||||||
create_by=user_id,
|
|
||||||
create_dept_id="",
|
|
||||||
create_time=now,
|
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
db.add(obj)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -430,28 +423,41 @@ def update_role_data_scope(
|
|||||||
obj.update_time = datetime.now()
|
obj.update_time = datetime.now()
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
|
|
||||||
# soft-delete old scope details
|
now = datetime.now()
|
||||||
db.query(SysRoleDataScopeDetail).filter(
|
details = scope_details if data_scope == "5" and scope_details else []
|
||||||
and_(SysRoleDataScopeDetail.role_id == role_id, SysRoleDataScopeDetail.del_flag == 0)
|
|
||||||
).update({"del_flag": 1, "update_time": datetime.now()})
|
|
||||||
|
|
||||||
# insert new scope details (only when custom data_scope == '5')
|
def build_scope(row_id):
|
||||||
if data_scope == "5" and scope_details:
|
return SysRoleDataScopeDetail(
|
||||||
now = datetime.now()
|
id=row_id,
|
||||||
for detail in scope_details:
|
role_id=role_id,
|
||||||
sd = SysRoleDataScopeDetail(
|
scope_type="",
|
||||||
id=generate_uuid(),
|
scope_value="",
|
||||||
role_id=role_id,
|
tenant_no=tenant_no,
|
||||||
scope_type=detail.get("scopeType", "dept"),
|
create_by=user_id,
|
||||||
scope_value=detail.get("scopeValue", ""),
|
create_dept_id="",
|
||||||
tenant_no=tenant_no,
|
create_time=now,
|
||||||
order_num=0,
|
)
|
||||||
create_by=user_id,
|
|
||||||
create_dept_id="",
|
def apply_scope(row, detail, _idx, _now):
|
||||||
create_time=now,
|
row.scope_type = detail.get("scopeType", "dept")
|
||||||
update_by=user_id,
|
row.scope_value = detail.get("scopeValue", "")
|
||||||
update_time=now,
|
row.order_num = 0
|
||||||
del_flag=0,
|
|
||||||
)
|
sync_child_rows(
|
||||||
db.add(sd)
|
db,
|
||||||
|
SysRoleDataScopeDetail,
|
||||||
|
parent_attr="role_id",
|
||||||
|
parent_id=role_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=details,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_scope,
|
||||||
|
apply_fields=apply_scope,
|
||||||
|
natural_key=lambda detail: f"{detail.get('scopeType', 'dept')}:{detail.get('scopeValue', '')}",
|
||||||
|
row_natural_key=lambda row: f"{row.scope_type}:{row.scope_value}",
|
||||||
|
duplicate_message="数据权限重复",
|
||||||
|
foreign_message="数据权限不属于当前角色",
|
||||||
|
reuse_deleted=True,
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -23,7 +23,7 @@ def _resolve_issued_user_display_name(
|
|||||||
uid = (user_id or "").strip()
|
uid = (user_id or "").strip()
|
||||||
if not uid:
|
if not uid:
|
||||||
return None
|
return None
|
||||||
u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
|
u = db.query(SysUser).filter(SysUser.id == uid).first()
|
||||||
if not u:
|
if not u:
|
||||||
return None
|
return None
|
||||||
return (u.nick_name or u.user_no or "").strip() or None
|
return (u.nick_name or u.user_no or "").strip() or None
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""样本客户服务扩展 CRUD。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.customer_service_dict import ensure_dict_value
|
||||||
|
from app.models.models import BizSampleCustomerService
|
||||||
|
from app.services.sample_customer_service_register import (
|
||||||
|
CUSTOMER_SERVICE_FIELD_KEYS,
|
||||||
|
merge_customer_service_fields,
|
||||||
|
normalize_dict_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
DICT_TYPE_BY_FIELD = {
|
||||||
|
"sms_send_status": "sms_send_status",
|
||||||
|
"microbe_followup_status": "microbe_followup_status",
|
||||||
|
"fullmoon_followup_status": "fullmoon_followup_status",
|
||||||
|
"household_registered": "household_registered",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _format_date(value: Optional[date]) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.strftime("%Y-%m-%d")
|
||||||
|
return value.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_datetime(value: Optional[datetime]) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return value.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def _row_field_dict(row: BizSampleCustomerService) -> Dict[str, Any]:
|
||||||
|
return {key: getattr(row, key) for key in CUSTOMER_SERVICE_FIELD_KEYS}
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_sample_code(
|
||||||
|
db: Session, tenant_no: str, sample_code: str
|
||||||
|
) -> Optional[BizSampleCustomerService]:
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
if not code:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
db.query(BizSampleCustomerService)
|
||||||
|
.filter(
|
||||||
|
BizSampleCustomerService.tenant_no == tenant_no,
|
||||||
|
BizSampleCustomerService.sample_code == code,
|
||||||
|
BizSampleCustomerService.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_map_by_sample_codes(
|
||||||
|
db: Session, tenant_no: str, sample_codes: List[str]
|
||||||
|
) -> Dict[str, BizSampleCustomerService]:
|
||||||
|
codes = list({(c or "").strip() for c in sample_codes if (c or "").strip()})
|
||||||
|
if not codes:
|
||||||
|
return {}
|
||||||
|
rows = (
|
||||||
|
db.query(BizSampleCustomerService)
|
||||||
|
.filter(
|
||||||
|
BizSampleCustomerService.tenant_no == tenant_no,
|
||||||
|
BizSampleCustomerService.sample_code.in_(codes),
|
||||||
|
BizSampleCustomerService.del_flag == 0,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return {row.sample_code: row for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def row_to_dict(row: BizSampleCustomerService) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"sampleCode": row.sample_code,
|
||||||
|
"collectionProtocolId": row.collection_protocol_id,
|
||||||
|
"smsSendDate": _format_date(row.sms_send_date),
|
||||||
|
"smsSendStatus": row.sms_send_status,
|
||||||
|
"microbeFollowupDate": _format_date(row.microbe_followup_date),
|
||||||
|
"microbeFollowupStatus": row.microbe_followup_status,
|
||||||
|
"fullmoonFollowupDate": _format_date(row.fullmoon_followup_date),
|
||||||
|
"fullmoonFollowupStatus": row.fullmoon_followup_status,
|
||||||
|
"householdRegistered": row.household_registered,
|
||||||
|
"einvoiceSendTime": _format_datetime(row.einvoice_send_time),
|
||||||
|
"effectiveAddress": row.effective_address,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_by_sample_code(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
sample_code: str,
|
||||||
|
collection_protocol_id: Optional[str],
|
||||||
|
user_id: Optional[str],
|
||||||
|
incoming: Dict[str, Any],
|
||||||
|
) -> BizSampleCustomerService:
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
if not code:
|
||||||
|
raise ValueError("sample_code is required")
|
||||||
|
|
||||||
|
row = get_by_sample_code(db, tenant_no, code)
|
||||||
|
now = datetime.now()
|
||||||
|
if row is None:
|
||||||
|
row = BizSampleCustomerService(
|
||||||
|
id=generate_uuid(),
|
||||||
|
sample_code=code,
|
||||||
|
collection_protocol_id=collection_protocol_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
else:
|
||||||
|
if collection_protocol_id and not row.collection_protocol_id:
|
||||||
|
row.collection_protocol_id = collection_protocol_id
|
||||||
|
row.update_by = user_id
|
||||||
|
row.update_time = now
|
||||||
|
|
||||||
|
existing = _row_field_dict(row)
|
||||||
|
merged = merge_customer_service_fields(existing, incoming)
|
||||||
|
|
||||||
|
for field, dict_type in DICT_TYPE_BY_FIELD.items():
|
||||||
|
if field not in merged:
|
||||||
|
continue
|
||||||
|
value = merged[field]
|
||||||
|
if value is None or normalize_dict_value(value) is None:
|
||||||
|
continue
|
||||||
|
merged[field] = ensure_dict_value(
|
||||||
|
db, tenant_no, dict_type, value, user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in CUSTOMER_SERVICE_FIELD_KEYS:
|
||||||
|
if key in merged:
|
||||||
|
setattr(row, key, merged[key])
|
||||||
|
|
||||||
|
db.flush()
|
||||||
|
return row
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,310 @@
|
|||||||
|
"""样本信息列表快捷登记写入。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud import sample_customer_service as sample_customer_service_crud
|
||||||
|
from app.crud import sample_insurance as sample_insurance_crud
|
||||||
|
from app.crud import sample_storage_cert as sample_storage_cert_crud
|
||||||
|
from app.crud.storage_release_review import _get_user_name
|
||||||
|
from app.services.sample_customer_service_register import has_any_customer_service_payload
|
||||||
|
from app.models.models import (
|
||||||
|
BizCollectionProtocol,
|
||||||
|
BizProtocolSample,
|
||||||
|
BizStorageReleaseReview,
|
||||||
|
)
|
||||||
|
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
|
||||||
|
from app.services.sample_info_quick_register import (
|
||||||
|
apply_quick_storage_payload,
|
||||||
|
unique_sample_items,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _protocol_accessible(db: Session, protocol_id: str, tenant_no: str) -> bool:
|
||||||
|
"""登记写入只校验本租户协议存在,不再按数据权限拦截。"""
|
||||||
|
row = (
|
||||||
|
db.query(BizCollectionProtocol.id)
|
||||||
|
.filter(
|
||||||
|
BizCollectionProtocol.id == protocol_id,
|
||||||
|
BizCollectionProtocol.del_flag == 0,
|
||||||
|
BizCollectionProtocol.tenant_no == tenant_no,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _donation_accessible(db: Session, donation_id: str, tenant_no: str) -> bool:
|
||||||
|
"""登记写入只校验本租户捐赠单存在,不再按数据权限拦截。"""
|
||||||
|
row = (
|
||||||
|
db.query(BizResearchDonation.id)
|
||||||
|
.filter(
|
||||||
|
BizResearchDonation.id == donation_id,
|
||||||
|
BizResearchDonation.del_flag == 0,
|
||||||
|
BizResearchDonation.tenant_no == tenant_no,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_latest_review(
|
||||||
|
rows: List[BizStorageReleaseReview],
|
||||||
|
) -> Optional[BizStorageReleaseReview]:
|
||||||
|
best = None
|
||||||
|
best_t = None
|
||||||
|
for rev in rows:
|
||||||
|
cur_t = rev.storage_time or rev.update_time or rev.create_time
|
||||||
|
if best is None:
|
||||||
|
best, best_t = rev, cur_t
|
||||||
|
continue
|
||||||
|
if cur_t is None:
|
||||||
|
continue
|
||||||
|
if best_t is None or cur_t > best_t:
|
||||||
|
best, best_t = rev, cur_t
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def batch_handle_storage_certs(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
items: List[Dict[str, str]],
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: str,
|
||||||
|
handle_time: Optional[datetime],
|
||||||
|
handler_user_id: Optional[str],
|
||||||
|
attachments: str,
|
||||||
|
remark: str,
|
||||||
|
mail_cert_time: Optional[datetime] = None,
|
||||||
|
update_mail_cert_time: bool = False,
|
||||||
|
do_commit: bool = True,
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
pairs = unique_sample_items(items)
|
||||||
|
if not pairs:
|
||||||
|
raise ValueError("请选择样本")
|
||||||
|
success = 0
|
||||||
|
fail = 0
|
||||||
|
for sample_code, protocol_id in pairs:
|
||||||
|
if not protocol_id or not _protocol_accessible(db, protocol_id, tenant_no):
|
||||||
|
fail += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
sample_storage_cert_crud.upsert_handle(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
sample_code=sample_code,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=handle_time,
|
||||||
|
handler_user_id=handler_user_id,
|
||||||
|
cert_image_urls=attachments,
|
||||||
|
remark=remark,
|
||||||
|
mail_cert_time=mail_cert_time,
|
||||||
|
update_mail_cert_time=update_mail_cert_time,
|
||||||
|
)
|
||||||
|
success += 1
|
||||||
|
except ValueError:
|
||||||
|
fail += 1
|
||||||
|
if do_commit:
|
||||||
|
db.commit()
|
||||||
|
return {"success": success, "fail": fail}
|
||||||
|
|
||||||
|
|
||||||
|
def batch_handle_insurance(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
items: List[Dict[str, str]],
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: str,
|
||||||
|
handle_time: Optional[datetime],
|
||||||
|
handler_user_id: Optional[str],
|
||||||
|
attachments: str,
|
||||||
|
remark: str,
|
||||||
|
policy_send_time: Optional[datetime] = None,
|
||||||
|
update_policy_send_time: bool = False,
|
||||||
|
do_commit: bool = True,
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
pairs = unique_sample_items(items)
|
||||||
|
if not pairs:
|
||||||
|
raise ValueError("请选择样本")
|
||||||
|
success = 0
|
||||||
|
fail = 0
|
||||||
|
for sample_code, protocol_id in pairs:
|
||||||
|
if not protocol_id or not _protocol_accessible(db, protocol_id, tenant_no):
|
||||||
|
fail += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
sample_insurance_crud.upsert_handle(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
sample_code=sample_code,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
user_id=user_id,
|
||||||
|
handle_time=handle_time,
|
||||||
|
handler_user_id=handler_user_id,
|
||||||
|
insurance_attachments=attachments,
|
||||||
|
handle_remark=remark,
|
||||||
|
policy_send_time=policy_send_time,
|
||||||
|
update_policy_send_time=update_policy_send_time,
|
||||||
|
)
|
||||||
|
success += 1
|
||||||
|
except ValueError:
|
||||||
|
fail += 1
|
||||||
|
if do_commit:
|
||||||
|
db.commit()
|
||||||
|
return {"success": success, "fail": fail}
|
||||||
|
|
||||||
|
|
||||||
|
def batch_register_customer_service(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
items: List[Dict[str, str]],
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: str,
|
||||||
|
incoming: Dict[str, Any],
|
||||||
|
do_commit: bool = True,
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
if not has_any_customer_service_payload(incoming):
|
||||||
|
raise ValueError("请至少填写一项客户服务信息")
|
||||||
|
pairs = unique_sample_items(items)
|
||||||
|
if not pairs:
|
||||||
|
raise ValueError("请选择样本")
|
||||||
|
success = 0
|
||||||
|
fail = 0
|
||||||
|
for sample_code, protocol_id in pairs:
|
||||||
|
if not protocol_id or not _protocol_accessible(db, protocol_id, tenant_no):
|
||||||
|
fail += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
sample_customer_service_crud.upsert_by_sample_code(
|
||||||
|
db,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
sample_code=sample_code,
|
||||||
|
collection_protocol_id=protocol_id,
|
||||||
|
user_id=user_id,
|
||||||
|
incoming=incoming,
|
||||||
|
)
|
||||||
|
success += 1
|
||||||
|
except ValueError:
|
||||||
|
fail += 1
|
||||||
|
if do_commit:
|
||||||
|
db.commit()
|
||||||
|
return {"success": success, "fail": fail}
|
||||||
|
|
||||||
|
|
||||||
|
def batch_quick_register_storage(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
items: List[Dict[str, str]],
|
||||||
|
reception_source: str,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: str,
|
||||||
|
handle_time: Optional[datetime],
|
||||||
|
handler_user_id: Optional[str],
|
||||||
|
attachments: str,
|
||||||
|
remark: str,
|
||||||
|
do_commit: bool = True,
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
source = (reception_source or "").strip()
|
||||||
|
if source not in ("cell_storage", "research_donation"):
|
||||||
|
raise ValueError("来源无效")
|
||||||
|
fields = apply_quick_storage_payload(
|
||||||
|
handle_time=handle_time,
|
||||||
|
handler_user_id=handler_user_id,
|
||||||
|
attachments=attachments,
|
||||||
|
remark=remark,
|
||||||
|
)
|
||||||
|
pairs = unique_sample_items(items)
|
||||||
|
if not pairs:
|
||||||
|
raise ValueError("请选择样本")
|
||||||
|
|
||||||
|
handler_name = _get_user_name(db, fields["storage_by"] or user_id)
|
||||||
|
success = 0
|
||||||
|
fail = 0
|
||||||
|
now = datetime.now()
|
||||||
|
for sample_code, protocol_id in pairs:
|
||||||
|
if source == "cell_storage":
|
||||||
|
if not protocol_id or not _protocol_accessible(db, protocol_id, tenant_no):
|
||||||
|
fail += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if not protocol_id or not _donation_accessible(db, protocol_id, tenant_no):
|
||||||
|
fail += 1
|
||||||
|
continue
|
||||||
|
rows = (
|
||||||
|
db.query(BizStorageReleaseReview)
|
||||||
|
.filter(
|
||||||
|
BizStorageReleaseReview.tenant_no == tenant_no,
|
||||||
|
BizStorageReleaseReview.del_flag == 0,
|
||||||
|
BizStorageReleaseReview.reception_source == source,
|
||||||
|
BizStorageReleaseReview.sample_code == sample_code,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
rev = _pick_latest_review(rows)
|
||||||
|
if rev is None:
|
||||||
|
rev = BizStorageReleaseReview(
|
||||||
|
id=generate_uuid(),
|
||||||
|
sample_code=sample_code,
|
||||||
|
reception_source=source,
|
||||||
|
protocol_id=protocol_id,
|
||||||
|
storage_status="yrk",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
update_by=user_id,
|
||||||
|
create_time=now,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(rev)
|
||||||
|
rev.storage_status = fields["storage_status"]
|
||||||
|
rev.storage_time = fields["storage_time"]
|
||||||
|
rev.storage_by = fields["storage_by"]
|
||||||
|
rev.storage_by_name = handler_name
|
||||||
|
rev.remark = fields["remark"] or None
|
||||||
|
if fields["attachments"]:
|
||||||
|
rev.review_attachments = fields["attachments"]
|
||||||
|
rev.update_by = user_id
|
||||||
|
rev.update_time = now
|
||||||
|
db.add(rev)
|
||||||
|
|
||||||
|
ymd = fields["storage_in_date"]
|
||||||
|
if source == "cell_storage":
|
||||||
|
ps = (
|
||||||
|
db.query(BizProtocolSample)
|
||||||
|
.filter(
|
||||||
|
BizProtocolSample.sample_code == sample_code,
|
||||||
|
BizProtocolSample.tenant_no == tenant_no,
|
||||||
|
BizProtocolSample.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if ps:
|
||||||
|
ps.sample_status = "yrk"
|
||||||
|
ps.storage_in_date = ymd
|
||||||
|
ps.update_time = now
|
||||||
|
db.add(ps)
|
||||||
|
else:
|
||||||
|
ds = (
|
||||||
|
db.query(BizResearchDonationSample)
|
||||||
|
.filter(
|
||||||
|
BizResearchDonationSample.sample_code == sample_code,
|
||||||
|
BizResearchDonationSample.tenant_no == tenant_no,
|
||||||
|
BizResearchDonationSample.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if ds:
|
||||||
|
ds.sample_status = "yrk"
|
||||||
|
ds.storage_in_date = ymd
|
||||||
|
ds.update_time = now
|
||||||
|
db.add(ds)
|
||||||
|
success += 1
|
||||||
|
if do_commit:
|
||||||
|
db.commit()
|
||||||
|
return {"success": success, "fail": fail}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""样本保险办理 CRUD。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
from app.models.models import BizSampleInsurance
|
||||||
|
from app.services.sample_info_progress import apply_handle_payload, insurance_handled
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_dt(t: Optional[datetime]) -> str:
|
||||||
|
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_sample_code(
|
||||||
|
db: Session, tenant_no: str, sample_code: str
|
||||||
|
) -> Optional[BizSampleInsurance]:
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
if not code:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
db.query(BizSampleInsurance)
|
||||||
|
.filter(
|
||||||
|
BizSampleInsurance.tenant_no == tenant_no,
|
||||||
|
BizSampleInsurance.sample_code == code,
|
||||||
|
BizSampleInsurance.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_map_by_sample_codes(
|
||||||
|
db: Session, tenant_no: str, codes: List[str]
|
||||||
|
) -> Dict[str, BizSampleInsurance]:
|
||||||
|
cleaned = list({(c or "").strip() for c in codes if (c or "").strip()})
|
||||||
|
if not cleaned:
|
||||||
|
return {}
|
||||||
|
rows = (
|
||||||
|
db.query(BizSampleInsurance)
|
||||||
|
.filter(
|
||||||
|
BizSampleInsurance.tenant_no == tenant_no,
|
||||||
|
BizSampleInsurance.sample_code.in_(cleaned),
|
||||||
|
BizSampleInsurance.del_flag == 0,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return {row.sample_code: row for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_handle(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
sample_code: str,
|
||||||
|
collection_protocol_id: Optional[str],
|
||||||
|
user_id: Optional[str],
|
||||||
|
handle_time: Optional[datetime],
|
||||||
|
handler_user_id: Optional[str],
|
||||||
|
insurance_attachments: str = "",
|
||||||
|
handle_remark: str = "",
|
||||||
|
policy_send_time: Optional[datetime] = None,
|
||||||
|
update_policy_send_time: bool = False,
|
||||||
|
) -> BizSampleInsurance:
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
if not code:
|
||||||
|
raise ValueError("sample_code is required")
|
||||||
|
|
||||||
|
payload = apply_handle_payload(
|
||||||
|
handle_time=handle_time,
|
||||||
|
handler_user_id=handler_user_id,
|
||||||
|
attachments=insurance_attachments or "",
|
||||||
|
remark=handle_remark or "",
|
||||||
|
)
|
||||||
|
now = datetime.now()
|
||||||
|
row = get_by_sample_code(db, tenant_no, code)
|
||||||
|
if row is None:
|
||||||
|
row = BizSampleInsurance(
|
||||||
|
id=generate_uuid(),
|
||||||
|
sample_code=code,
|
||||||
|
collection_protocol_id=(collection_protocol_id or "").strip() or None,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
order_num=0,
|
||||||
|
todo_status="0",
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
else:
|
||||||
|
if collection_protocol_id and not row.collection_protocol_id:
|
||||||
|
row.collection_protocol_id = (collection_protocol_id or "").strip() or None
|
||||||
|
row.update_by = user_id
|
||||||
|
row.update_time = now
|
||||||
|
|
||||||
|
row.handle_time = payload["handle_time"]
|
||||||
|
row.handler_user_id = payload["handler_user_id"]
|
||||||
|
row.todo_status = payload["make_or_todo_status"]
|
||||||
|
row.handle_date = payload["legacy_date"] or None
|
||||||
|
row.insurance_attachments = payload["attachments"] or None
|
||||||
|
row.handle_remark = payload["remark"] or None
|
||||||
|
if update_policy_send_time:
|
||||||
|
row.policy_send_time = policy_send_time
|
||||||
|
|
||||||
|
db.flush()
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def row_to_dict(row: BizSampleInsurance) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"sampleCode": row.sample_code or "",
|
||||||
|
"collectionProtocolId": row.collection_protocol_id or "",
|
||||||
|
"todoStatus": row.todo_status or "0",
|
||||||
|
"handleDate": (row.handle_date or "").strip() if row.handle_date else "",
|
||||||
|
"handleTime": _fmt_dt(row.handle_time) if getattr(row, "handle_time", None) else "",
|
||||||
|
"handlerUserId": getattr(row, "handler_user_id", None) or "",
|
||||||
|
"handleRemark": row.handle_remark or "",
|
||||||
|
"insuranceAttachments": row.insurance_attachments or "",
|
||||||
|
"policySendTime": _fmt_dt(row.policy_send_time)
|
||||||
|
if getattr(row, "policy_send_time", None)
|
||||||
|
else "",
|
||||||
|
"insuranceHandled": insurance_handled(row),
|
||||||
|
"tenantNo": row.tenant_no,
|
||||||
|
"orderNum": row.order_num if row.order_num is not None else 0,
|
||||||
|
"remark": row.remark or "",
|
||||||
|
"createTime": _fmt_dt(row.create_time),
|
||||||
|
"updateTime": _fmt_dt(row.update_time),
|
||||||
|
}
|
||||||
@@ -48,6 +48,18 @@ def _fmt_dt(t: Optional[datetime]) -> str:
|
|||||||
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
|
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_pending_list_items(items: List[Dict[str, Any]]) -> None:
|
||||||
|
"""待接收:etaTime 升序(空值最后),同 ETA 时 sampleCode 倒序。"""
|
||||||
|
items.sort(key=lambda x: (x.get("sampleCode") or ""), reverse=True)
|
||||||
|
items.sort(key=lambda x: (not x.get("etaTime"), x.get("etaTime") or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_received_list_items(items: List[Dict[str, Any]]) -> None:
|
||||||
|
"""已接收:receptionTime 倒序,同接收时间时 sampleCode 倒序。"""
|
||||||
|
items.sort(key=lambda x: (x.get("sampleCode") or ""), reverse=True)
|
||||||
|
items.sort(key=lambda x: (x.get("receptionTime") or ""), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
|
def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
|
||||||
val = (value or "").strip()
|
val = (value or "").strip()
|
||||||
if len(val) == 10:
|
if len(val) == 10:
|
||||||
@@ -501,6 +513,7 @@ def list_pending_protocol_samples(
|
|||||||
BizProtocolSampleTransport.eta_time.is_(None),
|
BizProtocolSampleTransport.eta_time.is_(None),
|
||||||
BizProtocolSampleTransport.eta_time.asc(),
|
BizProtocolSampleTransport.eta_time.asc(),
|
||||||
BizProtocolSample.create_time.asc(),
|
BizProtocolSample.create_time.asc(),
|
||||||
|
BizProtocolSample.sample_code.desc(),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@@ -516,13 +529,14 @@ def list_pending_protocol_samples(
|
|||||||
BizResearchDonationSampleTransport.eta_time.is_(None),
|
BizResearchDonationSampleTransport.eta_time.is_(None),
|
||||||
BizResearchDonationSampleTransport.eta_time.asc(),
|
BizResearchDonationSampleTransport.eta_time.asc(),
|
||||||
BizResearchDonationSample.create_time.asc(),
|
BizResearchDonationSample.create_time.asc(),
|
||||||
|
BizResearchDonationSample.sample_code.desc(),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
all_items.extend(_donation_rows_to_sample_list_items(db, donation_rows, tenant_no))
|
all_items.extend(_donation_rows_to_sample_list_items(db, donation_rows, tenant_no))
|
||||||
|
|
||||||
# 统一按 etaTime 排序(空值排后)
|
# 统一按 etaTime 升序(空值排后),同 ETA 时 sampleCode 倒序
|
||||||
all_items.sort(key=lambda x: (not x.get("etaTime"), x.get("etaTime") or ""))
|
_sort_pending_list_items(all_items)
|
||||||
|
|
||||||
start_eta = _normalize_date_bound(eta_time_start) if eta_time_start else ""
|
start_eta = _normalize_date_bound(eta_time_start) if eta_time_start else ""
|
||||||
end_eta = _normalize_date_bound(eta_time_end, end_of_day=True) if eta_time_end else ""
|
end_eta = _normalize_date_bound(eta_time_end, end_of_day=True) if eta_time_end else ""
|
||||||
@@ -628,7 +642,10 @@ def list_received_protocol_samples(
|
|||||||
page = max(1, page)
|
page = max(1, page)
|
||||||
page_size = min(max(1, page_size), 100)
|
page_size = min(max(1, page_size), 100)
|
||||||
receptions = (
|
receptions = (
|
||||||
q_rec.order_by(BizSampleReception.reception_time.desc())
|
q_rec.order_by(
|
||||||
|
BizSampleReception.reception_time.desc(),
|
||||||
|
BizSampleReception.sample_code.desc(),
|
||||||
|
)
|
||||||
.offset((page - 1) * page_size)
|
.offset((page - 1) * page_size)
|
||||||
.limit(page_size)
|
.limit(page_size)
|
||||||
.all()
|
.all()
|
||||||
@@ -681,8 +698,8 @@ def list_received_protocol_samples(
|
|||||||
donation_ordered = [donation_row_by_sample[sid] for sid in donation_sample_ids if sid in donation_row_by_sample]
|
donation_ordered = [donation_row_by_sample[sid] for sid in donation_sample_ids if sid in donation_row_by_sample]
|
||||||
items.extend(_donation_rows_to_sample_list_items(db, donation_ordered, tenant_no, reception_map=donation_reception_map))
|
items.extend(_donation_rows_to_sample_list_items(db, donation_ordered, tenant_no, reception_map=donation_reception_map))
|
||||||
|
|
||||||
# 按 reception_time DESC 排序
|
# 按 receptionTime 倒序,同时间时 sampleCode 倒序
|
||||||
items.sort(key=lambda x: x.get("receptionTime") or "", reverse=True)
|
_sort_received_list_items(items)
|
||||||
return items, int(total)
|
return items, int(total)
|
||||||
|
|
||||||
|
|
||||||
@@ -1114,12 +1131,6 @@ def update_sample_reception(
|
|||||||
rec.update_by = user_id
|
rec.update_by = user_id
|
||||||
rec.update_time = datetime.now()
|
rec.update_time = datetime.now()
|
||||||
|
|
||||||
check_result_svc.soft_delete_reception_check_results(
|
|
||||||
db,
|
|
||||||
reception_id=reception_id,
|
|
||||||
tenant_no=tenant_no,
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
check_result_svc.save_reception_check_results(
|
check_result_svc.save_reception_check_results(
|
||||||
db,
|
db,
|
||||||
reception_id=reception_id,
|
reception_id=reception_id,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from typing import Any, Dict, List, Optional
|
|||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.utils import generate_uuid
|
|
||||||
from app.crud import sample_reception_check_item as check_item_svc
|
from app.crud import sample_reception_check_item as check_item_svc
|
||||||
|
from app.crud.child_row_sync import sync_child_rows
|
||||||
from app.models.models import (
|
from app.models.models import (
|
||||||
BizSampleReception,
|
BizSampleReception,
|
||||||
BizSampleReceptionCheckItem,
|
BizSampleReceptionCheckItem,
|
||||||
@@ -105,7 +105,7 @@ def soft_delete_reception_check_results(
|
|||||||
tenant_no: str,
|
tenant_no: str,
|
||||||
user_id: Optional[str],
|
user_id: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""软删除某次接收的全部检查项结果(编辑接收时先清空再重写)。"""
|
"""软删除某次接收的全部检查项结果。删除接收记录时使用。"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
(
|
(
|
||||||
db.query(BizSampleReceptionCheckResult)
|
db.query(BizSampleReceptionCheckResult)
|
||||||
@@ -143,26 +143,56 @@ def save_reception_check_results(
|
|||||||
if unknown:
|
if unknown:
|
||||||
raise ValueError("存在无效的检查项")
|
raise ValueError("存在无效的检查项")
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
payload = []
|
||||||
for item in active_items:
|
for item in active_items:
|
||||||
item_id = str(item["id"])
|
item_id = str(item["id"])
|
||||||
checked = submitted.get(item_id, False)
|
payload.append(
|
||||||
row = BizSampleReceptionCheckResult(
|
{
|
||||||
id=generate_uuid(),
|
"check_item_id": item_id,
|
||||||
|
"item_code": item["itemCode"],
|
||||||
|
"item_name": item["itemName"],
|
||||||
|
"checked": 1 if submitted.get(item_id, False) else 0,
|
||||||
|
"order_num": item.get("orderNum") or 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_row(row_id):
|
||||||
|
return BizSampleReceptionCheckResult(
|
||||||
|
id=row_id,
|
||||||
reception_id=reception_id,
|
reception_id=reception_id,
|
||||||
check_item_id=item_id,
|
check_item_id="",
|
||||||
item_code=item["itemCode"],
|
item_code="",
|
||||||
item_name=item["itemName"],
|
item_name="",
|
||||||
checked=1 if checked else 0,
|
checked=0,
|
||||||
tenant_no=tenant_no,
|
tenant_no=tenant_no,
|
||||||
order_num=item.get("orderNum") or 0,
|
|
||||||
create_by=user_id,
|
create_by=user_id,
|
||||||
create_dept_id=create_dept_id or "",
|
create_dept_id=create_dept_id or "",
|
||||||
create_time=now,
|
create_time=now,
|
||||||
update_by=user_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
)
|
||||||
db.add(row)
|
|
||||||
|
def apply_row(row, item, _idx, _now):
|
||||||
|
row.check_item_id = item["check_item_id"]
|
||||||
|
row.item_code = item["item_code"]
|
||||||
|
row.item_name = item["item_name"]
|
||||||
|
row.checked = item["checked"]
|
||||||
|
row.order_num = item["order_num"]
|
||||||
|
|
||||||
|
sync_child_rows(
|
||||||
|
db,
|
||||||
|
BizSampleReceptionCheckResult,
|
||||||
|
parent_attr="reception_id",
|
||||||
|
parent_id=reception_id,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
items=payload,
|
||||||
|
user_id=user_id,
|
||||||
|
now=now,
|
||||||
|
build_row=build_row,
|
||||||
|
apply_fields=apply_row,
|
||||||
|
natural_key=lambda item: item["check_item_id"],
|
||||||
|
row_natural_key=lambda row: row.check_item_id or "",
|
||||||
|
duplicate_message="检查项重复",
|
||||||
|
foreign_message="检查结果不属于当前接收记录",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def sync_legacy_columns_from_results(
|
def sync_legacy_columns_from_results(
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""样本储存证办理 CRUD。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
from app.models.models import BizSampleStorageCert
|
||||||
|
from app.services.sample_info_progress import apply_handle_payload, storage_cert_handled
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_dt(t: Optional[datetime]) -> str:
|
||||||
|
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_sample_code(
|
||||||
|
db: Session, tenant_no: str, sample_code: str
|
||||||
|
) -> Optional[BizSampleStorageCert]:
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
if not code:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
db.query(BizSampleStorageCert)
|
||||||
|
.filter(
|
||||||
|
BizSampleStorageCert.tenant_no == tenant_no,
|
||||||
|
BizSampleStorageCert.sample_code == code,
|
||||||
|
BizSampleStorageCert.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_map_by_sample_codes(
|
||||||
|
db: Session, tenant_no: str, codes: List[str]
|
||||||
|
) -> Dict[str, BizSampleStorageCert]:
|
||||||
|
cleaned = list({(c or "").strip() for c in codes if (c or "").strip()})
|
||||||
|
if not cleaned:
|
||||||
|
return {}
|
||||||
|
rows = (
|
||||||
|
db.query(BizSampleStorageCert)
|
||||||
|
.filter(
|
||||||
|
BizSampleStorageCert.tenant_no == tenant_no,
|
||||||
|
BizSampleStorageCert.sample_code.in_(cleaned),
|
||||||
|
BizSampleStorageCert.del_flag == 0,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return {row.sample_code: row for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_handle(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
sample_code: str,
|
||||||
|
collection_protocol_id: Optional[str],
|
||||||
|
user_id: Optional[str],
|
||||||
|
handle_time: Optional[datetime],
|
||||||
|
handler_user_id: Optional[str],
|
||||||
|
cert_image_urls: str = "",
|
||||||
|
remark: str = "",
|
||||||
|
mail_cert_time: Optional[datetime] = None,
|
||||||
|
update_mail_cert_time: bool = False,
|
||||||
|
) -> BizSampleStorageCert:
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
if not code:
|
||||||
|
raise ValueError("sample_code is required")
|
||||||
|
|
||||||
|
payload = apply_handle_payload(
|
||||||
|
handle_time=handle_time,
|
||||||
|
handler_user_id=handler_user_id,
|
||||||
|
attachments=cert_image_urls or "",
|
||||||
|
remark=remark or "",
|
||||||
|
)
|
||||||
|
now = datetime.now()
|
||||||
|
row = get_by_sample_code(db, tenant_no, code)
|
||||||
|
if row is None:
|
||||||
|
row = BizSampleStorageCert(
|
||||||
|
id=generate_uuid(),
|
||||||
|
sample_code=code,
|
||||||
|
collection_protocol_id=(collection_protocol_id or "").strip() or None,
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
order_num=0,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
else:
|
||||||
|
if collection_protocol_id and not row.collection_protocol_id:
|
||||||
|
row.collection_protocol_id = (collection_protocol_id or "").strip() or None
|
||||||
|
row.update_by = user_id
|
||||||
|
row.update_time = now
|
||||||
|
|
||||||
|
row.handle_time = payload["handle_time"]
|
||||||
|
row.handler_user_id = payload["handler_user_id"]
|
||||||
|
row.make_status = payload["make_or_todo_status"]
|
||||||
|
row.make_date = payload["legacy_date"] or None
|
||||||
|
row.cert_image_urls = payload["attachments"] or None
|
||||||
|
row.remark = payload["remark"] or None
|
||||||
|
if update_mail_cert_time:
|
||||||
|
row.mail_cert_time = mail_cert_time
|
||||||
|
|
||||||
|
db.flush()
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def row_to_dict(row: BizSampleStorageCert) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"sampleCode": row.sample_code or "",
|
||||||
|
"collectionProtocolId": row.collection_protocol_id or "",
|
||||||
|
"makeStatus": row.make_status or "0",
|
||||||
|
"certImageUrls": row.cert_image_urls or "",
|
||||||
|
"makeDate": (row.make_date or "").strip() if row.make_date else "",
|
||||||
|
"handleTime": _fmt_dt(row.handle_time) if getattr(row, "handle_time", None) else "",
|
||||||
|
"handlerUserId": getattr(row, "handler_user_id", None) or "",
|
||||||
|
"mailCertTime": _fmt_dt(row.mail_cert_time) if getattr(row, "mail_cert_time", None) else "",
|
||||||
|
"storageCertHandled": storage_cert_handled(row),
|
||||||
|
"tenantNo": row.tenant_no,
|
||||||
|
"orderNum": row.order_num if row.order_num is not None else 0,
|
||||||
|
"remark": row.remark or "",
|
||||||
|
"createTime": _fmt_dt(row.create_time),
|
||||||
|
"updateTime": _fmt_dt(row.update_time),
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
"""样本储存证质检信息 CRUD。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud import sample_reception as reception_svc
|
||||||
|
from app.models.models import (
|
||||||
|
BizCellSampleType,
|
||||||
|
BizCollectionProtocol,
|
||||||
|
BizProtocolPartyCustomer,
|
||||||
|
BizProtocolSample,
|
||||||
|
BizSampleStorageCertQc,
|
||||||
|
)
|
||||||
|
from app.services.sample_storage_cert_qc import (
|
||||||
|
dumps_payload,
|
||||||
|
empty_detail,
|
||||||
|
loads_payload,
|
||||||
|
normalize_payload,
|
||||||
|
resolve_sample_type_key,
|
||||||
|
)
|
||||||
|
from app.services.sample_storage_cert_qc_prefill import collect_prefill
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_dict(
|
||||||
|
row: BizSampleStorageCertQc,
|
||||||
|
*,
|
||||||
|
lab_flow_code: str = "",
|
||||||
|
protocol_code: str = "",
|
||||||
|
mother_name: str = "",
|
||||||
|
sample_type_name: str = "",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"sampleCode": row.sample_code or "",
|
||||||
|
"labFlowCode": lab_flow_code or "",
|
||||||
|
"protocolCode": protocol_code or "",
|
||||||
|
"motherName": mother_name or "",
|
||||||
|
"sampleTypeName": sample_type_name or "",
|
||||||
|
"sampleTypeKey": row.sample_type_key,
|
||||||
|
"collectionProtocolId": row.collection_protocol_id or "",
|
||||||
|
"qcWorkOrderId": row.qc_work_order_id or "",
|
||||||
|
"filledFlag": str(row.filled_flag or "0") == "1",
|
||||||
|
"payload": loads_payload(row.payload),
|
||||||
|
"createTime": row.create_time.isoformat(sep=" ", timespec="seconds") if row.create_time else "",
|
||||||
|
"updateTime": row.update_time.isoformat(sep=" ", timespec="seconds") if row.update_time else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_sample_context(db: Session, tenant_no: str, sample_code: str):
|
||||||
|
code = (sample_code or "").strip()
|
||||||
|
if not code:
|
||||||
|
return None
|
||||||
|
sample = (
|
||||||
|
db.query(BizProtocolSample)
|
||||||
|
.filter(
|
||||||
|
BizProtocolSample.tenant_no == tenant_no,
|
||||||
|
BizProtocolSample.sample_code == code,
|
||||||
|
BizProtocolSample.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not sample:
|
||||||
|
return None
|
||||||
|
type_name = ""
|
||||||
|
if sample.sample_type_id:
|
||||||
|
st = (
|
||||||
|
db.query(BizCellSampleType)
|
||||||
|
.filter(
|
||||||
|
BizCellSampleType.id == sample.sample_type_id,
|
||||||
|
BizCellSampleType.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if st:
|
||||||
|
type_name = st.type_name or ""
|
||||||
|
protocol_code = ""
|
||||||
|
mother_name = ""
|
||||||
|
if sample.collection_protocol_id:
|
||||||
|
proto = (
|
||||||
|
db.query(BizCollectionProtocol)
|
||||||
|
.filter(
|
||||||
|
BizCollectionProtocol.id == sample.collection_protocol_id,
|
||||||
|
BizCollectionProtocol.tenant_no == tenant_no,
|
||||||
|
BizCollectionProtocol.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if proto:
|
||||||
|
protocol_code = proto.protocol_no or ""
|
||||||
|
pc = (
|
||||||
|
db.query(BizProtocolPartyCustomer)
|
||||||
|
.filter(
|
||||||
|
BizProtocolPartyCustomer.collection_protocol_id == sample.collection_protocol_id,
|
||||||
|
BizProtocolPartyCustomer.tenant_no == tenant_no,
|
||||||
|
BizProtocolPartyCustomer.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if pc:
|
||||||
|
mother_name = pc.mother_name or ""
|
||||||
|
lab_flow_code = ""
|
||||||
|
sc = (sample.sample_code or "").strip()
|
||||||
|
if sc:
|
||||||
|
code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(db, tenant_no, [sc])
|
||||||
|
lab_flow_code = code_map.get(sc, "") or ""
|
||||||
|
return sample, type_name, protocol_code, mother_name, lab_flow_code
|
||||||
|
|
||||||
|
|
||||||
|
def _with_prefill(db: Session, tenant_no: str, detail: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
detail["prefill"] = collect_prefill(
|
||||||
|
db,
|
||||||
|
tenant_no,
|
||||||
|
detail.get("sampleCode") or "",
|
||||||
|
detail.get("sampleTypeKey"),
|
||||||
|
)
|
||||||
|
return detail
|
||||||
|
|
||||||
|
|
||||||
|
def get_detail(db: Session, tenant_no: str, sample_code: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""样本不存在返回 None;存在但未填返回 empty_detail。prefill 只补空,不写入已存档 payload。"""
|
||||||
|
ctx = _load_sample_context(db, tenant_no, sample_code)
|
||||||
|
if not ctx:
|
||||||
|
return None
|
||||||
|
sample, type_name, protocol_code, mother_name, lab_flow_code = ctx
|
||||||
|
key = resolve_sample_type_key(type_name)
|
||||||
|
row = (
|
||||||
|
db.query(BizSampleStorageCertQc)
|
||||||
|
.filter(
|
||||||
|
BizSampleStorageCertQc.tenant_no == tenant_no,
|
||||||
|
BizSampleStorageCertQc.sample_code == sample.sample_code,
|
||||||
|
BizSampleStorageCertQc.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
detail = empty_detail(
|
||||||
|
sample_code=sample.sample_code or "",
|
||||||
|
lab_flow_code=lab_flow_code,
|
||||||
|
protocol_code=protocol_code,
|
||||||
|
mother_name=mother_name,
|
||||||
|
sample_type_name=type_name,
|
||||||
|
sample_type_key=key,
|
||||||
|
collection_protocol_id=sample.collection_protocol_id or "",
|
||||||
|
)
|
||||||
|
return _with_prefill(db, tenant_no, detail)
|
||||||
|
detail = _row_to_dict(
|
||||||
|
row,
|
||||||
|
lab_flow_code=lab_flow_code,
|
||||||
|
protocol_code=protocol_code,
|
||||||
|
mother_name=mother_name,
|
||||||
|
sample_type_name=type_name,
|
||||||
|
)
|
||||||
|
return _with_prefill(db, tenant_no, detail)
|
||||||
|
|
||||||
|
|
||||||
|
def upsert(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: str,
|
||||||
|
sample_code: str,
|
||||||
|
payload: Any,
|
||||||
|
qc_work_order_id: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
ctx = _load_sample_context(db, tenant_no, sample_code)
|
||||||
|
if not ctx:
|
||||||
|
raise ValueError("样本不存在")
|
||||||
|
sample, type_name, protocol_code, mother_name, lab_flow_code = ctx
|
||||||
|
key = resolve_sample_type_key(type_name)
|
||||||
|
if not key:
|
||||||
|
raise ValueError("该样本类型不支持填写储存证质检信息")
|
||||||
|
clean = normalize_payload(payload)
|
||||||
|
now = datetime.now()
|
||||||
|
row = (
|
||||||
|
db.query(BizSampleStorageCertQc)
|
||||||
|
.filter(
|
||||||
|
BizSampleStorageCertQc.tenant_no == tenant_no,
|
||||||
|
BizSampleStorageCertQc.sample_code == sample.sample_code,
|
||||||
|
BizSampleStorageCertQc.del_flag == 0,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
row = BizSampleStorageCertQc(
|
||||||
|
id=generate_uuid(),
|
||||||
|
sample_code=sample.sample_code,
|
||||||
|
collection_protocol_id=sample.collection_protocol_id,
|
||||||
|
qc_work_order_id=(qc_work_order_id or None),
|
||||||
|
sample_type_key=key,
|
||||||
|
payload=dumps_payload(clean),
|
||||||
|
filled_flag="1",
|
||||||
|
tenant_no=tenant_no,
|
||||||
|
create_by=user_id,
|
||||||
|
create_dept_id="",
|
||||||
|
create_time=now,
|
||||||
|
update_by=user_id,
|
||||||
|
update_time=now,
|
||||||
|
del_flag=0,
|
||||||
|
)
|
||||||
|
db.add(row)
|
||||||
|
else:
|
||||||
|
row.collection_protocol_id = sample.collection_protocol_id
|
||||||
|
row.sample_type_key = key
|
||||||
|
row.payload = dumps_payload(clean)
|
||||||
|
row.filled_flag = "1"
|
||||||
|
if qc_work_order_id:
|
||||||
|
row.qc_work_order_id = qc_work_order_id
|
||||||
|
row.update_by = user_id
|
||||||
|
row.update_time = now
|
||||||
|
db.commit()
|
||||||
|
db.refresh(row)
|
||||||
|
return _row_to_dict(
|
||||||
|
row,
|
||||||
|
lab_flow_code=lab_flow_code,
|
||||||
|
protocol_code=protocol_code,
|
||||||
|
mother_name=mother_name,
|
||||||
|
sample_type_name=type_name,
|
||||||
|
)
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"""顺丰月结列表、结算、补填。只处理 transport_mode = sfkd。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.data_scope import apply_data_scope
|
||||||
|
from app.crud.sample_freight import _fmt_dt, _normalize_date_bound, _user_nick_name_map
|
||||||
|
from app.models.models import BizSampleFreight
|
||||||
|
from app.services.sf_express_settlement import (
|
||||||
|
SF_TRANSPORT_MODE,
|
||||||
|
SETTLE_SETTLED,
|
||||||
|
assert_settle_request,
|
||||||
|
freight_amount_label,
|
||||||
|
plan_fill_amount,
|
||||||
|
plan_settle,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _base_query(db: Session, *, tenant_no: str, user_info: Optional[dict]):
|
||||||
|
query = db.query(BizSampleFreight).filter(
|
||||||
|
BizSampleFreight.tenant_no == tenant_no,
|
||||||
|
BizSampleFreight.del_flag == 0,
|
||||||
|
BizSampleFreight.transport_mode == SF_TRANSPORT_MODE,
|
||||||
|
)
|
||||||
|
return apply_data_scope(query, BizSampleFreight, user_info, db, tenant_no)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_filters(
|
||||||
|
query,
|
||||||
|
*,
|
||||||
|
waybill_no: Optional[str],
|
||||||
|
settle_status: Optional[str],
|
||||||
|
create_time_from: Optional[str],
|
||||||
|
create_time_to: Optional[str],
|
||||||
|
):
|
||||||
|
waybill = (waybill_no or "").strip()
|
||||||
|
if waybill:
|
||||||
|
query = query.filter(BizSampleFreight.waybill_no.like(f"%{waybill}%"))
|
||||||
|
status = (settle_status or "").strip()
|
||||||
|
if status:
|
||||||
|
query = query.filter(BizSampleFreight.settle_status == status)
|
||||||
|
if create_time_from and str(create_time_from).strip():
|
||||||
|
query = query.filter(
|
||||||
|
BizSampleFreight.create_time
|
||||||
|
>= _normalize_date_bound(str(create_time_from).strip())
|
||||||
|
)
|
||||||
|
if create_time_to and str(create_time_to).strip():
|
||||||
|
query = query.filter(
|
||||||
|
BizSampleFreight.create_time
|
||||||
|
<= _normalize_date_bound(str(create_time_to).strip(), end_of_day=True)
|
||||||
|
)
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
def _to_row(obj: BizSampleFreight, create_by_name: str) -> Dict[str, Any]:
|
||||||
|
amount = obj.freight_amount
|
||||||
|
status = obj.settle_status or ""
|
||||||
|
return {
|
||||||
|
"id": obj.id,
|
||||||
|
"waybillNo": obj.waybill_no or "",
|
||||||
|
"createBy": obj.create_by or "",
|
||||||
|
"createByName": create_by_name or obj.create_by or "",
|
||||||
|
"createTime": _fmt_dt(obj.create_time),
|
||||||
|
"settleStatus": status,
|
||||||
|
"settleStatusLabel": "已结算" if status == SETTLE_SETTLED else "未结算",
|
||||||
|
"freightAmount": float(amount) if amount is not None else None,
|
||||||
|
"freightAmountLabel": freight_amount_label(obj.transport_mode, amount),
|
||||||
|
"settleTime": _fmt_dt(obj.settle_time) if obj.settle_time else "",
|
||||||
|
"transportMode": obj.transport_mode or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_sf_settlements(
|
||||||
|
db,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
user_info: Optional[dict],
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 10,
|
||||||
|
waybill_no: Optional[str] = None,
|
||||||
|
settle_status: Optional[str] = None,
|
||||||
|
create_time_from: Optional[str] = None,
|
||||||
|
create_time_to: Optional[str] = None,
|
||||||
|
) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
|
page = max(1, int(page or 1))
|
||||||
|
page_size = max(1, min(100, int(page_size or 10)))
|
||||||
|
query = _apply_filters(
|
||||||
|
_base_query(db, tenant_no=tenant_no, user_info=user_info),
|
||||||
|
waybill_no=waybill_no,
|
||||||
|
settle_status=settle_status,
|
||||||
|
create_time_from=create_time_from,
|
||||||
|
create_time_to=create_time_to,
|
||||||
|
)
|
||||||
|
total = query.count()
|
||||||
|
rows = (
|
||||||
|
query.order_by(BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc())
|
||||||
|
.offset((page - 1) * page_size)
|
||||||
|
.limit(page_size)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
names = _user_nick_name_map(db, [row.create_by for row in rows if row.create_by])
|
||||||
|
return [_to_row(row, names.get(row.create_by or "", "")) for row in rows], total
|
||||||
|
|
||||||
|
|
||||||
|
def list_sf_settlements_for_export(
|
||||||
|
db,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
user_info: Optional[dict],
|
||||||
|
waybill_no: Optional[str] = None,
|
||||||
|
settle_status: Optional[str] = None,
|
||||||
|
create_time_from: Optional[str] = None,
|
||||||
|
create_time_to: Optional[str] = None,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
query = _apply_filters(
|
||||||
|
_base_query(db, tenant_no=tenant_no, user_info=user_info),
|
||||||
|
waybill_no=waybill_no,
|
||||||
|
settle_status=settle_status,
|
||||||
|
create_time_from=create_time_from,
|
||||||
|
create_time_to=create_time_to,
|
||||||
|
)
|
||||||
|
rows = query.order_by(
|
||||||
|
BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc()
|
||||||
|
).all()
|
||||||
|
if not rows:
|
||||||
|
raise ValueError("没有可导出的数据")
|
||||||
|
names = _user_nick_name_map(db, [row.create_by for row in rows if row.create_by])
|
||||||
|
return [_to_row(row, names.get(row.create_by or "", "")) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _lock_rows(
|
||||||
|
db, *, tenant_no: str, user_info: Optional[dict], ids: List[str]
|
||||||
|
) -> List[BizSampleFreight]:
|
||||||
|
wanted = [str(item or "").strip() for item in ids if str(item or "").strip()]
|
||||||
|
rows = (
|
||||||
|
_base_query(db, tenant_no=tenant_no, user_info=user_info)
|
||||||
|
.filter(BizSampleFreight.id.in_(wanted))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if len(rows) != len(set(wanted)):
|
||||||
|
raise ValueError("运费批次不存在")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def settle_sf_freights(
|
||||||
|
db,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: Optional[str],
|
||||||
|
user_info: Optional[dict],
|
||||||
|
ids: List[str],
|
||||||
|
freight_amount: Any = None,
|
||||||
|
):
|
||||||
|
assert_settle_request(ids, freight_amount)
|
||||||
|
rows = _lock_rows(db, tenant_no=tenant_no, user_info=user_info, ids=ids)
|
||||||
|
now = datetime.now()
|
||||||
|
submitted = None if len(ids) != 1 else freight_amount
|
||||||
|
for obj in rows:
|
||||||
|
if obj.transport_mode != SF_TRANSPORT_MODE:
|
||||||
|
raise ValueError("只能结算顺丰快递运费")
|
||||||
|
if (obj.settle_status or "") == SETTLE_SETTLED:
|
||||||
|
raise ValueError("该运费已结算")
|
||||||
|
amount, status, settle_time = plan_settle(
|
||||||
|
current_amount=obj.freight_amount, submitted_amount=submitted, now=now
|
||||||
|
)
|
||||||
|
obj.freight_amount = amount
|
||||||
|
obj.settle_status = status
|
||||||
|
obj.settle_time = settle_time
|
||||||
|
obj.update_by = user_id
|
||||||
|
obj.update_time = now
|
||||||
|
db.commit()
|
||||||
|
return {"ids": [row.id for row in rows]}
|
||||||
|
|
||||||
|
|
||||||
|
def fill_sf_freight_amount(
|
||||||
|
db,
|
||||||
|
*,
|
||||||
|
tenant_no: str,
|
||||||
|
user_id: Optional[str],
|
||||||
|
user_info: Optional[dict],
|
||||||
|
freight_id: str,
|
||||||
|
freight_amount: Any,
|
||||||
|
):
|
||||||
|
rows = _lock_rows(db, tenant_no=tenant_no, user_info=user_info, ids=[freight_id])
|
||||||
|
obj = rows[0]
|
||||||
|
if obj.transport_mode != SF_TRANSPORT_MODE:
|
||||||
|
raise ValueError("只能补填顺丰快递运费")
|
||||||
|
obj.freight_amount = plan_fill_amount(
|
||||||
|
current_amount=obj.freight_amount,
|
||||||
|
settle_status=obj.settle_status,
|
||||||
|
submitted_amount=freight_amount,
|
||||||
|
)
|
||||||
|
obj.update_by = user_id
|
||||||
|
obj.update_time = datetime.now()
|
||||||
|
db.commit()
|
||||||
|
return {"id": obj.id}
|
||||||
@@ -741,6 +741,30 @@ def _search_slot_select_matched_ids(
|
|||||||
return matched_ids
|
return matched_ids
|
||||||
|
|
||||||
|
|
||||||
|
def search_slot_ids_by_keyword(
|
||||||
|
db: Session,
|
||||||
|
tenant_no: str,
|
||||||
|
keyword: str,
|
||||||
|
*,
|
||||||
|
max_results: int = SLOT_SELECT_KEYWORD_SCAN_LIMIT,
|
||||||
|
) -> List[str]:
|
||||||
|
"""按路径/格位编码关键词解析储物格 ID(供细胞产品列表等跨实体检索复用)。"""
|
||||||
|
kw = (keyword or "").strip()
|
||||||
|
if not kw or max_results <= 0:
|
||||||
|
return []
|
||||||
|
base = db.query(BizStorageFacilitySlot).filter(
|
||||||
|
BizStorageFacilitySlot.del_flag == 0,
|
||||||
|
BizStorageFacilitySlot.tenant_no == tenant_no,
|
||||||
|
)
|
||||||
|
return _search_slot_select_matched_ids(
|
||||||
|
db,
|
||||||
|
base,
|
||||||
|
tenant_no,
|
||||||
|
kw,
|
||||||
|
max_results=max_results,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _apply_slot_select_query_filters(
|
def _apply_slot_select_query_filters(
|
||||||
db: Session,
|
db: Session,
|
||||||
query,
|
query,
|
||||||
|
|||||||
+49
-70
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.utils import generate_uuid
|
from app.core.utils import generate_uuid
|
||||||
|
from app.crud.child_row_sync import sync_link_rows
|
||||||
from app.crud.base import CRUDBase
|
from app.crud.base import CRUDBase
|
||||||
from app.models.models import SysUser, SysUserRole, SysUserPost, SysDept
|
from app.models.models import SysUser, SysUserRole, SysUserPost, SysDept
|
||||||
|
|
||||||
@@ -156,13 +157,19 @@ def get_user(db: Session, user_id: str) -> Optional[SysUser]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_post_ids_for_user(db: Session, user_id: str) -> List[str]:
|
def list_post_ids_for_user(
|
||||||
|
db: Session, user_id: str, tenant_no: Optional[str] = None
|
||||||
|
) -> List[str]:
|
||||||
"""用户当前有效职位 ID 列表。"""
|
"""用户当前有效职位 ID 列表。"""
|
||||||
|
query = db.query(SysUserPost).filter(
|
||||||
|
and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
|
||||||
|
)
|
||||||
|
tenant = (tenant_no or "").strip()
|
||||||
|
if tenant:
|
||||||
|
query = query.filter(SysUserPost.tenant_no == tenant)
|
||||||
return [
|
return [
|
||||||
r.post_id
|
r.post_id
|
||||||
for r in db.query(SysUserPost)
|
for r in query.all()
|
||||||
.filter(and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0))
|
|
||||||
.all()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -173,29 +180,19 @@ def set_user_post_ids_only(
|
|||||||
post_ids: List[str],
|
post_ids: List[str],
|
||||||
updater_id: Optional[str],
|
updater_id: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""仅替换 sys_user_post,不影响 sys_user_role。"""
|
"""按职位更新 sys_user_post,不影响 sys_user_role。"""
|
||||||
now = datetime.now()
|
sync_link_rows(
|
||||||
uniq = [p for p in dict.fromkeys(post_ids or []) if p]
|
db,
|
||||||
db.query(SysUserPost).filter(
|
SysUserPost,
|
||||||
and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
|
parent_attr="user_id",
|
||||||
).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
|
parent_id=user_id,
|
||||||
for pid in uniq:
|
link_attr="post_id",
|
||||||
db.add(
|
link_ids=post_ids,
|
||||||
SysUserPost(
|
tenant_no=tenant_no,
|
||||||
id=generate_uuid(),
|
user_id=updater_id,
|
||||||
user_id=user_id,
|
duplicate_message="职位重复",
|
||||||
post_id=pid,
|
foreign_message="职位关联不属于当前用户",
|
||||||
tenant_no=tenant_no,
|
)
|
||||||
order_num=0,
|
|
||||||
remark=None,
|
|
||||||
create_by=updater_id,
|
|
||||||
create_dept_id="",
|
|
||||||
create_time=now,
|
|
||||||
update_by=updater_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def list_user_ids_bound_to_post(db: Session, post_id: str, tenant_no: str) -> List[str]:
|
def list_user_ids_bound_to_post(db: Session, post_id: str, tenant_no: str) -> List[str]:
|
||||||
@@ -508,51 +505,33 @@ def _replace_associations(
|
|||||||
role_ids: Optional[List[str]] = None,
|
role_ids: Optional[List[str]] = None,
|
||||||
post_ids: Optional[List[str]] = None,
|
post_ids: Optional[List[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""全量替换关联。仅当对应参数不为 None 时才替换该侧(避免只改职位时清空直接角色)。"""
|
"""按关联 id 更新。仅当对应参数不为 None 时才替换该侧(避免只改职位时清空直接角色)。"""
|
||||||
now = datetime.now()
|
|
||||||
if role_ids is not None:
|
if role_ids is not None:
|
||||||
db.query(SysUserRole).filter(
|
sync_link_rows(
|
||||||
and_(SysUserRole.user_id == user_id, SysUserRole.del_flag == 0)
|
db,
|
||||||
).update({"del_flag": 1, "update_time": now})
|
SysUserRole,
|
||||||
for rid in role_ids:
|
parent_attr="user_id",
|
||||||
if not rid:
|
parent_id=user_id,
|
||||||
continue
|
link_attr="role_id",
|
||||||
db.add(SysUserRole(
|
link_ids=role_ids,
|
||||||
id=generate_uuid(),
|
tenant_no=tenant_no,
|
||||||
user_id=user_id,
|
user_id=creator_id,
|
||||||
role_id=rid,
|
duplicate_message="角色重复",
|
||||||
tenant_no=tenant_no,
|
foreign_message="角色关联不属于当前用户",
|
||||||
order_num=0,
|
)
|
||||||
remark=None,
|
|
||||||
create_by=creator_id,
|
|
||||||
create_dept_id="",
|
|
||||||
create_time=now,
|
|
||||||
update_by=creator_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
))
|
|
||||||
|
|
||||||
if post_ids is not None:
|
if post_ids is not None:
|
||||||
db.query(SysUserPost).filter(
|
sync_link_rows(
|
||||||
and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
|
db,
|
||||||
).update({"del_flag": 1, "update_time": now})
|
SysUserPost,
|
||||||
for pid in post_ids:
|
parent_attr="user_id",
|
||||||
if not pid:
|
parent_id=user_id,
|
||||||
continue
|
link_attr="post_id",
|
||||||
db.add(SysUserPost(
|
link_ids=post_ids,
|
||||||
id=generate_uuid(),
|
tenant_no=tenant_no,
|
||||||
user_id=user_id,
|
user_id=creator_id,
|
||||||
post_id=pid,
|
duplicate_message="职位重复",
|
||||||
tenant_no=tenant_no,
|
foreign_message="职位关联不属于当前用户",
|
||||||
order_num=0,
|
)
|
||||||
remark=None,
|
|
||||||
create_by=creator_id,
|
|
||||||
create_dept_id="",
|
|
||||||
create_time=now,
|
|
||||||
update_by=creator_id,
|
|
||||||
update_time=now,
|
|
||||||
del_flag=0,
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_hire_date(value: Optional[Union[str, date]]) -> Optional[date]:
|
def _parse_hire_date(value: Optional[Union[str, date]]) -> Optional[date]:
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""历史单据上的人员姓名。
|
||||||
|
|
||||||
|
用户管理里删除人员只会把 sys_user.del_flag 置为 1,账号不能再登录,
|
||||||
|
也不能再被选为新的负责人。已经写在工单、步骤、协议上的 user_id
|
||||||
|
仍要显示 nick_name;查不到人时调用方再回退成 id。
|
||||||
|
|
||||||
|
选人、校验、登录不要用本模块。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, Iterable, List
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.models import SysUser
|
||||||
|
|
||||||
|
|
||||||
|
def user_display_name_map(db: Session, user_ids: Iterable[str]) -> Dict[str, str]:
|
||||||
|
"""按 user_id 取展示名,包含已删除用户。优先昵称,否则账号。"""
|
||||||
|
ids: List[str] = []
|
||||||
|
seen = set()
|
||||||
|
for raw in user_ids or []:
|
||||||
|
uid = str(raw or "").strip()
|
||||||
|
if not uid or uid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(uid)
|
||||||
|
ids.append(uid)
|
||||||
|
if not ids:
|
||||||
|
return {}
|
||||||
|
rows = (
|
||||||
|
db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
|
||||||
|
.filter(SysUser.id.in_(ids))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
out: Dict[str, str] = {}
|
||||||
|
for row in rows:
|
||||||
|
name = (row.nick_name or "").strip() or (row.user_no or "").strip()
|
||||||
|
if name:
|
||||||
|
out[row.id] = name
|
||||||
|
return out
|
||||||
+15
-4
@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
|
|||||||
from app.core.api_request_log_middleware import ApiRequestLogMiddleware
|
from app.core.api_request_log_middleware import ApiRequestLogMiddleware
|
||||||
from app.core.api_crypto_middleware import ApiCryptoMiddleware
|
from app.core.api_crypto_middleware import ApiCryptoMiddleware
|
||||||
from app.models import models # noqa: F401
|
from app.models import models # noqa: F401
|
||||||
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router
|
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, sample_freight_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router, market_mobile_router, sample_storage_cert_qc_router, customer_service_dict_router, sample_cert_insurance_router, sample_code_correction_router
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -56,8 +56,9 @@ app = FastAPI(
|
|||||||
title=settings.APP_NAME,
|
title=settings.APP_NAME,
|
||||||
description="zkxw 管理端骨架后端(鉴权与菜单)",
|
description="zkxw 管理端骨架后端(鉴权与菜单)",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
docs_url="/docs",
|
docs_url="/docs" if settings.DEBUG else None,
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc" if settings.DEBUG else None,
|
||||||
|
openapi_url="/openapi.json" if settings.DEBUG else None,
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,6 +117,10 @@ app.include_router(admin_router, prefix=settings.API_V1_PREFIX)
|
|||||||
app.include_router(workflow_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(workflow_router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(basic_info_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(basic_info_router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(collection_protocol_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(collection_protocol_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(protocol_sample_payment_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(protocol_refund_record_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(presign_order_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(sample_freight_router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(ethnic_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(ethnic_router, prefix=settings.API_V1_PREFIX)
|
||||||
@@ -147,6 +152,11 @@ app.include_router(pre_transit_sample_router, prefix=settings.API_V1_PREFIX)
|
|||||||
app.include_router(oa_approval_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(oa_approval_router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(mini_auth_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(mini_auth_router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(mini_menu_router, prefix=settings.API_V1_PREFIX)
|
app.include_router(mini_menu_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(market_mobile_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(sample_storage_cert_qc_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(customer_service_dict_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(sample_cert_insurance_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(sample_code_correction_router, prefix=settings.API_V1_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
def _mask_secret(value: str) -> str:
|
def _mask_secret(value: str) -> str:
|
||||||
@@ -172,7 +182,8 @@ logger.info(_api_crypto_boot_msg)
|
|||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {"name": settings.APP_NAME, "version": "1.0.0", "docs": "/docs"}
|
docs = "/docs" if settings.DEBUG else None
|
||||||
|
return {"name": settings.APP_NAME, "version": "1.0.0", "docs": docs}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ from app.models.models import (
|
|||||||
BizProductionTaskStepDataCollection,
|
BizProductionTaskStepDataCollection,
|
||||||
BizProductionTaskOutput,
|
BizProductionTaskOutput,
|
||||||
BizStorageSlotOccupancy,
|
BizStorageSlotOccupancy,
|
||||||
|
BizSampleFreight,
|
||||||
|
BizSampleFreightItem,
|
||||||
)
|
)
|
||||||
from app.models.production_material import ( # noqa: F401
|
from app.models.production_material import ( # noqa: F401
|
||||||
BizProductionMaterial,
|
BizProductionMaterial,
|
||||||
@@ -123,6 +125,8 @@ __all__ = [
|
|||||||
"BizProductionTaskStepDataCollection",
|
"BizProductionTaskStepDataCollection",
|
||||||
"BizProductionTaskOutput",
|
"BizProductionTaskOutput",
|
||||||
"BizStorageSlotOccupancy",
|
"BizStorageSlotOccupancy",
|
||||||
|
"BizSampleFreight",
|
||||||
|
"BizSampleFreightItem",
|
||||||
"BizProductionMaterial",
|
"BizProductionMaterial",
|
||||||
"BizProductionMaterialStockBatch",
|
"BizProductionMaterialStockBatch",
|
||||||
"BizProductionMaterialUsage",
|
"BizProductionMaterialUsage",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
from sqlalchemy import Column, String, DateTime, Date, Integer, Text, CHAR, SmallInteger, Numeric
|
from sqlalchemy import Column, String, DateTime, Date, Integer, Text, CHAR, SmallInteger, Numeric, UniqueConstraint
|
||||||
|
|
||||||
from app.core.database import Base
|
from app.core.database import Base
|
||||||
|
|
||||||
@@ -334,6 +334,10 @@ class BizCustomer(Base):
|
|||||||
sex = Column(CHAR(1), nullable=True, default="2", comment="性别(0男1女2未知)")
|
sex = Column(CHAR(1), nullable=True, default="2", comment="性别(0男1女2未知)")
|
||||||
id_card_no = Column(String(32), nullable=True, comment="身份证号")
|
id_card_no = Column(String(32), nullable=True, comment="身份证号")
|
||||||
age = Column(Integer, nullable=True, comment="年龄")
|
age = Column(Integer, nullable=True, comment="年龄")
|
||||||
|
birth_ym = Column(CHAR(6), nullable=True, comment="出生年月YYYYMM")
|
||||||
|
ethnic = Column(String(32), nullable=True, comment="民族")
|
||||||
|
email = Column(String(128), nullable=True, comment="邮箱")
|
||||||
|
address = Column(String(512), nullable=True, comment="家庭住址")
|
||||||
phone_no = Column(String(32), nullable=True, comment="联系电话")
|
phone_no = Column(String(32), nullable=True, comment="联系电话")
|
||||||
customer_tags = Column(String(512), nullable=True, comment="客户标签,逗号分隔")
|
customer_tags = Column(String(512), nullable=True, comment="客户标签,逗号分隔")
|
||||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
@@ -1016,6 +1020,7 @@ class OaInstanceViewGrant(Base):
|
|||||||
source = Column(String(32), nullable=False, default="comment_mention", comment="comment_mention")
|
source = Column(String(32), nullable=False, default="comment_mention", comment="comment_mention")
|
||||||
comment_id = Column(String(64), nullable=True, comment="来源评论ID,撤回评论不删除授权")
|
comment_id = Column(String(64), nullable=True, comment="来源评论ID,撤回评论不删除授权")
|
||||||
granted_by = Column(String(64), nullable=True, comment="授权人")
|
granted_by = Column(String(64), nullable=True, comment="授权人")
|
||||||
|
is_read = Column(SmallInteger, nullable=False, default=0, comment="是否已读 0未读 1已读")
|
||||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
remark = Column(String(512), nullable=True, comment="备注")
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
@@ -1056,6 +1061,13 @@ class BizCollectionProtocol(Base):
|
|||||||
"""采集协议单 biz_collection_protocol"""
|
"""采集协议单 biz_collection_protocol"""
|
||||||
|
|
||||||
__tablename__ = "biz_collection_protocol"
|
__tablename__ = "biz_collection_protocol"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"tenant_no",
|
||||||
|
"source_presign_order_id",
|
||||||
|
name="uk_collection_protocol_presign_source",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
protocol_no_id = Column(String(64), nullable=True, comment="采集协议号表ID")
|
protocol_no_id = Column(String(64), nullable=True, comment="采集协议号表ID")
|
||||||
@@ -1075,6 +1087,9 @@ class BizCollectionProtocol(Base):
|
|||||||
to_research_time = Column(DateTime, nullable=True, comment="协议转科研时间")
|
to_research_time = Column(DateTime, nullable=True, comment="协议转科研时间")
|
||||||
to_research_desc = Column(String(512), nullable=True, comment="协议转科研描述")
|
to_research_desc = Column(String(512), nullable=True, comment="协议转科研描述")
|
||||||
collector_user_id = Column(String(64), nullable=True, comment="采集专员用户ID")
|
collector_user_id = Column(String(64), nullable=True, comment="采集专员用户ID")
|
||||||
|
protocol_total_amount = Column(Numeric(18, 2), nullable=True, comment="手填协议总金额")
|
||||||
|
source_presign_order_id = Column(String(64), nullable=True, comment="来源预签约订单ID")
|
||||||
|
payment_model_version = Column(SmallInteger, nullable=False, default=2, comment="缴费模型版本 1旧/2新")
|
||||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
remark = Column(String(512), nullable=True, comment="备注信息")
|
remark = Column(String(512), nullable=True, comment="备注信息")
|
||||||
@@ -1107,6 +1122,31 @@ class BizProtocolAudit(Base):
|
|||||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizProtocolStatusChange(Base):
|
||||||
|
"""采集协议单-状态变更记录 biz_protocol_status_change"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_protocol_status_change"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
||||||
|
change_type = Column(String(32), nullable=False, comment="变更类型 activate/expire/void/to_research")
|
||||||
|
from_status = Column(String(16), nullable=True, comment="变更前状态码")
|
||||||
|
to_status = Column(String(16), nullable=False, comment="变更后状态码")
|
||||||
|
change_time = Column(DateTime, nullable=False, comment="业务变更时间")
|
||||||
|
change_desc = Column(String(512), nullable=True, comment="变更描述")
|
||||||
|
void_type = Column(String(32), nullable=True, comment="作废类型(字典 xyzflx)")
|
||||||
|
operator_user_id = Column(String(64), nullable=True, comment="操作人用户ID")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
class BizStorageReleaseReview(Base):
|
class BizStorageReleaseReview(Base):
|
||||||
"""入库放行审核 biz_storage_release_review"""
|
"""入库放行审核 biz_storage_release_review"""
|
||||||
|
|
||||||
@@ -1211,6 +1251,184 @@ class BizProtocolPaymentDetail(Base):
|
|||||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizProtocolSamplePayment(Base):
|
||||||
|
"""采集协议单-按样本缴费方案 biz_protocol_sample_payment"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_protocol_sample_payment"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
||||||
|
protocol_sample_id = Column(String(64), nullable=True, comment="协议样本ID(可空兜底)")
|
||||||
|
sample_code = Column(String(32), nullable=False, comment="样本编码(业务关联键)")
|
||||||
|
pay_mode = Column(String(8), nullable=False, comment="dj趸交 fq分期")
|
||||||
|
contract_amount = Column(Numeric(18, 2), nullable=True, comment="该样本合同金额")
|
||||||
|
installment_count = Column(Integer, nullable=True, comment="分期期数")
|
||||||
|
installment_interval_months = Column(Numeric(10, 2), nullable=True, comment="每期间隔(月)")
|
||||||
|
installment_start_date = Column(CHAR(8), nullable=True, comment="分期开始YYYYMMDD")
|
||||||
|
installment_advance_days = Column(Integer, nullable=True, comment="第2期起应缴费提前天数")
|
||||||
|
installment_ratios = Column(String(256), nullable=True, comment="各期缴费比例JSON数组")
|
||||||
|
lump_sum_deadline = Column(CHAR(8), nullable=True, comment="趸交截止时间YYYYMMDD")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizProtocolPaymentSchedule(Base):
|
||||||
|
"""采集协议单-应缴批次 biz_protocol_payment_schedule"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_protocol_payment_schedule"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
sample_payment_id = Column(String(64), nullable=False, comment="样本缴费方案ID")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
||||||
|
sample_code = Column(String(32), nullable=False, comment="样本编码")
|
||||||
|
schedule_no = Column(Integer, nullable=True, comment="期次序号")
|
||||||
|
due_amount = Column(Numeric(18, 2), nullable=True, comment="应缴金额")
|
||||||
|
due_date = Column(CHAR(8), nullable=True, comment="应缴日期YYYYMMDD")
|
||||||
|
schedule_status = Column(String(16), nullable=True, comment="批次状态 open/cleared/void")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizProtocolPaymentTransfer(Base):
|
||||||
|
"""采集协议单-缴费转账笔 biz_protocol_payment_transfer"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_protocol_payment_transfer"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
schedule_id = Column(String(64), nullable=False, comment="应缴批次ID")
|
||||||
|
sample_payment_id = Column(String(64), nullable=False, comment="样本缴费方案ID")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
||||||
|
sample_code = Column(String(32), nullable=False, comment="样本编码")
|
||||||
|
transfer_amount = Column(Numeric(18, 2), nullable=True, comment="转账金额")
|
||||||
|
transfer_time = Column(DateTime, nullable=True, comment="转账时间")
|
||||||
|
transfer_type = Column(String(16), nullable=True, comment="转账方式")
|
||||||
|
transfer_attachments = Column(Text, nullable=True, comment="转账附件")
|
||||||
|
pay_status = Column(String(32), nullable=False, default="dfj", comment="缴费状态 dfj/jfqrz/yjf")
|
||||||
|
finance_confirm_attachments = Column(Text, nullable=True, comment="缴费财务确认附件")
|
||||||
|
finance_remark = Column(String(512), nullable=True, comment="缴费财务确认备注")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizProtocolRefundRecord(Base):
|
||||||
|
"""采集协议单-退费登记 biz_protocol_refund_record"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_protocol_refund_record"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
||||||
|
refund_amount = Column(Numeric(18, 2), nullable=True, comment="退费金额")
|
||||||
|
refund_time = Column(DateTime, nullable=True, comment="退费时间")
|
||||||
|
refund_remark = Column(String(512), nullable=True, comment="退费备注")
|
||||||
|
refund_attachments = Column(Text, nullable=True, comment="退费附件")
|
||||||
|
workflow_instance_id = Column(String(64), nullable=True, comment="OA工作流实例ID")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizPresignOrder(Base):
|
||||||
|
"""预签约订单 biz_presign_order"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_presign_order"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
order_date = Column(CHAR(8), nullable=True, comment="业务日期YYYYMMDD")
|
||||||
|
protocol_no = Column(String(64), nullable=True, comment="协议编号")
|
||||||
|
mother_name = Column(String(64), nullable=True, comment="母亲姓名")
|
||||||
|
expected_delivery_date = Column(CHAR(8), nullable=True, comment="预产期YYYYMMDD")
|
||||||
|
region_id = Column(String(64), nullable=True, comment="采集地区ID")
|
||||||
|
hospital_id = Column(String(64), nullable=True, comment="医院ID")
|
||||||
|
sample_collect_time = Column(DateTime, nullable=True, comment="采集时间")
|
||||||
|
pay_mode = Column(String(8), nullable=True, comment="整单缴费方式 dj/fq")
|
||||||
|
contract_amount = Column(Numeric(18, 2), nullable=True, comment="整单合同金额")
|
||||||
|
collector_user_id = Column(String(64), nullable=True, comment="采集专员用户ID")
|
||||||
|
sales_user_id = Column(String(64), nullable=True, comment="市场专员用户ID")
|
||||||
|
region_manager_user_id = Column(String(64), nullable=True, comment="采集经理/地区经理用户ID")
|
||||||
|
order_status = Column(String(16), nullable=True, comment="订单状态 draft/active/converted/void")
|
||||||
|
converted_protocol_id = Column(String(64), nullable=True, comment="转成的正式协议ID")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizPresignOrderSampleType(Base):
|
||||||
|
"""预签约订单-样本类型 biz_presign_order_sample_type"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_presign_order_sample_type"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
presign_order_id = Column(String(64), nullable=False, comment="预签约订单ID")
|
||||||
|
sample_type_id = Column(String(64), nullable=False, comment="样本类型ID")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizPresignTransfer(Base):
|
||||||
|
"""预签约订单-意向金转账笔 biz_presign_transfer"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_presign_transfer"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
presign_order_id = Column(String(64), nullable=False, comment="预签约订单ID")
|
||||||
|
transfer_amount = Column(Numeric(18, 2), nullable=True, comment="转账金额")
|
||||||
|
transfer_time = Column(DateTime, nullable=True, comment="转账时间")
|
||||||
|
transfer_type = Column(String(16), nullable=True, comment="转账方式")
|
||||||
|
transfer_attachments = Column(Text, nullable=True, comment="转账附件")
|
||||||
|
pay_status = Column(String(32), nullable=False, default="dfj", comment="缴费状态 dfj/jfqrz/yjf")
|
||||||
|
finance_confirm_attachments = Column(Text, nullable=True, comment="缴费财务确认附件")
|
||||||
|
finance_remark = Column(String(512), nullable=True, comment="缴费财务确认备注")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
class BizProtocolInvoiceRequest(Base):
|
class BizProtocolInvoiceRequest(Base):
|
||||||
"""采集协议单开票申请主表 biz_protocol_invoice_request"""
|
"""采集协议单开票申请主表 biz_protocol_invoice_request"""
|
||||||
|
|
||||||
@@ -1271,6 +1489,8 @@ class BizProtocolPartyCustomer(Base):
|
|||||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
||||||
customer_id = Column(String(64), nullable=True, comment="客户ID")
|
customer_id = Column(String(64), nullable=True, comment="客户ID")
|
||||||
|
father_customer_id = Column(String(64), nullable=True, comment="父亲客户ID")
|
||||||
|
newborn_customer_id = Column(String(64), nullable=True, comment="子女客户ID")
|
||||||
mother_name = Column(String(100), nullable=True, comment="母亲姓名")
|
mother_name = Column(String(100), nullable=True, comment="母亲姓名")
|
||||||
mother_phone = Column(String(32), nullable=True, comment="母亲电话")
|
mother_phone = Column(String(32), nullable=True, comment="母亲电话")
|
||||||
mother_id_card = Column(String(32), nullable=True, comment="母亲身份证号")
|
mother_id_card = Column(String(32), nullable=True, comment="母亲身份证号")
|
||||||
@@ -1313,7 +1533,8 @@ class BizProtocolCollectionInfo(Base):
|
|||||||
region_id = Column(String(64), nullable=True, comment="区域ID")
|
region_id = Column(String(64), nullable=True, comment="区域ID")
|
||||||
hospital_id = Column(String(64), nullable=True, comment="医院ID")
|
hospital_id = Column(String(64), nullable=True, comment="医院ID")
|
||||||
medical_staff = Column(String(200), nullable=True, comment="医护人员")
|
medical_staff = Column(String(200), nullable=True, comment="医护人员")
|
||||||
sales_user_id = Column(String(64), nullable=True, comment="销售人员用户ID")
|
sales_user_id = Column(String(64), nullable=True, comment="市场专员用户ID")
|
||||||
|
region_manager_user_id = Column(String(64), nullable=True, comment="地区经理用户ID")
|
||||||
sample_collect_time = Column(DateTime, nullable=True, comment="样本采集时间")
|
sample_collect_time = Column(DateTime, nullable=True, comment="样本采集时间")
|
||||||
maternal_blood_collect_time = Column(DateTime, nullable=True, comment="母血采集时间")
|
maternal_blood_collect_time = Column(DateTime, nullable=True, comment="母血采集时间")
|
||||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
@@ -1388,6 +1609,51 @@ class BizProtocolSampleTransport(Base):
|
|||||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizSampleFreight(Base):
|
||||||
|
"""样本运费批次 biz_sample_freight"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_sample_freight"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
freight_no = Column(String(32), nullable=False, default="", comment="运费批次号 YF+YYYYMMDD+序号")
|
||||||
|
transport_mode = Column(String(100), nullable=False, default="", comment="运输方式字典值")
|
||||||
|
freight_amount = Column(Numeric(18, 2), nullable=True, comment="运费(元),顺丰月结未填时为空")
|
||||||
|
waybill_no = Column(String(64), nullable=True, comment="顺丰单号")
|
||||||
|
settle_status = Column(String(8), nullable=True, comment="结算状态 wjs/yjs")
|
||||||
|
settle_time = Column(DateTime, nullable=True, comment="结算时间")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizSampleFreightItem(Base):
|
||||||
|
"""样本运费关联 biz_sample_freight_item"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_sample_freight_item"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
freight_id = Column(String(64), nullable=False, comment="运费批次ID")
|
||||||
|
sample_source = Column(String(32), nullable=False, comment="cell_storage|research_donation")
|
||||||
|
sample_id = Column(String(64), nullable=False, comment="样本ID")
|
||||||
|
sample_code = Column(String(32), nullable=True, comment="样本编码快照")
|
||||||
|
protocol_id = Column(String(64), nullable=True, comment="协议单ID")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
class BizAttachment(Base):
|
class BizAttachment(Base):
|
||||||
"""业务附件 biz_attachment"""
|
"""业务附件 biz_attachment"""
|
||||||
|
|
||||||
@@ -1422,6 +1688,126 @@ class BizProtocolStorageCert(Base):
|
|||||||
make_status = Column(CHAR(1), nullable=True, default="0", comment="制作状态 0-未制作 1-已制作")
|
make_status = Column(CHAR(1), nullable=True, default="0", comment="制作状态 0-未制作 1-已制作")
|
||||||
cert_image_urls = Column(String(4000), nullable=True, comment="证书图片URLs")
|
cert_image_urls = Column(String(4000), nullable=True, comment="证书图片URLs")
|
||||||
make_date = Column(CHAR(8), nullable=True, comment="制作日期YYYYMMDD")
|
make_date = Column(CHAR(8), nullable=True, comment="制作日期YYYYMMDD")
|
||||||
|
handle_time = Column(DateTime, nullable=True, comment="办理时间")
|
||||||
|
handler_user_id = Column(String(64), nullable=True, comment="办理人用户ID")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizSampleStorageCert(Base):
|
||||||
|
"""样本储存证办理 biz_sample_storage_cert"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_sample_storage_cert"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
sample_code = Column(String(32), nullable=False, comment="样本编码")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=True, comment="采集协议单ID冗余")
|
||||||
|
make_status = Column(CHAR(1), nullable=True, default="0", comment="制作状态 0-未制作 1-已制作")
|
||||||
|
cert_image_urls = Column(String(4000), nullable=True, comment="证书图片URLs")
|
||||||
|
make_date = Column(CHAR(8), nullable=True, comment="制作日期YYYYMMDD")
|
||||||
|
handle_time = Column(DateTime, nullable=True, comment="办理时间")
|
||||||
|
handler_user_id = Column(String(64), nullable=True, comment="办理人用户ID")
|
||||||
|
mail_cert_time = Column(DateTime, nullable=True, comment="邮寄储存证时间")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizSampleStorageCertQc(Base):
|
||||||
|
"""样本储存证质检信息 biz_sample_storage_cert_qc"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_sample_storage_cert_qc"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
sample_code = Column(String(32), nullable=False, comment="样本编码")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=True, comment="采集协议单ID冗余")
|
||||||
|
qc_work_order_id = Column(String(64), nullable=True, comment="最近填写来源工单ID")
|
||||||
|
sample_type_key = Column(String(32), nullable=False, comment="perinatal_hsc|uc_msc")
|
||||||
|
payload = Column(Text, nullable=False, comment="业务字段 JSON")
|
||||||
|
filled_flag = Column(CHAR(1), nullable=False, default="0", comment="是否已保存过 0否1是")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizSampleCodeCorrectionLog(Base):
|
||||||
|
"""样本编码纠错审计日志 biz_sample_code_correction_log"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_sample_code_correction_log"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
reception_source = Column(String(32), nullable=False, comment="来源 cell_storage/research_donation")
|
||||||
|
old_sample_code = Column(String(32), nullable=False, comment="旧样本编码")
|
||||||
|
new_sample_code = Column(String(32), nullable=False, comment="新样本编码")
|
||||||
|
impact_json = Column(Text, nullable=True, comment="各表影响行数 JSON")
|
||||||
|
reason = Column(String(512), nullable=True, comment="更正原因")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizSampleCustomerService(Base):
|
||||||
|
"""样本客户服务扩展 biz_sample_customer_service"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_sample_customer_service"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
sample_code = Column(String(32), nullable=False, comment="样本编码")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=True, comment="采集协议单ID冗余")
|
||||||
|
sms_send_date = Column(Date, nullable=True, comment="短信发送日期")
|
||||||
|
sms_send_status = Column(String(64), nullable=True, comment="短信发送情况")
|
||||||
|
microbe_followup_date = Column(Date, nullable=True, comment="微生物回访日期")
|
||||||
|
microbe_followup_status = Column(String(64), nullable=True, comment="微生物回访情况")
|
||||||
|
fullmoon_followup_date = Column(Date, nullable=True, comment="满月回访时间")
|
||||||
|
fullmoon_followup_status = Column(String(64), nullable=True, comment="满月回访情况")
|
||||||
|
household_registered = Column(String(64), nullable=True, comment="是否上户口")
|
||||||
|
einvoice_send_time = Column(DateTime, nullable=True, comment="电子发票发送时间")
|
||||||
|
effective_address = Column(String(512), nullable=True, comment="有效地址")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizCustomerServiceDict(Base):
|
||||||
|
"""客户服务字典 biz_customer_service_dict"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_customer_service_dict"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
dict_type = Column(String(64), nullable=False, comment="字典类型")
|
||||||
|
dict_value = Column(String(128), nullable=False, comment="选项值")
|
||||||
|
is_system = Column(CHAR(1), nullable=False, default="0", comment="1预置 0手输")
|
||||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
remark = Column(String(512), nullable=True, comment="备注")
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
@@ -2146,6 +2532,25 @@ class BizCellProductSpec(Base):
|
|||||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizCellProductPackage(Base):
|
||||||
|
"""细胞产品包装 biz_cell_product_package"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_cell_product_package"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
package_code = Column(String(64), nullable=False, comment="包装编码")
|
||||||
|
package_name = Column(String(200), nullable=False, comment="包装名称")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
class BizCellProductPrice(Base):
|
class BizCellProductPrice(Base):
|
||||||
"""细胞产品价格体系 biz_cell_product_price"""
|
"""细胞产品价格体系 biz_cell_product_price"""
|
||||||
|
|
||||||
@@ -3439,6 +3844,8 @@ class BizProtocolFullmoonTodo(Base):
|
|||||||
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
|
||||||
todo_status = Column(CHAR(1), nullable=False, default="0", comment="1已办保险 0未办保险")
|
todo_status = Column(CHAR(1), nullable=False, default="0", comment="1已办保险 0未办保险")
|
||||||
handle_date = Column(CHAR(8), nullable=True, comment="办理日期")
|
handle_date = Column(CHAR(8), nullable=True, comment="办理日期")
|
||||||
|
handle_time = Column(DateTime, nullable=True, comment="办理时间")
|
||||||
|
handler_user_id = Column(String(64), nullable=True, comment="办理人用户ID")
|
||||||
handle_remark = Column(String(512), nullable=True, comment="办理备注")
|
handle_remark = Column(String(512), nullable=True, comment="办理备注")
|
||||||
insurance_attachments = Column(String(4000), nullable=True, comment="保险附件逗号分隔")
|
insurance_attachments = Column(String(4000), nullable=True, comment="保险附件逗号分隔")
|
||||||
insurance_company_id = Column(String(64), nullable=True, comment="保险公司ID")
|
insurance_company_id = Column(String(64), nullable=True, comment="保险公司ID")
|
||||||
@@ -3460,6 +3867,40 @@ class BizProtocolFullmoonTodo(Base):
|
|||||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizSampleInsurance(Base):
|
||||||
|
"""样本保险办理 biz_sample_insurance"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_sample_insurance"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
sample_code = Column(String(32), nullable=False, comment="样本编码")
|
||||||
|
collection_protocol_id = Column(String(64), nullable=True, comment="采集协议单ID冗余")
|
||||||
|
todo_status = Column(CHAR(1), nullable=False, default="0", comment="1已办保险 0未办保险")
|
||||||
|
handle_date = Column(CHAR(8), nullable=True, comment="办理日期")
|
||||||
|
handle_time = Column(DateTime, nullable=True, comment="办理时间")
|
||||||
|
handler_user_id = Column(String(64), nullable=True, comment="办理人用户ID")
|
||||||
|
handle_remark = Column(String(512), nullable=True, comment="办理备注")
|
||||||
|
insurance_attachments = Column(String(4000), nullable=True, comment="保险附件逗号分隔")
|
||||||
|
insurance_company_id = Column(String(64), nullable=True, comment="保险公司ID")
|
||||||
|
insurance_company_name = Column(String(200), nullable=True, comment="保险公司名称(冗余快照)")
|
||||||
|
insurance_type = Column(String(200), nullable=True, comment="保险险种")
|
||||||
|
insurance_policy_no = Column(String(100), nullable=True, comment="保险单号")
|
||||||
|
insurance_effective_date = Column(CHAR(8), nullable=True, comment="保险单生效时间YYYYMMDD")
|
||||||
|
insurance_expiry_date = Column(CHAR(8), nullable=True, comment="保险单失效时间YYYYMMDD")
|
||||||
|
insurance_subscribed = Column(CHAR(1), nullable=True, default="0", comment="是否已订购保险")
|
||||||
|
insurance_fee = Column(Numeric(18, 2), nullable=True, comment="保单费用(元)")
|
||||||
|
policy_send_time = Column(DateTime, nullable=True, comment="保单发送时间")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
class BizInsuranceCompany(Base):
|
class BizInsuranceCompany(Base):
|
||||||
"""保险公司 biz_insurance_company"""
|
"""保险公司 biz_insurance_company"""
|
||||||
|
|
||||||
@@ -3681,6 +4122,8 @@ class BizPilotInfusionSchedule(Base):
|
|||||||
infusion_plan_id = Column(String(64), nullable=False, comment="输注方案ID")
|
infusion_plan_id = Column(String(64), nullable=False, comment="输注方案ID")
|
||||||
cell_type = Column(String(200), nullable=True, comment="细胞类型")
|
cell_type = Column(String(200), nullable=True, comment="细胞类型")
|
||||||
spec_text = Column(String(200), nullable=True, comment="规格")
|
spec_text = Column(String(200), nullable=True, comment="规格")
|
||||||
|
package_id = Column(String(64), nullable=True, comment="产品包装ID")
|
||||||
|
package_text = Column(String(200), nullable=True, comment="产品包装名称(冗余展示)")
|
||||||
infusion_time = Column(DateTime, nullable=True, comment="输注时间")
|
infusion_time = Column(DateTime, nullable=True, comment="输注时间")
|
||||||
infusion_location = Column(String(200), nullable=True, comment="输注地点")
|
infusion_location = Column(String(200), nullable=True, comment="输注地点")
|
||||||
infusion_method = Column(String(32), nullable=True, comment="输注方式(字典szfs)")
|
infusion_method = Column(String(32), nullable=True, comment="输注方式(字典szfs)")
|
||||||
@@ -3713,6 +4156,8 @@ class BizPilotInfusionScheduleProduct(Base):
|
|||||||
project_name = Column(String(200), nullable=True, comment="项目名称")
|
project_name = Column(String(200), nullable=True, comment="项目名称")
|
||||||
cell_type_name = Column(String(200), nullable=True, comment="细胞类型")
|
cell_type_name = Column(String(200), nullable=True, comment="细胞类型")
|
||||||
spec_text = Column(String(200), nullable=True, comment="规格")
|
spec_text = Column(String(200), nullable=True, comment="规格")
|
||||||
|
package_id = Column(String(64), nullable=True, comment="产品包装ID")
|
||||||
|
package_text = Column(String(200), nullable=True, comment="产品包装名称(冗余展示)")
|
||||||
agreement_price = Column(Numeric(18, 2), nullable=True, comment="协议单价(元)")
|
agreement_price = Column(Numeric(18, 2), nullable=True, comment="协议单价(元)")
|
||||||
actual_price = Column(Numeric(18, 2), nullable=True, comment="实际单价(元)")
|
actual_price = Column(Numeric(18, 2), nullable=True, comment="实际单价(元)")
|
||||||
quantity = Column(Integer, nullable=True, default=1, comment="份数")
|
quantity = Column(Integer, nullable=True, default=1, comment="份数")
|
||||||
@@ -3728,6 +4173,30 @@ class BizPilotInfusionScheduleProduct(Base):
|
|||||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizWxNotifyLog(Base):
|
||||||
|
"""微信服务号通知发送日志(幂等) biz_wx_notify_log"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_wx_notify_log"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
biz_type = Column(String(64), nullable=False, comment="业务类型,如 basic_experiment_delivery_remind")
|
||||||
|
biz_id = Column(String(64), nullable=False, comment="业务主键(如交付计划ID)")
|
||||||
|
notify_date = Column(String(10), nullable=False, comment="提醒日 YYYY-MM-DD")
|
||||||
|
user_id = Column(String(64), nullable=True, comment="接收用户ID")
|
||||||
|
status = Column(String(16), nullable=False, default="success", comment="success/fail")
|
||||||
|
errcode = Column(String(64), nullable=True, comment="微信/内部错误码")
|
||||||
|
errmsg = Column(String(512), nullable=True, comment="错误信息")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
class BizPilotCheckup(Base):
|
class BizPilotCheckup(Base):
|
||||||
"""中试申请单-体检信息 biz_pilot_checkup"""
|
"""中试申请单-体检信息 biz_pilot_checkup"""
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class BizResearchDonation(Base):
|
|||||||
|
|
||||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
donation_no = Column(String(64), nullable=False, comment="捐赠协议编号")
|
donation_no = Column(String(64), nullable=False, comment="捐赠协议编号")
|
||||||
|
monthly_verify_no = Column(String(16), nullable=True, comment="当月核量号 YYYYMM+3位序号")
|
||||||
donation_status = Column(String(16), nullable=False, default="dsx", comment="状态 dsx/sxz/ygq/yzf/sdfq/zckc")
|
donation_status = Column(String(16), nullable=False, default="dsx", comment="状态 dsx/sxz/ygq/yzf/sdfq/zckc")
|
||||||
donation_date = Column(String(8), nullable=True, comment="捐赠日期YYYYMMDD")
|
donation_date = Column(String(8), nullable=True, comment="捐赠日期YYYYMMDD")
|
||||||
source_type = Column(String(20), nullable=False, default="direct", comment="来源类型 direct/from_storage")
|
source_type = Column(String(20), nullable=False, default="direct", comment="来源类型 direct/from_storage")
|
||||||
@@ -64,6 +65,31 @@ class BizResearchDonationAudit(Base):
|
|||||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
|
class BizDonationStatusChange(Base):
|
||||||
|
"""科研捐赠-状态变更记录 biz_donation_status_change"""
|
||||||
|
|
||||||
|
__tablename__ = "biz_donation_status_change"
|
||||||
|
|
||||||
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
|
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠单ID")
|
||||||
|
change_type = Column(String(32), nullable=False, comment="变更类型 activate/expire/void/to_storage")
|
||||||
|
from_status = Column(String(16), nullable=True, comment="变更前状态码")
|
||||||
|
to_status = Column(String(16), nullable=False, comment="变更后状态码")
|
||||||
|
change_time = Column(DateTime, nullable=False, comment="业务变更时间")
|
||||||
|
change_desc = Column(String(512), nullable=True, comment="变更描述")
|
||||||
|
void_type = Column(String(32), nullable=True, comment="作废类型(字典 xyzflx)")
|
||||||
|
operator_user_id = Column(String(64), nullable=True, comment="操作人用户ID")
|
||||||
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||||
|
remark = Column(String(512), nullable=True, comment="备注")
|
||||||
|
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||||
|
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||||
|
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||||
|
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||||
|
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||||
|
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||||
|
|
||||||
|
|
||||||
class BizResearchDonationParty(Base):
|
class BizResearchDonationParty(Base):
|
||||||
"""科研捐赠当事人信息 biz_research_donation_party"""
|
"""科研捐赠当事人信息 biz_research_donation_party"""
|
||||||
|
|
||||||
@@ -71,7 +97,9 @@ class BizResearchDonationParty(Base):
|
|||||||
|
|
||||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||||
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
|
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
|
||||||
customer_id = Column(String(64), nullable=True, comment="客户ID")
|
customer_id = Column(String(64), nullable=True, comment="母亲客户ID")
|
||||||
|
father_customer_id = Column(String(64), nullable=True, comment="父亲客户ID")
|
||||||
|
newborn_customer_id = Column(String(64), nullable=True, comment="子女客户ID")
|
||||||
mother_name = Column(String(100), nullable=True, comment="母亲姓名")
|
mother_name = Column(String(100), nullable=True, comment="母亲姓名")
|
||||||
mother_phone = Column(String(32), nullable=True, comment="母亲电话")
|
mother_phone = Column(String(32), nullable=True, comment="母亲电话")
|
||||||
mother_id_card = Column(String(32), nullable=True, comment="母亲身份证号")
|
mother_id_card = Column(String(32), nullable=True, comment="母亲身份证号")
|
||||||
@@ -114,7 +142,8 @@ class BizResearchDonationCollection(Base):
|
|||||||
region_id = Column(String(64), nullable=True, comment="区域ID")
|
region_id = Column(String(64), nullable=True, comment="区域ID")
|
||||||
hospital_id = Column(String(64), nullable=True, comment="医院ID")
|
hospital_id = Column(String(64), nullable=True, comment="医院ID")
|
||||||
medical_staff = Column(String(200), nullable=True, comment="医护人员")
|
medical_staff = Column(String(200), nullable=True, comment="医护人员")
|
||||||
sales_user_id = Column(String(64), nullable=True, comment="销售人员用户ID")
|
sales_user_id = Column(String(64), nullable=True, comment="市场专员用户ID")
|
||||||
|
region_manager_user_id = Column(String(64), nullable=True, comment="地区经理用户ID")
|
||||||
sample_collect_time = Column(DateTime, nullable=True, comment="样本采集时间")
|
sample_collect_time = Column(DateTime, nullable=True, comment="样本采集时间")
|
||||||
maternal_blood_collect_time = Column(DateTime, nullable=True, comment="母血采集时间")
|
maternal_blood_collect_time = Column(DateTime, nullable=True, comment="母血采集时间")
|
||||||
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
|
||||||
|
|||||||
@@ -0,0 +1,575 @@
|
|||||||
|
"""移动市场接口的白名单请求 DTO。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import List, Literal, Optional
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
AliasChoices,
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
condecimal,
|
||||||
|
constr,
|
||||||
|
field_validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MarketPaymentAmount = condecimal(
|
||||||
|
gt=Decimal('0'),
|
||||||
|
max_digits=18,
|
||||||
|
decimal_places=2,
|
||||||
|
allow_inf_nan=False,
|
||||||
|
)
|
||||||
|
MarketContractAmount = condecimal(
|
||||||
|
ge=Decimal('0'),
|
||||||
|
max_digits=18,
|
||||||
|
decimal_places=2,
|
||||||
|
allow_inf_nan=False,
|
||||||
|
)
|
||||||
|
MarketSampleTypeId = constr(min_length=1, max_length=64)
|
||||||
|
|
||||||
|
_CUSTOMER_INT_LABELS = {
|
||||||
|
'birth_order': '胎次',
|
||||||
|
'newborn_weight_gram': '新生儿体重',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _blank_to_none(value):
|
||||||
|
"""移动端数字框清空后会提交 "",可选数字应视为未填。"""
|
||||||
|
if isinstance(value, str):
|
||||||
|
text = value.strip()
|
||||||
|
return None if text == '' else text
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_customer_int(value, field_name: str):
|
||||||
|
if value is None or isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
label = _CUSTOMER_INT_LABELS[field_name]
|
||||||
|
if isinstance(value, float):
|
||||||
|
if value.is_integer():
|
||||||
|
return int(value)
|
||||||
|
raise ValueError(f'{label}须为整数')
|
||||||
|
if isinstance(value, str):
|
||||||
|
text = value.strip().replace(',', '')
|
||||||
|
if text == '':
|
||||||
|
return None
|
||||||
|
if re.fullmatch(r'[+-]?\d+\.?', text):
|
||||||
|
return int(text.rstrip('.'))
|
||||||
|
if re.fullmatch(r'[+-]?\d+\.\d+', text):
|
||||||
|
number = Decimal(text)
|
||||||
|
if number == number.to_integral_value():
|
||||||
|
return int(number)
|
||||||
|
raise ValueError(f'{label}须为整数')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class MarketAttachmentDownloadBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
file_url: str = Field(..., min_length=1, max_length=1000, alias='fileUrl')
|
||||||
|
file_name: Optional[str] = Field(None, max_length=200, alias='fileName')
|
||||||
|
|
||||||
|
|
||||||
|
class MarketPresignTransferBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
transfer_amount: Optional[MarketPaymentAmount] = Field(
|
||||||
|
None, alias="transferAmount"
|
||||||
|
)
|
||||||
|
transfer_time: Optional[str] = Field(None, alias="transferTime")
|
||||||
|
transfer_type: Optional[str] = Field(
|
||||||
|
None, max_length=16, alias="transferType"
|
||||||
|
)
|
||||||
|
transfer_attachments: Optional[str] = Field(
|
||||||
|
None, max_length=4000, alias="transferAttachments"
|
||||||
|
)
|
||||||
|
remark: Optional[str] = Field(None, max_length=512)
|
||||||
|
|
||||||
|
@field_validator('transfer_time')
|
||||||
|
@classmethod
|
||||||
|
def validate_transfer_time(cls, value: Optional[str]) -> Optional[str]:
|
||||||
|
if value in (None, ''):
|
||||||
|
return value
|
||||||
|
text = str(value).strip()
|
||||||
|
if re.fullmatch(
|
||||||
|
r'(?:\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
|
||||||
|
r'|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}'
|
||||||
|
r'(?::\d{2}(?:\.\d{1,6})?)?)',
|
||||||
|
text,
|
||||||
|
) is None:
|
||||||
|
raise ValueError('转账时间格式无效')
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(text)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError('转账时间格式无效') from exc
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class MarketPresignOrderBody(BaseModel):
|
||||||
|
"""预签单可写字段;租户与财务字段不接受客户端输入。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
order_date: Optional[str] = Field(None, alias="orderDate")
|
||||||
|
protocol_no: Optional[str] = Field(
|
||||||
|
None, max_length=64, alias="protocolNo"
|
||||||
|
)
|
||||||
|
mother_name: Optional[str] = Field(
|
||||||
|
None, max_length=64, alias="motherName"
|
||||||
|
)
|
||||||
|
expected_delivery_date: Optional[str] = Field(
|
||||||
|
None, alias="expectedDeliveryDate"
|
||||||
|
)
|
||||||
|
region_id: Optional[str] = Field(
|
||||||
|
None, max_length=64, alias="regionId"
|
||||||
|
)
|
||||||
|
hospital_id: Optional[str] = Field(
|
||||||
|
None, max_length=64, alias="hospitalId"
|
||||||
|
)
|
||||||
|
sample_collect_time: Optional[str] = Field(None, alias="sampleCollectTime")
|
||||||
|
pay_mode: Optional[str] = Field(None, max_length=8, alias="payMode")
|
||||||
|
contract_amount: Optional[MarketContractAmount] = Field(
|
||||||
|
None, alias="contractAmount"
|
||||||
|
)
|
||||||
|
collector_user_id: Optional[str] = Field(
|
||||||
|
None, max_length=64, alias="collectorUserId"
|
||||||
|
)
|
||||||
|
region_manager_user_id: Optional[str] = Field(
|
||||||
|
None, max_length=64, alias="regionManagerUserId"
|
||||||
|
)
|
||||||
|
order_status: Optional[
|
||||||
|
Literal['draft', 'active', 'converted', 'void']
|
||||||
|
] = Field(None, alias='orderStatus')
|
||||||
|
remark: Optional[str] = Field(None, max_length=512)
|
||||||
|
sample_type_ids: Optional[List[MarketSampleTypeId]] = Field(
|
||||||
|
None, min_length=1, max_length=100, alias="sampleTypeIds"
|
||||||
|
)
|
||||||
|
transfers: Optional[List[MarketPresignTransferBody]] = None
|
||||||
|
|
||||||
|
@field_validator('order_date', 'expected_delivery_date')
|
||||||
|
@classmethod
|
||||||
|
def validate_ymd(cls, value: Optional[str]) -> Optional[str]:
|
||||||
|
if value in (None, ''):
|
||||||
|
return value
|
||||||
|
text = str(value).strip()
|
||||||
|
if len(text) != 8 or not text.isdigit():
|
||||||
|
raise ValueError('日期必须使用 YYYYMMDD 格式')
|
||||||
|
try:
|
||||||
|
datetime.strptime(text, '%Y%m%d')
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError('日期必须是有效的 YYYYMMDD') from exc
|
||||||
|
return text
|
||||||
|
|
||||||
|
@field_validator('mother_name')
|
||||||
|
@classmethod
|
||||||
|
def validate_mother_name(cls, value: Optional[str]) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
raise ValueError('母亲姓名不能为空')
|
||||||
|
return text
|
||||||
|
|
||||||
|
@field_validator('sample_collect_time')
|
||||||
|
@classmethod
|
||||||
|
def validate_sample_collect_time(
|
||||||
|
cls, value: Optional[str]
|
||||||
|
) -> Optional[str]:
|
||||||
|
if value in (None, ''):
|
||||||
|
return value
|
||||||
|
text = str(value).strip()
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(text.replace('Z', '+00:00'))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError('采集时间格式无效') from exc
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class MarketPresignOrderCreateBody(MarketPresignOrderBody):
|
||||||
|
"""新增预签单必填字段;销售专员始终由服务端身份决定。"""
|
||||||
|
|
||||||
|
mother_name: str = Field(min_length=1, max_length=64, alias='motherName')
|
||||||
|
sample_type_ids: List[MarketSampleTypeId] = Field(
|
||||||
|
min_length=1, max_length=100, alias='sampleTypeIds'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolCustomerBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
customer_id: Optional[str] = Field(None, alias='customerId')
|
||||||
|
father_customer_id: Optional[str] = Field(None, alias='fatherCustomerId')
|
||||||
|
newborn_customer_id: Optional[str] = Field(None, alias='newbornCustomerId')
|
||||||
|
mother_name: Optional[str] = Field(None, alias='motherName')
|
||||||
|
mother_phone: Optional[str] = Field(None, alias='motherPhone')
|
||||||
|
mother_id_card: Optional[str] = Field(None, alias='motherIdCard')
|
||||||
|
mother_birth_ym: Optional[str] = Field(None, alias='motherBirthYm')
|
||||||
|
mother_ethnic: Optional[str] = Field(None, alias='motherEthnic')
|
||||||
|
mother_email: Optional[str] = Field(None, alias='motherEmail')
|
||||||
|
mother_address: Optional[str] = Field(None, alias='motherAddress')
|
||||||
|
father_name: Optional[str] = Field(None, alias='fatherName')
|
||||||
|
father_phone: Optional[str] = Field(None, alias='fatherPhone')
|
||||||
|
father_id_card: Optional[str] = Field(None, alias='fatherIdCard')
|
||||||
|
newborn_name: Optional[str] = Field(None, alias='newbornName')
|
||||||
|
newborn_id_card: Optional[str] = Field(None, alias='newbornIdCard')
|
||||||
|
expected_delivery_date: Optional[str] = Field(None, alias='expectedDeliveryDate')
|
||||||
|
newborn_delivery_time: Optional[str] = Field(None, alias='newbornDeliveryTime')
|
||||||
|
newborn_sex: Optional[str] = Field(None, alias='newbornSex')
|
||||||
|
delivery_mode: Optional[str] = Field(None, alias='deliveryMode')
|
||||||
|
birth_order: Optional[int] = Field(None, alias='birthOrder')
|
||||||
|
newborn_weight_gram: Optional[int] = Field(None, alias='newbornWeightGram')
|
||||||
|
apgar_1min: Optional[float] = Field(None, alias='apgar1min')
|
||||||
|
apgar_5min: Optional[float] = Field(None, alias='apgar5min')
|
||||||
|
collector_user_id: Optional[str] = Field(None, alias='collectorUserId')
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
@field_validator('birth_order', 'newborn_weight_gram', mode='before')
|
||||||
|
@classmethod
|
||||||
|
def normalize_optional_int(cls, value, info):
|
||||||
|
return _optional_customer_int(value, info.field_name)
|
||||||
|
|
||||||
|
@field_validator('apgar_1min', 'apgar_5min', mode='before')
|
||||||
|
@classmethod
|
||||||
|
def normalize_optional_float(cls, value):
|
||||||
|
return _blank_to_none(value)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolCollectionBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
region_id: Optional[str] = Field(None, alias='regionId')
|
||||||
|
hospital_id: Optional[str] = Field(None, alias='hospitalId')
|
||||||
|
medical_staff: Optional[str] = Field(None, alias='medicalStaff')
|
||||||
|
sales_user_id: Optional[str] = Field(None, alias='salesUserId')
|
||||||
|
region_manager_user_id: Optional[str] = Field(
|
||||||
|
None, alias='regionManagerUserId'
|
||||||
|
)
|
||||||
|
sample_collect_time: Optional[str] = Field(None, alias='sampleCollectTime')
|
||||||
|
maternal_blood_collect_time: Optional[str] = Field(
|
||||||
|
None, alias='maternalBloodCollectTime'
|
||||||
|
)
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolSampleFreightDraftBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
client_key: Optional[str] = Field(None, alias='clientKey')
|
||||||
|
transport_mode: Optional[str] = Field(None, alias='transportMode')
|
||||||
|
freight_amount: Optional[MarketContractAmount] = Field(None, alias='freightAmount')
|
||||||
|
waybill_no: Optional[str] = Field(None, alias='waybillNo')
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolSampleBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
id: Optional[str] = Field(None, max_length=64, alias='sampleId')
|
||||||
|
sample_type_id: Optional[str] = Field(None, alias='sampleTypeId')
|
||||||
|
sample_code_id: Optional[str] = Field(None, alias='sampleCodeId')
|
||||||
|
sample_code: Optional[str] = Field(None, alias='sampleCode')
|
||||||
|
collect_amount: Optional[float] = Field(None, alias='collectAmount')
|
||||||
|
unit_id: Optional[str] = Field(None, alias='unitId')
|
||||||
|
price: Optional[MarketContractAmount] = None
|
||||||
|
original_price: Optional[MarketContractAmount] = Field(None, alias='originalPrice')
|
||||||
|
storage_years: Optional[int] = Field(None, alias='storageYears')
|
||||||
|
sample_status: Optional[str] = Field(None, alias='sampleStatus')
|
||||||
|
is_special: Optional[int] = Field(None, alias='isSpecial')
|
||||||
|
special_remark: Optional[str] = Field(None, alias='specialRemark')
|
||||||
|
remark: Optional[str] = None
|
||||||
|
eta_time: Optional[str] = Field(None, alias='etaTime')
|
||||||
|
freight_id: Optional[str] = Field(None, alias='freightId')
|
||||||
|
freight_draft: Optional[MarketProtocolSampleFreightDraftBody] = Field(
|
||||||
|
None, alias='freightDraft'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolAttachmentsBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
protocol_attachments: Optional[str] = Field(
|
||||||
|
None, max_length=4000, alias='protocolAttachments'
|
||||||
|
)
|
||||||
|
consent_form_urls: Optional[str] = Field(
|
||||||
|
None, max_length=4000, alias='consentFormUrls'
|
||||||
|
)
|
||||||
|
health_survey_form_urls: Optional[str] = Field(
|
||||||
|
None, max_length=4000, alias='healthSurveyFormUrls'
|
||||||
|
)
|
||||||
|
attachment_urls: Optional[str] = Field(
|
||||||
|
None, max_length=4000, alias='attachmentUrls'
|
||||||
|
)
|
||||||
|
infectious_disease_report_urls: Optional[str] = Field(
|
||||||
|
None, max_length=4000, alias='infectiousDiseaseReportUrls'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolNestedTransferBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
id: Optional[str] = None
|
||||||
|
transfer_amount: Optional[MarketPaymentAmount] = Field(None, alias='transferAmount')
|
||||||
|
transfer_time: Optional[str] = Field(None, alias='transferTime')
|
||||||
|
transfer_type: Optional[str] = Field(None, max_length=16, alias='transferType')
|
||||||
|
transfer_attachments: Optional[str] = Field(None, max_length=4000, alias='transferAttachments')
|
||||||
|
remark: Optional[str] = Field(None, max_length=512)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolPaymentScheduleBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
id: Optional[str] = None
|
||||||
|
schedule_no: Optional[int] = Field(None, alias='scheduleNo')
|
||||||
|
due_amount: Optional[MarketContractAmount] = Field(None, alias='dueAmount')
|
||||||
|
due_date: Optional[str] = Field(None, alias='dueDate')
|
||||||
|
schedule_status: Optional[str] = Field(None, alias='scheduleStatus')
|
||||||
|
transfers: Optional[List[MarketProtocolNestedTransferBody]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolSamplePaymentBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
id: Optional[str] = None
|
||||||
|
sample_code: str = Field(..., min_length=1, alias='sampleCode')
|
||||||
|
protocol_sample_id: Optional[str] = Field(None, alias='protocolSampleId')
|
||||||
|
pay_mode: str = Field(..., alias='payMode')
|
||||||
|
contract_amount: Optional[MarketContractAmount] = Field(None, alias='contractAmount')
|
||||||
|
lump_sum_deadline: Optional[str] = Field(None, alias='lumpSumDeadline')
|
||||||
|
installment_count: Optional[int] = Field(None, alias='installmentCount')
|
||||||
|
installment_interval_months: Optional[float] = Field(
|
||||||
|
None, alias='installmentIntervalMonths'
|
||||||
|
)
|
||||||
|
installment_start_date: Optional[str] = Field(
|
||||||
|
None, alias='installmentStartDate'
|
||||||
|
)
|
||||||
|
installment_advance_days: Optional[int] = Field(
|
||||||
|
None, alias='installmentAdvanceDays'
|
||||||
|
)
|
||||||
|
installment_ratios: Optional[List[MarketContractAmount]] = Field(
|
||||||
|
None, alias='installmentRatios'
|
||||||
|
)
|
||||||
|
remark: Optional[str] = None
|
||||||
|
schedules: Optional[List[MarketProtocolPaymentScheduleBody]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolPaymentBody(BaseModel):
|
||||||
|
"""历史 v1 整单缴费方案;不接收协议版本或财务状态字段。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
pay_mode: Literal['dj', 'fq', 'djfq', 'fgd'] = Field(..., alias='payMode')
|
||||||
|
contract_amount: Optional[MarketContractAmount] = Field(
|
||||||
|
None, alias='contractAmount'
|
||||||
|
)
|
||||||
|
lump_sum_deadline: Optional[str] = Field(None, max_length=8, alias='lumpSumDeadline')
|
||||||
|
installment_count: Optional[int] = Field(None, ge=1, alias='installmentCount')
|
||||||
|
installment_interval_months: Optional[Decimal] = Field(
|
||||||
|
None, ge=Decimal('0'), alias='installmentIntervalMonths'
|
||||||
|
)
|
||||||
|
installment_start_date: Optional[str] = Field(
|
||||||
|
None, max_length=8, alias='installmentStartDate'
|
||||||
|
)
|
||||||
|
installment_advance_days: Optional[int] = Field(
|
||||||
|
None, ge=0, le=365, alias='installmentAdvanceDays'
|
||||||
|
)
|
||||||
|
installment_ratios: Optional[List[MarketContractAmount]] = Field(
|
||||||
|
None, alias='installmentRatios'
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator('lump_sum_deadline', 'installment_start_date')
|
||||||
|
@classmethod
|
||||||
|
def validate_ymd(cls, value: Optional[str]) -> Optional[str]:
|
||||||
|
if value in (None, ''):
|
||||||
|
return value
|
||||||
|
text = str(value).strip()
|
||||||
|
if len(text) != 8 or not text.isdigit():
|
||||||
|
raise ValueError('日期必须使用 YYYYMMDD 格式')
|
||||||
|
try:
|
||||||
|
datetime.strptime(text, '%Y%m%d')
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError('日期必须是有效的 YYYYMMDD') from exc
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolPaymentDetailBody(BaseModel):
|
||||||
|
"""历史 v1 缴费明细的市场可写字段白名单。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
id: Optional[str] = Field(None, max_length=64)
|
||||||
|
installment_no: Optional[int] = Field(None, ge=1, alias='installmentNo')
|
||||||
|
installment_due_date: Optional[str] = Field(
|
||||||
|
None, max_length=8, alias='installmentDueDate'
|
||||||
|
)
|
||||||
|
due_amount: Optional[MarketContractAmount] = Field(None, alias='dueAmount')
|
||||||
|
transfer_type: Optional[str] = Field(None, max_length=16, alias='transferType')
|
||||||
|
transfer_amount: Optional[MarketPaymentAmount] = Field(
|
||||||
|
None, alias='transferAmount'
|
||||||
|
)
|
||||||
|
transfer_time: Optional[str] = Field(None, alias='transferTime')
|
||||||
|
transfer_attachments: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
max_length=4000,
|
||||||
|
validation_alias=AliasChoices('transferAttachments', 'attachments'),
|
||||||
|
serialization_alias='transferAttachments',
|
||||||
|
)
|
||||||
|
remark: Optional[str] = Field(None, max_length=512)
|
||||||
|
|
||||||
|
@field_validator('installment_due_date')
|
||||||
|
@classmethod
|
||||||
|
def validate_ymd(cls, value: Optional[str]) -> Optional[str]:
|
||||||
|
if value in (None, ''):
|
||||||
|
return value
|
||||||
|
text = str(value).strip()
|
||||||
|
if len(text) != 8 or not text.isdigit():
|
||||||
|
raise ValueError('日期必须使用 YYYYMMDD 格式')
|
||||||
|
try:
|
||||||
|
datetime.strptime(text, '%Y%m%d')
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError('日期必须是有效的 YYYYMMDD') from exc
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolBody(BaseModel):
|
||||||
|
"""移动协议白名单。协议/捐赠状态可改;审核、财务、退款及转换时间字段一律拒绝。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
source: Literal['cell_storage', 'research_donation']
|
||||||
|
protocol_status: Optional[
|
||||||
|
Literal['dsx', 'sxz', 'ygq', 'yzf', 'sdfq', 'zky']
|
||||||
|
] = Field(None, alias='protocolStatus')
|
||||||
|
donation_status: Optional[
|
||||||
|
Literal['dsx', 'sxz', 'ygq', 'yzf', 'sdfq', 'zckc']
|
||||||
|
] = Field(None, alias='donationStatus')
|
||||||
|
protocol_no: Optional[str] = Field(None, alias='protocolNo')
|
||||||
|
donation_no: Optional[str] = Field(None, alias='donationNo')
|
||||||
|
register_date: Optional[str] = Field(None, alias='registerDate')
|
||||||
|
donation_date: Optional[str] = Field(None, alias='donationDate')
|
||||||
|
collector_user_id: Optional[str] = Field(None, alias='collectorUserId')
|
||||||
|
protocol_total_amount: Optional[MarketContractAmount] = Field(
|
||||||
|
None, alias='protocolTotalAmount'
|
||||||
|
)
|
||||||
|
remark: Optional[str] = None
|
||||||
|
customer: Optional[MarketProtocolCustomerBody] = None
|
||||||
|
collection: Optional[MarketProtocolCollectionBody] = None
|
||||||
|
samples: Optional[List[MarketProtocolSampleBody]] = None
|
||||||
|
attachments: Optional[MarketProtocolAttachmentsBody] = None
|
||||||
|
sample_payments: Optional[List[MarketProtocolSamplePaymentBody]] = Field(
|
||||||
|
None, alias='samplePayments'
|
||||||
|
)
|
||||||
|
payment: Optional[MarketProtocolPaymentBody] = None
|
||||||
|
payment_details: Optional[List[MarketProtocolPaymentDetailBody]] = Field(
|
||||||
|
None, alias='paymentDetails'
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator('register_date', 'donation_date')
|
||||||
|
@classmethod
|
||||||
|
def validate_protocol_ymd(cls, value: Optional[str]) -> Optional[str]:
|
||||||
|
"""接受 YYYYMMDD / YYYY-MM-DD,统一规范为 CHAR(8) YYYYMMDD。"""
|
||||||
|
if value in (None, ''):
|
||||||
|
return value
|
||||||
|
text = str(value).strip().replace('-', '').replace('/', '')
|
||||||
|
if len(text) != 8 or not text.isdigit():
|
||||||
|
raise ValueError('日期必须使用 YYYYMMDD 格式')
|
||||||
|
try:
|
||||||
|
datetime.strptime(text, '%Y%m%d')
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError('日期必须是有效的 YYYYMMDD') from exc
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class MarketProtocolAuditBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
audit_status: Literal['shtg', 'shth'] = Field(..., alias='auditStatus')
|
||||||
|
audit_remark: Optional[str] = Field(None, max_length=512, alias='auditRemark')
|
||||||
|
|
||||||
|
|
||||||
|
class MarketPaymentTransferBody(BaseModel):
|
||||||
|
"""市场侧仅可登记/编辑业务转账草稿,不接收状态及财务字段。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
transfer_amount: Optional[MarketPaymentAmount] = Field(
|
||||||
|
None,
|
||||||
|
alias='transferAmount',
|
||||||
|
)
|
||||||
|
transfer_time: Optional[str] = Field(None, alias='transferTime')
|
||||||
|
transfer_type: Optional[str] = Field(None, max_length=16, alias='transferType')
|
||||||
|
transfer_attachments: Optional[str] = Field(
|
||||||
|
None, max_length=4000, alias='transferAttachments'
|
||||||
|
)
|
||||||
|
remark: Optional[str] = Field(None, max_length=512)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketPaymentTransferCreateBody(MarketPaymentTransferBody):
|
||||||
|
schedule_id: str = Field(..., min_length=1, max_length=64, alias='scheduleId')
|
||||||
|
|
||||||
|
|
||||||
|
class MarketSampleHandleBody(BaseModel):
|
||||||
|
"""市场样本仅允许登记轻量办理信息。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
handle_time: Optional[str] = Field(None, alias='handleTime')
|
||||||
|
handler_user_id: Optional[str] = Field(
|
||||||
|
None, max_length=64, alias='handlerUserId'
|
||||||
|
)
|
||||||
|
attachments: Optional[str] = Field(None, max_length=4000)
|
||||||
|
remark: Optional[str] = Field(None, max_length=512)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketSampleStorageCertBody(MarketSampleHandleBody):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MarketSampleInsuranceBody(MarketSampleHandleBody):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MarketFreightItemBody(BaseModel):
|
||||||
|
"""客户端只提交样本身份;协议与样本编码快照由服务端派生。"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
sample_source: Literal['cell_storage', 'research_donation'] = Field(
|
||||||
|
..., alias='sampleSource'
|
||||||
|
)
|
||||||
|
sample_id: str = Field(..., min_length=1, max_length=64, alias='sampleId')
|
||||||
|
|
||||||
|
|
||||||
|
class MarketFreightItemsBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
items: List[MarketFreightItemBody] = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketFreightCreateBody(MarketFreightItemsBody):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
transport_mode: str = Field(..., min_length=1, max_length=100, alias='transportMode')
|
||||||
|
freight_amount: Optional[Decimal] = Field(
|
||||||
|
None, ge=Decimal('0'), max_digits=18, decimal_places=2,
|
||||||
|
allow_inf_nan=False, alias='freightAmount'
|
||||||
|
)
|
||||||
|
waybill_no: Optional[str] = Field(None, alias='waybillNo')
|
||||||
|
|
||||||
|
|
||||||
|
class MarketFreightUpdateBody(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||||
|
|
||||||
|
transport_mode: Optional[str] = Field(
|
||||||
|
None, min_length=1, max_length=100, alias='transportMode'
|
||||||
|
)
|
||||||
|
freight_amount: Optional[Decimal] = Field(
|
||||||
|
None, ge=Decimal('0'), max_digits=18, decimal_places=2,
|
||||||
|
allow_inf_nan=False, alias='freightAmount'
|
||||||
|
)
|
||||||
|
waybill_no: Optional[str] = Field(None, alias='waybillNo')
|
||||||
Loaded 100 of 853 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user