first commit

This commit is contained in:
strepsiades
2026-03-09 16:31:42 +08:00
commit 4a9c444b10
486 changed files with 52479 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
from pydantic import BaseModel, Field, ValidationError, model_validator
from typing import Any, Dict
from typing import Any, Optional, TypeVar, Generic, List
from pydantic import BaseModel, Field, model_serializer
class BaseResponseWithID(BaseModel):
"""
Mixin: 统一处理输入中的 id/_id -> id
约束:
- 若 id 与 _id 同时出现且值不同 -> 报错
- 若仅 _id 出现 -> 转成 id
- 若两者都缺失 -> 报错
- 最终保证存在 id: str
"""
id: str = Field(..., description="资源唯一标识ID")
@model_validator(mode="before")
@classmethod
def normalize_id(cls, data: Any) -> Any:
# 仅处理 dict-like 输入;其他情况交给 Pydantic 默认流程
if not isinstance(data, dict):
return data
has_id = "id" in data
has__id = "_id" in data
if has_id and has__id:
if str(data["id"]) != str(data["_id"]):
raise ValidationError([
{
"type": "value_error",
"loc": ("id",),
"msg": "id and _id both provided but not equal",
"input": data.get("id"),
}
], cls)
# 相同则优先保留 id,去掉 _id
data = {**data}
data.pop("_id", None)
return data
if has__id and not has_id:
# 将 _id 统一映射为 id
data = {**data}
data["id"] = str(data["_id"])
data.pop("_id", None)
return data
if not has_id and not has__id:
raise ValidationError([
{
"type": "value_error.missing",
"loc": ("id",),
"msg": "id or _id is required",
"input": data,
}
], cls)
# 默认返回(已有 id
return data
class RequestApprovalMixin(BaseModel):
"""
请求审批相关的基础模型
"""
pass
T = TypeVar("T")
# def _build_full_response_dict(code: int, message: Optional[str], data: Any) -> dict:
# """构建与 __orjson__ 完全一致的响应字典"""
# # 处理 data 的序列化(与 __orjson__ 逻辑同步)
# if data is None:
# clean_data = {}
# elif isinstance(data, BaseModel):
# clean_data = data.model_dump()
# else:
# clean_data = data
# return {
# "code": code,
# "message": message,
# "data": clean_data
# }
class Success(BaseModel, Generic[T]):
code: int = 200
message: Optional[str] = "OK"
data: Optional[T] = None
# def __init__(self, **kwargs):
# super().__init__(**kwargs)
# # 自动捕获完整响应结构到 push_log.result
# push_log = current_push_log.get()
# if push_log is not None:
# full_response = _build_full_response_dict(self.code, self.message, self.data)
# push_log.result = full_response
#
class Fail(BaseModel):
code: int = 400
message: Optional[str] = None
data: Optional[Any] = Field(default_factory=dict)
class PaginatedList(BaseModel, Generic[T]):
list: List[T] = Field(default_factory=list)
total: int = 0
page: int = 1
page_size: int = 20
+13
View File
@@ -0,0 +1,13 @@
from datetime import datetime, date
import pytz
def get_current_date(date_formate='%Y-%m-%d'):
"""
获取当前日期字符串,格式化为指定格式,默认格式为'YYYY-MM-DD'
"""
tz = pytz.timezone('Asia/Shanghai') # 设置为中国时区
current_date = datetime.now(tz)
return current_date.strftime(date_formate)
def datetime_date(date: str) -> date:
return datetime.strptime(date, "%Y-%m-%d").date()
+9
View File
@@ -0,0 +1,9 @@
from enum import Enum
class ApprovalStatus(str, Enum):
REGISTERED = "registered"
UNDER_REVIEW = "under_review"
APPROVED = "approved"
REJECTED = "rejected"
PENDING_CONFIRM = "pending_confirm"
CONFIRMED = "confirmed"
+178
View File
@@ -0,0 +1,178 @@
"""
推送接口通用schema
## 响应相关
1. BaseResponseSchema: 所有接口响应的基础schema
2. BasePushResponseSchema: 推送接口响应的基础schema,包含push_id
3. PushResultSchema: 查询推送结果的schema
## 请求相关
4. BaseCreateRequestSchema[T]: 创建资源的基础请求schema(泛型),业务数据在data字段
5. BaseUpdateRequestSchema[T]: 更新资源的基础请求schema(泛型),业务数据在data字段
6. BaseDeleteRequestSchema: 删除资源的基础请求schema,只需resource_id
7. BulkPushRequestSchema: 批量推送请求的schema
## 说明
- 所有元数据字段(timestamp, nonce, signature, uid, push_id)在请求体最外层
- push_id使用UUIDv7格式,支持时间排序,同时作为幂等键和查询键
- 业务数据嵌套在data字段中(删除操作除外,只需resource_id
- 所有业务Schema必须继承BasePushData,确保包含project_type字段
- resource_id在外层,用于标识要操作的资源(更新/删除时必填)
- 使用泛型T可快速定义各业务的请求schemaT必须继承BasePushData
"""
from pydantic import BaseModel, Field
from typing import TypeVar, Generic, Optional, List, Dict, Any
from ..project.enums import ProjectType
# 定义泛型
T = TypeVar('T', bound='BasePushData')
# ==================== 业务数据基类 ====================
class BasePushData(BaseModel):
"""
所有推送业务数据的基类
所有业务Schema必须继承此类,确保包含project_type字段
"""
project_type: ProjectType = Field(..., description="项目类型:offshore_wind/pumped_storage/photovoltaic等")
# ==================== Mixin ====================
class SignatureMixin(BaseModel):
"""签名字段混入"""
timestamp: int = Field(..., description="毫秒级 Unix 时间戳")
nonce: str = Field(..., description="随机字符串,防止重放攻击")
sign: str = Field(..., description="签名字符串")
uid: str = Field(..., description="集团侧用户id")
class PushRequestMixin(BaseModel):
"""推送ID字段混入"""
push_id: str = Field(..., description="推送ID,客户端生成的唯一标识(UUIDv7,支持时间排序)")
# ==================== 响应相关 ====================
class BaseResponseSchema(BaseModel):
"""所有接口响应的基础schema"""
code: int = 200
message: Optional[str] = "OK"
data: Optional[T] = None
class BasePushResponseSchema(BaseModel):
"""推送接口响应的基础schema"""
biz_id: str | None = Field(None, description="资源ID(create操作成功后返回新创建的IDupdate/delete返回原ID")
class PushResultSchema(BaseModel):
"""查询推送结果的schema"""
push_id: str = Field(..., description="推送ID(客户端生成的UUIDv7")
status: str = Field(..., description="推送状态:queued/processing/success/failed")
result: Dict[str, Any] | None = Field(None, description="处理结果(成功时包含created_id等信息)")
error: str | None = Field(None, description="错误信息(失败时)")
created_at: str = Field(..., description="推送创建时间(ISO 8601格式)")
updated_at: str = Field(..., description="推送更新时间(ISO 8601格式)")
class PushIdsSchema(BaseModel):
"""支持从请求体接收的 push_ids 列表字符串(逗号拼接)"""
push_ids: Optional[str] = Field(None, description="推送id字符串逗号拼接")
# ==================== 请求相关 ====================
class BaseCreateRequestSchema(SignatureMixin, PushRequestMixin, BaseModel, Generic[T]):
"""
创建资源的基础请求schema(泛型)
泛型T必须继承BasePushData,确保包含project_type字段
使用示例:
class ProjectInputSchema(BasePushData):
name: str = Field(...)
capacity: float = Field(...)
class ProjectCreateRequest(BaseCreateRequestSchema[ProjectInputSchema]):
pass
"""
# 业务数据(嵌套)
data: T = Field(..., description="业务数据")
class BaseUpdateRequestSchema(SignatureMixin, PushRequestMixin, BaseModel, Generic[T]):
"""
更新资源的基础请求schema(泛型)
泛型T必须继承BasePushData,确保包含project_type字段
resource_id在外层,用于标识要更新的资源
使用示例:
class ProjectInputSchema(BasePushData):
name: str = Field(...)
capacity: float = Field(...)
class ProjectUpdateRequest(BaseUpdateRequestSchema[ProjectInputSchema]):
pass
"""
# 资源标识
biz_id: str = Field(..., description="要更新的资源ID,类型id,如project传project_id,unit传unit_id")
# 业务数据(嵌套)
data: T = Field(..., description="业务数据(只需包含要更新的字段)")
class BaseDeleteRequestSchema(SignatureMixin, PushRequestMixin, BaseModel):
"""
删除资源的基础请求schema
使用示例:
class ProjectDeleteRequest(BaseDeleteRequestSchema):
pass
"""
# 资源标识
biz_id: str = Field(..., description="要删除的资源ID")
class BulkPushItem(PushRequestMixin, BaseModel):
"""
批量推送中的单个项目
注意:
- action为create时:data必填,resource_id留空
- action为update时:data和resource_id都必填
- action为delete时:resource_id必填,data留空
- data必须继承BasePushData,确保包含project_type字段
"""
action: str = Field(..., description="操作类型:create/update/delete")
resource_id: str | None = Field(None, description="资源IDupdate/delete时必填,create时留空)")
data: BasePushData | None = Field(None, description="业务数据(create/update时必填,delete时留空,必须包含project_type字段)")
class BulkPushItemResult(BaseModel):
"""批量推送中单个项目的处理结果"""
push_id: str = Field(..., description="推送ID(对应请求中的push_id")
status: str = Field(..., description="处理状态:queued/processing/success/failed")
resource_id: str | None = Field(None, description="资源IDcreate成功时返回新IDupdate/delete返回原ID")
error: str | None = Field(None, description="错误信息(失败时)")
class BulkPushRequestSchema(SignatureMixin, BaseModel):
"""
批量推送请求的schema
一次请求可推送多条数据,每条都有独立的幂等键和操作类型
"""
# 批量数据
items: List[BulkPushItem] = Field(..., description="批量推送的数据列表")
class BulkPushResponseSchema(BaseResponseSchema):
"""批量推送响应的schema"""
batch_id: str = Field(..., description="批量推送的批次ID")
total: int = Field(..., description="总数量")
results: List[BulkPushItemResult] = Field(..., description="每条数据的处理结果,包含push_id和resource_id")
+85
View File
@@ -0,0 +1,85 @@
from datetime import datetime
from typing import Annotated
from pydantic import AfterValidator
from functools import lru_cache
import re
# 预编译正则表达式,避免重复编译
DATE_PATTERN = re.compile(r'^\d{4}-\d{2}-\d{2}$')
TIMESTAMP_PATTERNS = {
'iso_z': re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$'),
'iso_offset': re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+\+00:00$'),
'simple_space': re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'),
'simple_t': re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$'),
}
@lru_cache(maxsize=1024) # 缓存最近1024个不同的日期
def _validate_date_cached(v: str) -> str:
"""缓存版本的日期验证"""
# 快速格式检查(避免无效字符串进入 strptime)
if not DATE_PATTERN.match(v):
raise ValueError(f"日期格式应为 YYYY-MM-DD,实际为: {v}")
# 验证日期有效性(例如排除 2023-02-30)
datetime.strptime(v, "%Y-%m-%d")
return v
def validate_date(v: str) -> str:
"""校验日期格式为 YYYY-MM-DD"""
if v is None or v == "":
return v
return _validate_date_cached(v)
@lru_cache(maxsize=2048) # 时间戳变化更多,给更大的缓存
def _validate_timestamp_cached(v: str) -> str:
"""缓存版本的时间戳验证"""
# 按常见程度排序,优先检查最可能的格式
if TIMESTAMP_PATTERNS['iso_z'].match(v):
datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%fZ")
elif TIMESTAMP_PATTERNS['simple_space'].match(v):
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
elif TIMESTAMP_PATTERNS['simple_t'].match(v):
datetime.strptime(v, "%Y-%m-%dT%H:%M:%S")
elif TIMESTAMP_PATTERNS['iso_offset'].match(v):
datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f+00:00")
else:
# 兜底逻辑:如果正则都不匹配,尝试原来的逻辑
try:
if v.endswith('Z'):
datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%fZ")
elif v.endswith('+00:00'):
datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f+00:00")
else:
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
datetime.strptime(v, "%Y-%m-%dT%H:%M:%S")
except ValueError:
raise ValueError("时间戳格式应为 YYYY-MM-DDTHH:MM:SS.sssZ、+00:00、YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DDTHH:MM:SS")
return v
def validate_timestamp(v: str) -> str:
"""校验时间戳格式"""
if v is None or v == "":
return v
return _validate_timestamp_cached(v)
# 定义新类型
DateStr = Annotated[str, AfterValidator(validate_date)]
TimestampStr = Annotated[str, AfterValidator(validate_timestamp)]
# 可选:提供缓存统计功能用于调试
def get_cache_stats():
"""获取缓存统计信息,用于性能调试"""
return {
'date_cache': _validate_date_cached.cache_info(),
'timestamp_cache': _validate_timestamp_cached.cache_info(),
}
def clear_validation_cache():
"""清空验证缓存(用于测试或内存清理)"""
_validate_date_cached.cache_clear()
_validate_timestamp_cached.cache_clear()
+40
View File
@@ -0,0 +1,40 @@
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional, Literal
from ..common.base import BaseResponseWithID
class CompanyResponse(BaseResponseWithID):
code: str = Field(..., description="公司编码")
company_type: str = Field(..., description="公司类型")
name: str = Field(..., description="公司名称")
short_name: str = Field(..., description="公司简称")
parent_id: Optional[str] = Field(None, description="父公司ID")
path: List[str] = Field(default_factory=list, description="公司路径 [..., 上级id, 本身id]")
path_name: List[str] = Field(default_factory=list, description="公司路径名称 [..., 上级名称, 本身名称]")
province: Optional[str] = Field(None, description="省份编码") # 存省份编码
city: Optional[str] = Field(None, description="城市编码") # 存城市编码
sort: int = Field(0, description="排序")
status: str = Field("", description="公司状态")
company_nature: Optional[list] = Field(..., description="公司性质")
credit_code: Optional[str] = Field(None, description="信用代码", examples=["914403007109310121"])
city_name: Optional[str] = Field(None, description="城市名称", examples=[None])
province_name: Optional[str] = Field(None, description="省份名称", examples=[None])
parent_name: Optional[str] = Field(None, description="父级公司名称", examples=[None])
@field_validator(
"path",
"path_name",
mode="before"
)
def formate_null_to_list(cls, field_value):
if field_value is None:
return []
return field_value
class CompanyListResponse(BaseModel):
page: int
page_size: int
total: int
list: List[CompanyResponse] = []
+121
View File
@@ -0,0 +1,121 @@
from pydantic import BaseModel, Field, field_validator, model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
quantity: float = Field(..., description="计划完成量", examples=[100.0])
is_audit: Optional[bool] = Field(False, description="是否已审核", examples=[False])
@field_validator("quantity")
def validate_price(cls, v):
"""验证金额必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='计划完成量不能小于等于0')
return v
class ConstructionResponse(BaseResponseWithID):
"""
施工任务模型,存储在CouchDB中的施工任务文档
"""
id : str = Field(..., description="施工任务ID")
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"])
show_name: str = Field(..., description="展示名称", examples=["桩基础施工任务"])
construction_section: str = Field(..., description="施工区域", examples=["GF"])
status: Optional[int] = Field(None, description="施工状态", examples=[0])
task_type: str = Field(..., description="施工任务类型", examples=["zjcsgg"])
plan_start_date: Optional[DateStr] = Field(None, description="计划开工时间", examples=["2024-01-01"])
plan_complete_date: Optional[DateStr] = Field(None, description="计划完工时间", examples=["2024-06-01"])
actual_complete_date: Optional[DateStr] = Field(None, description="实际完工时间", examples=["2024-05-30"])
complete_quantity: float = Field(0.0, description="实际完成量累计", examples=[0.0])
complete_rate: float = Field(0.0, description="实际完成率(%)", examples=[0.0])
contract_quantity: Optional[float] = Field(None, description="设计量", examples=[1000.0])
plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表")
statistic_date: Optional[DateStr] = Field(None, description="统计节点时间", examples=["2024-05-31"])
# sort: int = Field(0, description="排序字段", examples=[0])
is_crucial:Optional[bool] = Field(False, description="是否为关键线路")
# 2025-10-20新增字段,延迟时间
delay_days: Optional[int] = Field(None, description="延迟时间(天)", examples=[0])
class ConstructionCreate(BaseModel):
"""
施工任务创建模型
不对应operation
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"])
show_name: str = Field(..., description="展示名称", examples=["桩基础施工任务"])
construction_section: str = Field(..., description="施工区域", examples=["GF"])
task_type: str = Field(..., description="施工任务类型", examples=["zjcsgg"])
is_crucial:Optional[bool] = Field(False, description="是否为关键线路")
class ConstructionUpdate(BaseModel):
"""
施工任务更新模型
CONSTRUCTION_BASE_INFO = "construction.base_info"
"""
id: str = Field(..., description="施工任务ID")
contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"])
show_name: str | None = Field(None, description="展示名称(仅QT类施工可以修改名称)", examples=["桩基础施工任务"])
is_crucial:bool | None = Field(False, description="是否为关键线路")
class ConstructionPlanUpdate(BaseModel):
"""
施工计划更新模型
对应 CONSTRUCTION_PLAN = "construction.plan"
业务逻辑说明:
- API要求计划节点时间必须晚于计划开工日期(不能等于)
- 自动清理脏数据:如果 plan_node 有2个或以上节点,删除:
1. 开头节点:如果其 date 等于 plan_start_date
2. 结尾节点:如果其 date 等于 plan_complete_date
- 这样避免提交包含边界节点的数据导致API报错
"""
id: str = Field(..., description="施工任务ID")
plan_start_date: Optional[str] = Field(None, description="计划开工时间", examples=["2024-01-01"])
plan_complete_date: Optional[str] = Field(None, description="计划完工时间", examples=["2024-01-01"])
contract_quantity: float = Field(..., description="设计量")
plan_node: Optional[List[PlanNode]] = Field(default_factory=list, description="计划节点列表")
@field_validator("contract_quantity")
def validate_price(cls, v):
"""验证金额必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='设计量不能小于0')
return v
@model_validator(mode='after')
def clean_plan_nodes(self):
"""
清理计划节点列表中的边界节点
如果 plan_node 有2个或以上节点,删除:
- 开头节点:date 等于 plan_start_date
- 结尾节点:date 等于 plan_complete_date
这避免了API报错"计划节点时间必须晚于计划开工日期"
"""
if not self.plan_node or len(self.plan_node) < 2:
return self
cleaned_nodes = list(self.plan_node)
# 删除开头的边界节点
if (self.plan_start_date and
cleaned_nodes and
cleaned_nodes[0].date == self.plan_start_date):
cleaned_nodes.pop(0)
# 删除结尾的边界节点
if (self.plan_complete_date and
cleaned_nodes and
cleaned_nodes[-1].date == self.plan_complete_date):
cleaned_nodes.pop()
self.plan_node = cleaned_nodes
return self
+133
View File
@@ -0,0 +1,133 @@
from pydantic import BaseModel, Field,field_validator
from ..common.datetime import get_current_date
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.enums import ApprovalStatus
from datetime import datetime
class ConstructionDetailResponse(BaseResponseWithID):
"""
物资明细返回值
"""
id: str = Field(..., description="施工任务明细ID")
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
parent_id: str = Field(..., description="所属施工任务ID", examples=["task789"])
rate: Optional[float] = Field(None, description="实际完成率", examples=[0.0])
quantity: float = Field(0.0, ge=0, description="实际完成量累计", examples=[0.0])
date: DateStr = Field(..., description="节点日期", examples=["2024-05-31"])
status: Optional[int] = Field(None, description="进度状态", examples=[0])
change_count: int = Field(0, description="变更次数", examples=[0])
remark: Optional[str] = Field(None, description="备注")
is_complete: Optional[bool] = Field(
None, description="是否为全部完成", examples=[False]
)
class ConstructionDetailCreate(BaseModel):
"""
施工明细创建模型
CONSTRUCTION_DETAIL = "construction.detail"
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
parent_id: str = Field(..., description="所属施工任务ID", examples=["task789"])
quantity: float = Field(0.0, ge=0, description="实际完成量累计", examples=[0.0])
date: DateStr = Field(..., description="节点日期", examples=["2024-05-31"])
remark: Optional[str] = Field(None, description="备注")
is_complete: Optional[bool] = Field(
False, description="是否为全部完成", examples=[False]
)
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='实际完成量累计不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v > get_current_date():
raise HTTPException(detail='节点日期不能大于今天', status_code=400)
return v
class ConstructionDetailUpdate(BaseModel):
"""
施工明细更新模型
CONSTRUCTION_DETAIL = "construction.detail"
"""
id: str = Field(..., description="施工任务明细ID")
quantity: Optional[float] = Field(None, ge=0, description="实际完成量累计", examples=[0.0])
date: Optional[DateStr] = Field(None, description="节点日期", examples=["2024-05-31"])
remark: Optional[str] = Field(None, description="备注")
is_complete: Optional[bool] = Field(
None, description="是否为全部完成", examples=[False]
)
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus | None = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
if v is None:
return v
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v is None:
return None
if v < 0:
raise HTTPException(status_code=400, detail='实际完成量累计不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v is None:
return None
if v > get_current_date():
raise HTTPException(detail='节点日期不能大于今天', status_code=400)
return v
class ConstructionDetailBulkUpdate(BaseModel):
detail_list: List[ConstructionDetailUpdate]
+122
View File
@@ -0,0 +1,122 @@
from pydantic import BaseModel, Field,field_validator
from typing import List, Optional, Literal
from ..common.datetime import datetime_date,get_current_date
from fastapi import HTTPException
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
class ContractResponse(BaseResponseWithID):
"""
合同返回信息
"""
id : str = Field(..., description="合同ID")
project_id: str = Field("", description="所属项目ID")
name: str = Field("", description="合同名称")
short_name: str = Field("", description="合同简称")
code: str = Field("", description="合同编码")
contract_type: str = Field("", description="合同类型")
currency: str = Field("CNY", description="币种")
manager_name: str = Field("", description="负责人姓名")
manager_phone: str = Field("", description="负责人电话")
sign_company: str = Field("", description="签订公司")
company_id: str = Field("", description="签订公司id")
sign_date: DateStr = Field("", description="签订日期", examples=["2023-10-01"])
credit_code: Optional[str] = Field("", description="签订公司信用代码")
total_price: float = Field(0.0, description="合同金额")
sub_name: Optional[str] = Field(None, description="合同子标题")
class ContractCreate(BaseModel):
"""
创建合同
CONTRACT = "contract"
"""
name: str|None = Field(None, description="合同名称")
sub_name:str|None= Field(None, description="合同子标题")
code: str = Field("", description="合同编码")
contract_type: str = Field("", description="合同类型")
project_id: str = Field("", description="所属项目ID")
currency: str = Field("CNY", description="币种")
manager_name: str = Field("", description="负责人姓名")
manager_phone: str = Field("", description="负责人电话")
sign_company: str = Field("", description="签订公司")
company_id: Optional[str] = Field(None, description="签订公司id")
sign_date: DateStr = Field("", description="签订日期", examples=["2023-10-01"])
credit_code: Optional[str] = Field(None, description="签订公司信用代码")
total_price: float = Field(0.0, description="合同金额")
@field_validator("total_price")
def validate_price(cls, v):
"""验证金额必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='合同金额不能小于0')
return v
@field_validator('sign_date')
def validate_actual_date_not_future(cls, v: str) -> str:
if v == "":
return ""
try:
actual = datetime_date(v)
except ValueError:
raise HTTPException(status_code=400, detail='日期格式为(YYYY-MM-DD)')
today = datetime_date(get_current_date())
if actual > today:
raise HTTPException(status_code=400, detail='签约日期不能晚于今天')
return v
class ContractUpdate(BaseModel):
"""
更新合同
CONTRACT = "contract"
"""
id : str = Field(..., description="合同ID")
sub_name:str|None= Field(None, description="合同子标题")
currency: str|None = Field(None, description="币种")
manager_name: str|None= Field(None, description="负责人姓名")
manager_phone: str|None= Field(None, description="负责人电话")
sign_company: str|None= Field(None, description="签订公司")
company_id: Optional[str] = Field(None, description="签订公司id")
sign_date: str|None = Field(None, description="签订日期", examples=["2023-10-01"])
credit_code: Optional[str] = Field(None, description="签订公司信用代码")
total_price: float|None= Field(None, description="合同金额")
@field_validator("total_price")
def validate_price(cls, v):
"""验证金额必须为非负数"""
if v is None:
return None
if v < 0:
raise HTTPException(status_code=400, detail='合同金额不能小于0')
return v
@field_validator('sign_date')
def validate_actual_date_not_future(cls, v: str):
if v is None:
return None
if v == "":
return ""
try:
actual = datetime_date(v)
except ValueError:
raise HTTPException(status_code=400, detail='日期格式为(YYYY-MM-DD)')
today = datetime_date(get_current_date())
if actual > today:
raise HTTPException(status_code=400, detail='签约日期不能晚于今天')
return v
@@ -0,0 +1,101 @@
from pydantic import Field, BaseModel,field_validator,model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
quantity: float = Field(..., description="计划完成量", examples=[100.0])
is_audit: Optional[bool] = Field(False, description="是否已审核", examples=[False])
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证计划完成量必须大于0"""
if v < 0:
raise HTTPException(status_code=400, detail='计划完成量不能小于0')
return v
class ContractSettlementResponse(BaseResponseWithID):
"""
合同结算返回
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
contract_id: str = Field(..., description="所属合同ID", examples=["contract456"])
contract_type: Optional[str] = Field(None, description="合同类型")
show_name: Optional[str] = Field(None, description="合同简称", examples=["主变采购合同"])
complete_price: float = Field(0.0, description="实际完成金额", examples=[100000.0])
contract_price: float = Field(0.0, description="合同金额", examples=[120000.0])
actual_complete_date: Optional[DateStr] = Field(None, description="实际结束时间", examples=["2024-06-30"])
actual_start_date: Optional[DateStr] = Field(None, description="实际开始时间", examples=["2024-01-01"])
plan_complete_date: Optional[DateStr] = Field(None, description="计划结束时间", examples=["2024-06-30"])
plan_start_date: Optional[DateStr] = Field(None, description="计划开始时间", examples=["2024-01-01"])
plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表")
status: Optional[int] = Field(None,description="供货状态", examples=[0])
# 2025-10-20新增字段,延迟时间
delay_days: Optional[int] = Field(None, description="延迟时间(天)", examples=[0])
statistic_date: Optional[DateStr] = Field(
None, description="统计节点截止日期", examples=["2024-05-31"]
)
class ContractSettlementCreate(BaseModel):
"""
合同结算创建模型
CONTRACT_SETTLEMENT_BASE_INFO = "contract_settlement.base_info"
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
contract_id: str = Field(..., description="所属合同ID", examples=["contract456"])
show_name: Optional[str] = Field(None, description="合同简称", examples=["主变采购合同"])
class ContractSettlementUpdate(BaseModel):
"""
合同结算更新模型
CONTRACT_SETTLEMENT_BASE_INFO = "contract_settlement.base_info"
"""
id: str = Field(..., description="合同结算ID")
contract_id: str = Field(..., description="所属合同ID", examples=["contract456"])
show_name: Optional[str] = Field(None, description="合同简称", examples=["主变采购合同"])
class ContractSettlementPlanUpdate(BaseModel):
"""
合同结算计划节点更新模型
CONTRACT_SETTLEMENT_PLAN_NODE = "contract_settlement.plan_node"
开始时间和金额在合同中维护,这里只维护计划节点和计划结束时间
业务逻辑说明:
- API要求计划节点时间必须晚于计划开始时间(不能等于)
- 自动清理脏数据:如果 plan_node 有2个或以上节点,删除边界节点
"""
id: str = Field(..., description="合同结算ID")
plan_start_date: Optional[DateStr] = Field(None, exclude=True, description="计划开始时间(不推往后端,仅用于本地首节点清洗)", examples=["2024-06-30"])
plan_complete_date: Optional[DateStr] = Field(None, description="计划结束时间", examples=["2024-06-30"])
plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表")
@model_validator(mode='after')
def clean_plan_nodes(self):
"""
清理计划节点列表中的边界节点
注:settlement的plan_start_date在contract中维护,此处利用外部提供的辅助字段清除非法首尾节点。
"""
if not self.plan_node or len(self.plan_node) < 2:
return self
cleaned_nodes = list(self.plan_node)
# 删除开头的边界节点
if (self.plan_start_date and
cleaned_nodes and
cleaned_nodes[0].date == self.plan_start_date):
cleaned_nodes.pop(0)
# 删除结尾的边界节点
if (self.plan_complete_date and
cleaned_nodes and
cleaned_nodes[-1].date == self.plan_complete_date):
cleaned_nodes.pop()
self.plan_node = cleaned_nodes
return self
@@ -0,0 +1,128 @@
from pydantic import Field, BaseModel,field_validator
from fastapi import HTTPException
from ..common.datetime import get_current_date
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.enums import ApprovalStatus
from datetime import datetime
class ContractSettlementDetailResponse(BaseResponseWithID):
"""
合同结算明细返回模型
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
parent_id: str = Field(..., description="所属合同结算ID", examples=["settlement789"])
date: DateStr = Field(..., description="结算日期", examples=["2024-05-31"])
quantity: float = Field(..., description="合同金额", examples=[0])
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: Optional[bool] = Field(None, description="是否完成", examples=[False])
write_off_rate: Optional[float] = Field(None, description="核销率", examples=[0.0])
change_count: int = Field(0, description="变更次数", examples=[0])
status: Optional[int] = Field(None, description="进度状态", examples=[0])
rate: Optional[float] = Field(None, description="实际完成率", examples=[0.0])
class ContractSettlementDetailCreate(BaseModel):
"""
合同结算明细创建模型
CONTRACT_SETTLEMENT_DETAIL = "contract_settlement.detail"
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
parent_id: str = Field(..., description="所属合同结算ID", examples=["settlement789"])
date: DateStr = Field(..., description="结算日期", examples=["2024-05-31"])
quantity: float = Field(..., description="合同金额", examples=[0])
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: bool = Field(False, description="是否完成", examples=[False])
write_off_rate: Optional[float] = Field(None, description="核销率", examples=[0.0])
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='合同金额不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v > get_current_date():
raise HTTPException(detail='结算日期不能大于今天', status_code=400)
return v
class ContractSettlementDetailUpdate(BaseModel):
"""
合同结算明细更新模型
CONTRACT_SETTLEMENT_DETAIL = "contract_settlement.detail"
"""
id: str = Field(..., description="合同明细ID")
date: DateStr | None = Field(None, description="结算日期", examples=["2024-05-31"])
quantity: float | None = Field(None, description="合同金额", examples=[0])
remark: str | None = Field(None, description="填报备注", examples=[""])
is_complete: bool | None = Field(None, description="是否完成", examples=[False])
write_off_rate: float | None = Field(None, description="核销率", examples=[0.0])
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus | None = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
if v is None:
return v
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v is None:
return None
if v < 0:
raise HTTPException(status_code=400, detail='合同金额不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v is None:
return None
if v > get_current_date():
raise HTTPException(detail='结算日期不能大于今天', status_code=400)
return v
class ContractSettlementDetailBulkUpdate(BaseModel):
detail_list: List[ContractSettlementDetailUpdate]
+147
View File
@@ -0,0 +1,147 @@
from pydantic import Field, BaseModel,field_validator,model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
quantity: float = Field(..., description="计划完成量", examples=[100.0])
is_audit: Optional[bool] = Field(False, description="是否已审核", examples=[False])
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='计划完成量不能小于等于0')
return v
class MaterialResponse(BaseResponseWithID):
"""
供货模型
"""
id : str = Field(..., description="物资ID", examples=["material123"])
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
contract_id: str = Field("", description="所属合同ID", examples=["contract456"])
show_name: str = Field("", description="展示名称", examples=["主变"])
material_type: str = Field("", description="物资类型")
contract_quantity: float = Field(0.0, description="合同数量", examples=[1000.0])
plan_start_quantity: float = Field(
0.0, description="计划首批到货量", examples=[1000.0]
)
complete_quantity: float = Field(0.0, description="实际到货累计", examples=[0.0])
complete_rate: float = Field(0.0, description="实际到货率(%)", examples=[0.0])
plan_start_date: Optional[DateStr] = Field(
None, description="计划首批到货时间", examples=["2024-01-01"]
)
plan_complete_date: Optional[DateStr] = Field(
None, description="计划全部到货时间", examples=["2024-06-01"]
)
actual_start_date: Optional[DateStr] = Field(
None, description="实际首批到货时间", examples=["2024-01-15"]
)
actual_complete_date: Optional[DateStr] = Field(
None, description="实际全部到货时间", examples=["2024-06-10"]
)
plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表")
status: Optional[int] = Field(None, description="供货状态", examples=[0])
# 2025-10-20新增字段,延迟时间
delay_days: Optional[int] = Field(None, description="延迟时间(天)", examples=[0])
statistic_date: Optional[DateStr] = Field(
None, description="统计节点截止日期", examples=["2024-05-31"]
)
class MaterialCreate(BaseModel):
"""
创建物资
MATERIAL_BASE_INFO = "material.base_info"
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
contract_id: str = Field(..., description="所属合同ID", examples=["contract456"])
show_name: str = Field(..., description="展示名称", examples=["主变"])
material_type: str = Field(..., description="物资类型")
contract_quantity: float = Field(0.0, description="合同数量", examples=[1000.0])
@field_validator("contract_quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='合同数量不能小于0')
return v
class MaterialUpdate(BaseModel):
"""
更新物资
MATERIAL_BASE_INFO = "material.base_info"
"""
id: str = Field(..., description="物资ID", examples=["material123"])
contract_id: Optional[str] = Field(None, description="所属合同ID", examples=["contract456"])
show_name: Optional[str] = Field(None, description="展示名称", examples=["主变"])
class MaterialPlanUpdate(BaseModel):
"""
物资计划更新模型
MATERIAL_PLAN = "material.plan"
业务逻辑说明:
- API要求计划节点时间必须晚于计划首批到货时间(不能等于)
- 自动清理脏数据:如果 plan_node 有2个或以上节点,删除:
1. 开头节点:如果其 date 等于 plan_start_date
2. 结尾节点:如果其 date 等于 plan_complete_date
- 这样避免提交包含边界节点的数据导致API报错
"""
id: str = Field(..., description="物资ID", examples=["material123"])
plan_start_quantity: Optional[float] = Field(
None, description="计划首批到货量", examples=[1000.0]
)
plan_start_date: Optional[str] = Field(None, description="计划首批到货时间", examples=["2024-01-01"])
plan_complete_date: Optional[str] = Field(None, description="计划全部到货时间", examples=["2024-06-01"])
contract_quantity: float = Field(..., description="合同数量", examples=[1000.0])
plan_node: Optional[List[PlanNode]] = Field(default_factory=list, description="计划节点列表")
@field_validator("contract_quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='合同数量不能小于0')
return v
@field_validator("plan_start_quantity")
def validate_plan_start_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='计划首批到货量不能小于0')
return v
@model_validator(mode='after')
def clean_plan_nodes(self):
"""
清理计划节点列表中的边界节点
如果 plan_node 有2个或以上节点,删除:
- 开头节点:date 等于 plan_start_date
- 结尾节点:date 等于 plan_complete_date
这避免了API报错“计划节点时间必须晚于计划首批到货时间”
"""
if not self.plan_node or len(self.plan_node) < 2:
return self
cleaned_nodes = list(self.plan_node)
# 删除开头的边界节点
if (self.plan_start_date and
cleaned_nodes and
cleaned_nodes[0].date == self.plan_start_date):
cleaned_nodes.pop(0)
# 删除结尾的边界节点
if (self.plan_complete_date and
cleaned_nodes and
cleaned_nodes[-1].date == self.plan_complete_date):
cleaned_nodes.pop()
self.plan_node = cleaned_nodes
return self
+132
View File
@@ -0,0 +1,132 @@
from pydantic import Field, BaseModel,field_validator
from fastapi import HTTPException
from ..common.datetime import get_current_date
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.enums import ApprovalStatus
from datetime import datetime
class MaterialDetailResponse(BaseResponseWithID):
"""
物资明细模型,存储在CouchDB中的物资明细文档
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
parent_id: str = Field(..., description="所属物资ID", examples=["material789"])
date: DateStr = Field(..., description="实际到货日期", examples=["2024-05-31"])
rate: Optional[float] = Field(None, description="实际到货率(%)", examples=[0.0])
quantity: float = Field(0.0, ge=0, description="实际到货量累计", examples=[0.0])
status: Optional[int] = Field(None, description="状态 0:正常 1:超前 -1:延误", examples=[0])
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: Optional[bool] = Field(
None, description="是否为全部到货", examples=[False]
)
class MaterialDetailCreate(BaseModel):
"""
物资明细创建模型
MATERIAL_DETAIL = "material.detail"
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
parent_id: str = Field(..., description="所属物资ID", examples=["material789"])
quantity: float = Field(0.0, ge=0, description="实际到货量累计", examples=[0.0])
date: DateStr = Field(..., description="实际到货日期", examples=["2024-05-31"])
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: Optional[bool] = Field(
None, description="是否为全部到货", examples=[False]
)
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='实际完成量累计不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v > get_current_date():
raise HTTPException(detail='节点日期不能大于今天', status_code=400)
return v
class MaterialDetailUpdate(BaseModel):
"""
物资明细更新模型
MATERIAL_DETAIL = "material.detail"
"""
id: str = Field(..., description="物资明细ID")
quantity: Optional[float] = Field(None, ge=0, description="实际到货量累计", examples=[0.0])
date: Optional[DateStr] = Field(None, description="实际到货日期", examples=["2024-05-31"])
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: Optional[bool] = Field(
None, description="是否为全部到货", examples=[False]
)
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus | None = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
if v is None:
return v
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v is None:
return None
if v < 0:
raise HTTPException(status_code=400, detail='实际完成量累计不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v is None:
return None
if v > get_current_date():
raise HTTPException(detail='节点日期不能大于今天', status_code=400)
return v
class MaterialDetailBulkUpdate(BaseModel):
detail_list: List[MaterialDetailUpdate]
+144
View File
@@ -0,0 +1,144 @@
from pydantic import Field, BaseModel,field_validator,model_validator
from fastapi import HTTPException
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
class PlanNode(BaseModel):
date: DateStr = Field(..., description="计划节点时间", examples=["2024-01-01"])
quantity: float = Field(..., description="计划完成量", examples=[100.0])
is_audit: Optional[bool] = Field(False, description="是否已审核", examples=[False])
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证计划到场人数必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='计划节点人数不能小于0')
return v
class PowerResponse(BaseResponseWithID):
"""
力能模型
"""
code: Optional[str] = Field(
"personnel",
description="力能类型(人员模块拓展为力能模块,可以在本字段保存人员/机具等编码)",
examples=["personnel"],
)
project_id: str = Field(..., description="所属项目", examples=["project123"])
contract_id: Optional[str] = Field(
None,
description="所属合同(目前前端页面显示没有按这个字段,仅通过脚本绑定了epc合同,用于接口数据推送。后续行为待讨论)",
examples=["contract456"],
)
show_name: str = Field("", description="展示名称", examples=["施工班组A"])
status: Optional[int] = Field(None, description="人员状态", examples=[0])
plan_attendance: int = Field(
0,
description="计划出勤人数(暂时定为最后一次报明细时的计划出勤人数)",
examples=[10],
)
actual_daily_attendance: int = Field(0, description="实际日均出勤", examples=[8])
attendance_rate: float = Field(0.0, description="出勤率", examples=[0.8])
statistic_date: Optional[DateStr] = Field(
None, description="统计节点", examples=["2024-05-31"]
)
plan_exit_date: Optional[DateStr] = Field(
None, description="计划退场时间", examples=["2024-06-30"]
)
actual_exit_date: Optional[DateStr] = Field(
None, description="实际退场时间", examples=["2024-07-05"]
)
plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表")
plan_start_date: Optional[DateStr] = Field(
None, description="计划首批到场时间", examples=["2024-05-01"]
)
plan_start_quantity: int = Field(0, description="计划首批到场人数", examples=[5], ge=0)
actual_start_date: Optional[DateStr] = Field(
None, description="实际首批到场时间", examples=["2024-05-02"]
)
actual_start_quantity: Optional[int] = Field(
None, description="实际首批到场人数", examples=[4]
)
class PowerCreate(BaseModel):
"""
创建力能
POWER_BASE_INFO = "personnel.base_info"
"""
project_id: str = Field(..., description="所属项目ID", examples=["project123"])
contract_id: Optional[str] = Field(
None, description="所属合同ID", examples=["contract456"]
)
#show_name: str = Field(..., description="展示名称", examples=["施工班组A"])
code: Optional[str] = Field(
"personnel",
description="力能类型(人员模块拓展为力能模块,可以在本字段保存人员/机具等编码)",
examples=["personnel"],
)
class PowerUpdate(BaseModel):
"""
更新力能
POWER_BASE_INFO = "personnel.base_info"
"""
id: str = Field(..., description="力能ID")
contract_id: Optional[str] = Field(
None, description="所属合同ID", examples=["contract456"]
)
#show_name: Optional[str] = Field(None, description="展示名称", examples=["施工班组A"])
class PowerPlanUpdate(BaseModel):
"""
力能计划更新模型
POWER_PLAN = "personnel.plan"
业务逻辑说明:
- API要求计划节点时间必须晚于计划首批到场时间(不能等于)
- 自动清理脏数据:如果 plan_node 有2个或以上节点,删除边界节点
"""
id: str = Field(..., description="力能ID")
plan_start_date: str = Field(..., description="计划首批到场时间")
plan_start_quantity: int = Field(..., description="计划首批到场人数")
plan_exit_date: DateStr = Field(
..., description="计划退场时间", examples=["2024-06-30"]
)
plan_node: List[PlanNode] = Field(default_factory=list, description="计划节点列表")
@field_validator("plan_start_quantity")
def validate_quantity(cls, v):
"""验证计划首批到场人数必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='计划首批进场人数必须大于等于0')
return v
@model_validator(mode='after')
def clean_plan_nodes(self):
"""
清理计划节点列表中的边界节点
如果 plan_node 有2个或以上节点,删除:
- 开头节点:date 等于 plan_start_date
- 结尾节点:date 等于 plan_exit_date
这避免了API报错“计划节点时间必须晚于计划首批到场时间”
"""
if not self.plan_node or len(self.plan_node) < 2:
return self
cleaned_nodes = list(self.plan_node)
# 删除开头的边界节点
if (self.plan_start_date and
cleaned_nodes and
cleaned_nodes[0].date == self.plan_start_date):
cleaned_nodes.pop(0)
# 删除结尾的边界节点
if (self.plan_exit_date and
cleaned_nodes and
cleaned_nodes[-1].date == self.plan_exit_date):
cleaned_nodes.pop()
self.plan_node = cleaned_nodes
return self
+133
View File
@@ -0,0 +1,133 @@
from pydantic import Field, BaseModel,field_validator
from fastapi import HTTPException
from ..common.datetime import get_current_date
from typing import List, Optional, Literal
from ..common.validators import DateStr
from ..common.base import BaseResponseWithID
from ..common.enums import ApprovalStatus
from datetime import datetime
class PowerDetail(BaseResponseWithID):
"""
力能配置明细
"""
project_id: str = Field(..., description="所属项目", examples=["project123"])
parent_id: str = Field(..., description="所属力能配置ID", examples=["personnel789"])
date: DateStr = Field(..., description="本次日期", examples=["2024-05-31"])
quantity: int = Field(..., description="累计完成量", examples=[0], ge=0)
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: Optional[bool] = Field(
None, description="是否为全部退场", examples=[False]
)
change_count: int = Field(0, description="变更次数", examples=[0])
status: Optional[int] = Field(None, description="进度状态", examples=[0])
rate: Optional[float] = Field(None, description="出勤率", examples=[0])
class PowerDetailCreate(BaseModel):
"""
力能配置明细创建模型
POWER_DETAIL = "power.detail"
"""
project_id: str = Field(..., description="所属项目", examples=["project123"])
parent_id: str = Field(..., description="所属力能配置ID", examples=["personnel789"])
date: DateStr = Field(..., description="本次日期", examples=["2024-05-31"])
quantity: int = Field(..., description="累计完成量", examples=[0], ge=0)
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: Optional[bool] = Field(
None, description="是否为全部退场", examples=[False]
)
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v < 0:
raise HTTPException(status_code=400, detail='实际完成量累计不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v > get_current_date():
raise HTTPException(detail='节点日期不能大于今天', status_code=400)
return v
class PowerDetailUpdate(BaseModel):
"""
力能配置明细更新模型
POWER_DETAIL = "power.detail"
"""
id: str = Field(..., description="力能配置明细ID")
date: Optional[DateStr] = Field(None, description="本次日期", examples=["2024-05-31"])
quantity: Optional[int] = Field(None, description="累计完成量", examples=[0], ge=0)
remark: Optional[str] = Field(None, description="填报备注", examples=[""])
is_complete: Optional[bool] = Field(
None, description="是否为全部退场", examples=[False]
)
confirmation_time: str | None = Field(None, description="确认时间", examples=["2025-12-17 11:13:49"])
approval_time: str | None = Field(None, description="审批时间", examples=["2025-12-17 11:13:49"])
approval_status: ApprovalStatus | None = Field("registered", description="审批状态", examples=["registered"])
@field_validator("approval_status")
def validate_approval_status(cls, v):
"""验证审批字段"""
if v is None:
return v
try:
return ApprovalStatus(v) # 尝试转换
except ValueError:
raise HTTPException(400, f"审批状态 '{v}' 无效")
@field_validator("confirmation_time", "approval_time")
def validate_time(cls, v):
"""验证时间"""
if v is None:
return v
try:
datetime.strptime(v, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(detail=f'{v}时间格式错误,应为:2025-12-17 11:13:49', status_code=400)
return v
@field_validator("quantity")
def validate_quantity(cls, v):
"""验证实际完成量累计必须为非负数"""
if v is None:
return None
if v < 0:
raise HTTPException(status_code=400, detail='实际完成量累计不能小于0')
return v
@field_validator("date")
def validate_date(cls, v):
if v is None:
return None
if v > get_current_date():
raise HTTPException(detail='节点日期不能大于今天', status_code=400)
return v
class PowerDetailBulkUpdate(BaseModel):
detail_list: List[PowerDetailUpdate]
+30
View File
@@ -0,0 +1,30 @@
from enum import Enum
from pydantic import BaseModel, Field
class ProgressType(str, Enum):
"""推进分类枚举"""
A = "A"
B = "B"
C = "C"
class KeyConstraints(str, Enum):
NONE = "NONE"
LAND = "LAND"
TRANSMISSION = "TRANSMISSION"
BOTH = "BOTH"
QT = "QT"
class DBTCSCORE(BaseModel):
year: str = Field(..., description="评分年份, yyyy格式")
is_jg: bool = Field(..., description="是否为工达标投产")
score: float = Field(..., description="集团考核评分")
class ProjectType(str, Enum):
"""项目类型枚举"""
PHOTOVOLTAIC = "photovoltaic" # 光伏
OFFSHORE_WIND = "offshore_wind" # 海风
ONSHORE_WIND = "onshore_wind" # 陆风
HYDROPOWER = "hydropower" # 水电
THERMAL_POWER = "thermal_power" # 火电
PUMPED_STORAGE = "pumped_storage" # 抽蓄
GAS_TURBINE = "gas_turbine" # 气电
+76
View File
@@ -0,0 +1,76 @@
"""
项目全部信息的增删查改模板,聚合了项目相关schema
"""
from pydantic import BaseModel, Field, field_serializer
from typing import Optional
from .project_base import (
ProjectResponseBase,
ProjectEnsureCapacity,
ProjectClimbCapacity,
ProjectChallengeCapacity,
ProjectActualConstruction,
ProjectPlanConstruction,
ProjectInvestmentDecision,
ProjectKeyConstraints,
ProjectProductionCapacity,
ProjectCompleteInvestment,
ProjectDynamicInvestment
)
from ..project_extensions.preparation import PreparationDetail
from ..project_extensions.production import ProductionDetail
from ..project_extensions.project_detail import ProjectDetailProject
from ..project_extensions.generator_unit import (
GeneratorUnitProject,
PostGeneratorUnits
)
from ..project_extensions.lar import (
LarDetailSchema,
LarChangeListSchema,
LarChangeUpdateSchema,
LarMainInfoUpdateSchema,
LarResettlementSchema,
LarInvestmentSchema, LarChangeDetailSchema
)
from ..project_extensions.contract_change import (
ContractChangeResponse,
PostContractChange
)
from ..project_multi_type import (
PhotovoltaicExtension,
OnShoreWindExtension,
OffShoreWindExtension,
HydropowerExtension,
PumpedStorageExtension,
GasTurbineExtension,
ThermalPowerExtension
)
class ProjectInfoResponse(BaseModel):
"""
项目全部信息响应模型
"""
project: ProjectResponseBase = Field(..., description="项目基本信息")
project_detail: list[ProjectDetailProject] = Field(..., description="项目容量信息")
generator_units: list[GeneratorUnitProject] = Field(..., description="发电单元信息列表(仅限水电、抽蓄、煤电、气电)")
lar: Optional[LarDetailSchema] = Field(None, description="移民征地信息(仅限水电、抽蓄)")
lar_change: list[LarChangeDetailSchema] = Field(default_factory=list, description="移民变更信息列表(仅限水电、抽蓄)")
contract_changes: list[ContractChangeResponse] = Field(..., description="合同变更信息列表(仅限水电、抽蓄、煤电、气电)")
# 扩展字段
photovoltaic_extension: PhotovoltaicExtension | None = Field(None, description="光伏项目扩展信息")
onshore_wind_extension: OnShoreWindExtension | None = Field(None, description="陆风项目扩展信息")
offshore_wind_extension: OffShoreWindExtension | None = Field(None, description="海风项目扩展信息")
hydropower_extension: HydropowerExtension | None = Field(None, description="水电项目扩展信息")
pumped_storage_extension: PumpedStorageExtension | None = Field(None, description="抽蓄项目扩展信息")
gas_turbine_extension: GasTurbineExtension | None = Field(None, description="燃气轮机项目扩展信息")
thermal_power_extension: ThermalPowerExtension | None = Field(None, description="火电项目扩展信息")
# 投产节点
production_list: list[ProductionDetail] = Field(default_factory=list, description="投产节点列表")
# 里程碑节点
preparation_list: list[PreparationDetail] = Field(default_factory=list, description="里程碑节点列表")
+355
View File
@@ -0,0 +1,355 @@
"""
项目数据推送相关schema
"""
from typing import List, Optional
from fastapi import HTTPException
from pydantic import Field, BaseModel, field_validator
from ..common.push_system import BaseUpdateRequestSchema
from ..common.base import BaseResponseWithID
from ..common.datetime import get_current_date
from ..common.validators import DateStr
from .enums import ProgressType, KeyConstraints, DBTCSCORE
class ProjectResponseBase(BaseResponseWithID):
"""
项目基础信息, 复制自project_model
不进行继承,通过检查器确保字段和模型一致
"""
# 基础信息-不允许更改
id : str = Field(..., description="项目ID", examples=["uuid"])
project_type: str = Field("", description="项目类型")
region_name: List[str] = Field(default_factory=list, description="项目所属区域名称", examples=[["华东", "江苏"]])
region_path: List[str] = Field(default_factory=list, description="项目所属区域路径", examples=[["110000", "120000", "120001"]])
province: str = Field("", description="省份(region_path第一个)", examples=["110000"])
is_domestic :bool=Field(True, description="是否是国内项目")
company_id: str = Field("", description="所属公司ID", examples=["company123"])
company_name: str = Field("", description="所属公司名称", examples=["国电投江苏公司"])
syxs: Optional[str] = Field(None, description="升压站型式")
sub_type: Optional[str] = Field(None, description="光伏类别")
scgc_type: Optional[str] = Field(None, description="送出工程建设方式")
# 基础信息-允许更改
address: str = Field("", description="项目地址", examples=["某省某市某区"])
code: str = Field("", description="项目编码", examples=["XM2025001"])
name: str = Field(..., description="项目名称", examples=["XX新能源项目"])
short_name: str = Field("", description="项目简称", examples=["XX项目"])
manager_name: str = Field("", description="负责人姓名", examples=["张三"])
manager_phone: str = Field("", description="负责人电话", examples=["13800000000"])
# 容量信息
authorization_date: Optional[DateStr] = Field(None, description="核准日期", examples=["2024-01-01"])
authorized_capacity: float = Field(0.0, description="核准容量(万千瓦)", examples=[10.0])
# 投决
investment_decision_capacity: Optional[float] = Field(None, description="投资决策容量", examples=[8.0])
investment_decision_date: Optional[DateStr] = Field(None, description="投资决策日期", examples=["2024-02-01"])
# 开工
plan_construction_capacity: float = Field(0.0, description="本年度计划开工容量", examples=[5.0])
plan_construction_date: Optional[DateStr] = Field(None, description="本年度计划开工日期", examples=["2024-03-01"])
actual_construction_capacity: float = Field(0.0, description="本年度实际开工容量", examples=[4.0])
actual_construction_date: Optional[DateStr] = Field(None, description="本年度实际开工日期", examples=["2024-04-01"])
accumulated_construction_capacity: float = Field(0.0, description="累计开工容量", examples=[12.0])
constructing_capacity: float = Field(0.0, description="在建容量", examples=[6.0])
# 投产
ensure_production_capacity: float = Field(0.0, description="本年度确保投产容量", examples=[3.0])
challenge_production_capacity: float = Field(0.0, description="本年度揭榜投产容量", examples=[2.0])
climb_production_capacity: float = Field(0.0, description="本年度登高投产容量", examples=[1.0])
plan_production_date: Optional[DateStr] = Field(None, description="计划投产日期", examples=["2024-08-01"])
plan_full_production_date: Optional[DateStr] = Field(None, description="计划全部投产日期", examples=["2024-09-01"])
actual_production_date: Optional[DateStr] = Field(None, description="实际投产日期", examples=["2024-08-15"])
actual_full_production_date: Optional[DateStr] = Field(None, description="实际全投日期", examples=["2024-09-10"])
production_capacity: Optional[float] = Field(0.0, description="本年度投产容量", examples=[2.0])
production_date: Optional[DateStr] = Field(None, description="本年度首批投产日期", examples=["2024-09-01"])
total_production_capacity: Optional[float] = Field(0.0, description="全部投产容量", examples=[2.0])
# 投资
dynamic_investment: float = Field(0.0, description="动态总投资", examples=[100000.0])
complete_investment: float = Field(0.0, description="完成投资", examples=[80000.0])
# 其他信息
progress_type: ProgressType = Field(..., description="推进分类", examples=[ProgressType.A])
key_constraints: KeyConstraints = Field(KeyConstraints.NONE, description="关键制约", examples=[KeyConstraints.NONE])
key_constraints_remark: Optional[str] = Field("", description="关键制约备注", examples=[""])
status: Optional[str] = Field(None, description="项目状态")
apply_file_ids: List[str] = Field(default_factory=list, description="申请表附件ID列表", examples=[[]])
# 拓展信息
dbtc_score_list: Optional[list] = Field(default_factory=list, description='达标投产评分列表') # 这里项目用的是list而不是List
class ProjectResponseBaseList(BaseModel):
page: int
page_size: int
total: int
list: List[ProjectResponseBase] = []
# ==================== Update ====================
class KeyConstraintsRemarkMixin:
"""关键制约备注校验混入类"""
@field_validator("key_constraints_remark")
def validate_key_constraints_remark(cls, v):
if v is None:
return v
if len(v) > 500:
raise HTTPException(detail="关键制约备注不能超过500个字符", status_code=400)
return v
class AuthorizationDateMixin:
"""关键制约备注校验混入类"""
@field_validator("authorization_date")
def validate_actual_date(cls, v):
if v is None:
return v
if v > get_current_date():
raise HTTPException(detail='核准/备案日期不能大于今天', status_code=400)
return v
class ProjectBaseInfoUpdate(BaseModel, KeyConstraintsRemarkMixin, AuthorizationDateMixin):
"""
项目基本信息 PROJECT_BASE_INFO
"""
# 基础信息-允许更改
id: str = Field(..., description="项目id", examples=["uuid"])
address: str | None = Field(None, description="项目地址", examples=["某省某市某区"])
code: str | None = Field(None, description="项目编码", examples=["XM2025001"])
name: str = Field(..., description="项目名称", examples=["XX新能源项目"])
short_name: str | None = Field(None, description="项目简称", examples=["XX项目"])
manager_name: str | None = Field(None, description="负责人姓名", examples=["张三"])
manager_phone: str | None = Field(None, description="负责人电话", examples=["13800000000"])
# 其他信息
progress_type: ProgressType = Field(..., description="推进分类", examples=[ProgressType.A])
key_constraints: KeyConstraints | None = Field(None, description="关键制约", examples=[KeyConstraints.NONE])
key_constraints_remark: str | None = Field(None, description="关键制约备注", examples=[""])
apply_file_ids: List[str] | None = Field(None, description="申请表附件ID列表", examples=[[]])
# 拓展信息
dbtc_score_list: list[DBTCSCORE] | None = Field(None, description='达标投产评分列表') # 这里项目用的是list而不是List
# 核准
authorization_date: DateStr | None = Field(None, description="核准日期", examples=["2024-01-01"])
authorized_capacity: float | None = Field(None, description="核准容量(万千瓦)", examples=[10.0])
class _ProductionPlanNode(BaseModel):
year: int = Field(..., description="计划年份", examples=[2024])
capacity: float = Field(..., description="确保投产容量", examples=[3.0])
class ProjectEnsureCapacity(BaseModel):
"""
PROJECT_ENSURE_CAPACITY
填写项目确保投产容量列表,系统根据列表自动计算ensure_production_capacity字段
"""
# 确保投产容量
ensure_production_capacity_list : list[_ProductionPlanNode] = Field(..., description="确保投产容量列表(每次必须填写全部)")
class ProjectClimbCapacity(BaseModel):
"""
PROJECT_CLIMB_CAPACITY
填写项目登高投产容量列表,系统根据列表自动计算climb_production_capacity字段
"""
# 登高投产容量
climb_production_capacity_list : list[_ProductionPlanNode] = Field(..., description="登高投产容量列表(每次必须填写全部)")
class ProjectChallengeCapacity(BaseModel):
"""
PROJECT_CHALLENGE_CAPACITY
填写项目揭榜投产容量列表,系统根据列表自动计算challenge_production_capacity字段
"""
# 揭榜投产容量
challenge_production_capacity_list : list[_ProductionPlanNode] = Field(..., description="揭榜投产容量列表(每次必须填写全部)")
class _ActualNodeData(BaseModel):
capacity: float = Field(..., description="实际节点容量", examples=[4.0])
date: DateStr = Field(..., description="实际节点日期", examples=["2024-04-01"])
class ProjectActualConstruction(BaseModel):
"""
PROJECT_ACTUAL_CONSTRUCTION
项目实际开工节点
系统根据实际开工节点自动计算以下字段:
actual_construction_capacity 本年度实际开工容量
actual_construction_date 本年度实际开工日期
accumulated_construction_capacity 累计开工容量
constructing_capacity 在建容量
"""
actual_construction_list : list[_ActualNodeData] | None = Field(None, description="实际开工节点列表(每次必须填写全部)")
class _PlanNodeData(BaseModel):
capacity: float = Field(..., description="计划节点容量", examples=[5.0])
date: DateStr = Field(..., description="本年度计日期", examples=["2024-03-01"])
@field_validator("capacity")
def validate_capacity(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="节点容量必须大于等于0")
return v
class ProjectPlanConstruction(BaseModel):
"""
PROJECT_PLAN_CONSTRUCTION
项目计划开工节点
系统根据计划开工节点自动计算:
plan_construction_capacity 本年度计划开工容量
plan_construction_date 本年度计划开工日期
"""
plan_construction_list : list[_PlanNodeData] | None = Field(None, description="计划开工节点列表(每次必须填写全部)")
class ProjectInvestmentDecision(BaseModel):
"""
PROJECT_INVESTMENT_DECISION
投资决策容量和日期
"""
investment_decision_capacity: float = Field(..., description="投资决策容量", examples=[8.0])
investment_decision_date: DateStr = Field(..., description="投资决策日期", examples=["2024-02-01"])
@field_validator("investment_decision_date")
def validate_investment_decision_date(cls, v):
if v is None:
return v
if v > get_current_date():
raise HTTPException(detail='投资决策日期不能大于今天', status_code=400)
return v
@field_validator("investment_decision_capacity")
def validate_investment_decision_capacity(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="投资决策容量必须大于等于0")
return v
class ProjectKeyConstraints(BaseModel, KeyConstraintsRemarkMixin):
"""
PROJECT_KEY_CONSTRAINTS
关键制约
"""
key_constraints: KeyConstraints | None = Field(None, description="关键制约", examples=[KeyConstraints.NONE])
key_constraints_remark: str | None = Field(None, description="关键制约备注", examples=[""])
class _ActualProductionNode(BaseModel):
capacity: float = Field(..., description="实际投产容量", examples=[2.0])
date: DateStr = Field(..., description="实际投产日期", examples=["2024-08-15"])
@field_validator("capacity")
def validate_capacity(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="实际投产容量必须大于等于0")
return v
@field_validator("date")
def validate_date(cls, v):
if v is None:
return v
if v > get_current_date():
raise HTTPException(detail='实际投产日期不能大于今天', status_code=400)
return v
class ProjectProductionCapacity(BaseModel):
"""
PROJECT_PRODUCTION_CAPACITY
填写项目投产数据
每个项目在各年份或一年内可能有多次实际投产
系统会根据投产节点列表数据,自动计算
production_capacity 本年度投产容量
production_date 本年度首批投产日期
total_production_capacity 全部投产容量
"""
plan_production_date: DateStr | None = Field(None, description="集团公司计划投产日期", examples=["2024-08-01"])
plan_full_production_date: DateStr | None = Field(None, description="集团公司计划全部投产日期", examples=["2024-09-01"])
ic_plan_first_production_date: DateStr | None = Field(None, description="区域公司计划首批投产日期(只有新能源需要)",examples=["2024-08-01"])
ic_plan_full_production_date: DateStr | None = Field(None, description="区域公司计划全投日期(只有新能源需要)",examples=["2024-09-01"])
actual_production_date: DateStr | None = Field(None, description="实际投产日期(只有新能源需要)", examples=["2024-08-15"])
actual_full_production_date: DateStr | None = Field(None, description="实际全投日期(只有新能源需要)", examples=["2024-09-10"])
actual_production_list : list[_ActualProductionNode] | None = Field(None, description="实际投产节点列表(每次必须填写全部)(只有新能源需要)")
@field_validator("actual_production_date")
def validate_actual_production_date(cls, v):
if v is None:
return v
if v > get_current_date():
raise HTTPException(detail='实际投产日期不能大于今天', status_code=400)
return v
@field_validator("actual_full_production_date")
def validate_actual_full_production_date(cls, v):
if v is None:
return v
if v > get_current_date():
raise HTTPException(detail='实际全投日期不能大于今天', status_code=400)
return v
class ProjectCompleteInvestment(BaseModel):
"""
PROJECT_COMPLETE_INVESTMENT
完成投资投资
"""
complete_investment: float = Field(..., description="完成投资", examples=[80000.0])
@field_validator("complete_investment")
def validate_complete_investment(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400,detail="完成投资必须大于等于0")
return v
class ProjectSettlementInvestment(BaseModel):
"""
PROJECT_SETTLEMENT_INVESTMENT
合同结算投资情况
"""
implementation_estimate: float = Field(..., description="执行概算", examples=[80000.0])
static_total_investment: float = Field(..., description="静态总投资", examples=[80000.0])
completed_investment: float = Field(..., description="完成投资", examples=[80000.0])
dynamic_investment: float = Field(..., description="动态总投资", examples=[80000.0])
@field_validator("implementation_estimate")
def validate_implementation_estimate(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400,detail="执行概算必须大于等于0")
return v
@field_validator("static_total_investment")
def validate_static_total_investment(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="静态总投资必须大于等于0")
return v
@field_validator("completed_investment")
def validate_completed_investment(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="完成投资必须大于等于0")
return v
@field_validator("dynamic_investment")
def validate_dynamic_investment(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="动态总投资必须大于等于0")
return v
class ProjectDynamicInvestment(BaseModel):
"""
PROJECT_DYNAMIC_INVESTMENT
"""
# 动态总投资
dynamic_investment: float | None = Field(None, description="动态总投资", examples=[100000.0])
@field_validator("dynamic_investment")
def validate_dynamic_investment(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="动态总投资必须大于等于0")
return v
# ======================= 水电抽蓄专用 =============================
# ==================== 枚举说明 用于文档自动生成 ====================
@@ -0,0 +1,75 @@
"""
contract_change相关schemea
"""
from ..common.validators import DateStr
from pydantic import BaseModel, Field
from typing import Optional
from ..common.base import BaseResponseWithID
from pydantic import field_validator
from fastapi import HTTPException
class ContractChangeResponse(BaseResponseWithID):
"""
合同变更模型
"""
id: Optional[str] = Field(default=None, description="合同变更ID")
project_id : Optional[str] = Field(None, description="所属项目ID")
contract_id: Optional[str] = Field(None, description="关联合同ID")
name: Optional[str] = Field(None, description="合同名称")
sub_name: Optional[str] = Field(None, description="合同子标题(epc合同和pc合同会有子标题)")
sign_company:Optional[str] = Field(None, description="签约公司")
title: Optional[str] = Field(None, description="变更事项")
reason: Optional[str] = Field(None, description="变更理由")
before_amount: Optional[float] = Field(None, description="变更前金额")
after_amount: Optional[float] = Field(None, description="变更后金额")
change_count: Optional[float] = Field(None, description="变更金额")
change_rate: Optional[str] = Field(None, description="变更比例")
change_time: Optional[str] = Field(None, description="变更时间(格式:YYYY-MM-DD)")
attachment_ids:Optional[list] = Field(None, description="附件ID列表")
attachment_names:Optional[list] = Field(None, description="附件名称列表")
created_at:Optional[str]=Field(None, description="创建时间")
created_by_name: Optional[str] = Field(None, description="创建人名称")
class PostContractChange(BaseModel):
"""
合同变更请求(水火抽蓄气电)
PROJECT_CONTRACT_CHANGE = "project.contract-change"
"""
id: str = Field(..., description="合同变更ID,更新时传入,创建时不传")
project_id: str = Field(..., description="所属项目ID")
contract_id: str = Field(..., description="合同ID")
title: str = Field(..., description="变更事项")
reason: str = Field(..., description="变更理由")
before_amount: float = Field(..., description="变更前金额")
after_amount: float = Field(..., description="变更后金额")
change_time: DateStr = Field(..., description="变更时间(格式:YYYY-MM-DD)")
attachment_ids: list = Field(..., description="附件ID列表")
@field_validator("after_amount")
def validate_after_amount(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="变更后金额必须小于等于0")
return v
class CreateContractChange(BaseModel):
"""
创建合同变更请求(水火抽蓄气电)
PROJECT_CONTRACT_CHANGE = "project.contract-change"
"""
project_id: str = Field(..., description="所属项目ID")
contract_id: str = Field(..., description="合同ID")
title: str = Field(..., description="变更事项")
reason: str = Field(..., description="变更理由")
before_amount: float = Field(..., description="变更前金额")
after_amount: float = Field(..., description="变更后金额")
change_time: DateStr = Field(..., description="变更时间(格式:YYYY-MM-DD)")
attachment_ids: list = Field(..., description="附件ID列表")
@field_validator("after_amount")
def validate_after_amount(cls, v: float) -> float:
if v < 0:
raise HTTPException(status_code=400, detail="变更后金额不能小于0")
return v
@@ -0,0 +1,34 @@
from pydantic import BaseModel, Field,field_validator
from typing import Optional
from ..common.base import BaseResponseWithID
from ..common.datetime import datetime_date,get_current_date
from fastapi import HTTPException
class GeneratorUnitProject(BaseResponseWithID):
project_id : Optional[str]= Field(None, description="项目ID")
serial_number: Optional[int] = Field(None, description="机组序号")
approval_status:Optional[str]=Field(None, description="审批状态")
capacity: Optional[float] = Field(None, description="单机容量(MW)")
plan_production_date: Optional[str] = Field(None, description="计划投产日期")
actual_production_date: Optional[str] = Field(None, description="实际投产日期")
class PostGeneratorUnits(BaseModel):
model_config = {"populate_by_name": True}
id:str = Field(..., description="机组ID")
project_id: str = Field(..., description="所属项目ID")
actual_production_date: str = Field(..., alias="actual_date", description="实际投产日期(格式:YYYY-MM-DD)")
@field_validator('actual_production_date', mode="before")
def validate_actual_date_not_future(cls, v: str) -> str:
if v == "":
return ""
try:
actual = datetime_date(v)
except ValueError:
raise HTTPException(status_code=400,detail='日期格式为(YYYY-MM-DD)')
today = datetime_date(get_current_date())
if actual > today:
raise HTTPException(status_code=400,detail='机组日期不能晚于今天')
return v
+206
View File
@@ -0,0 +1,206 @@
"""
移民征地相关schema
"""
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from ..common.base import BaseResponseWithID
from ..common.validators import DateStr
from ..common.datetime import get_current_date
class LarDetailSchema(BaseResponseWithID):
id: str = Field(..., description="移民征地id")
project_id: Optional[str] = Field(None, description="项目ID")
plan_resettlement_pop: Optional[int] = Field(None, description="计划搬迁人数")
completed_resettlement_pop: Optional[int] = Field(None, description="完成搬迁人数")
plan_investment_amt: Optional[float] = Field(None, description="计划投资金额 ")
completed_investment_amt: Optional[float] = Field(None, description="完成投资金额 ")
plan_urban_comp: Optional[float] = Field(None, description="计划城镇补偿金额 ")
actual_urban_comp: Optional[float] = Field(None, description="实际城镇补偿金额 ")
plan_rural_comp: Optional[float] = Field(None, description="计划农村补偿金额 ")
actual_rural_comp: Optional[float] = Field(None, description="实际农村补偿金额 ")
plan_spec_project_reconstr: Optional[float] = Field(None, description="计划专项工程复建 ")
actual_spec_project_reconstr: Optional[float] = Field(None, description="实际专项工程复建 ")
plan_reservoir_bottom_cleanup: Optional[float] = Field(None, description="计划库底清理 ")
actual_reservoir_bottom_cleanup: Optional[float] = Field(None, description="实际库底清理 ")
class LarMainInfoUpdateSchema(BaseResponseWithID):
id: str = Field(..., description="移民征地id")
project_id: str = Field(..., description="项目id")
plan_urban_comp: float = Field(..., description="计划城镇补偿金额 ")
actual_urban_comp: float = Field(..., description="实际城镇补偿金额 ")
plan_rural_comp: float = Field(..., description="计划农村补偿金额 ")
actual_rural_comp: float = Field(..., description="实际农村补偿金额 ")
plan_spec_project_reconstr: float = Field(..., description="计划专项工程复建 ")
actual_spec_project_reconstr: float = Field(..., description="实际专项工程复建 ")
plan_reservoir_bottom_cleanup: float = Field(..., description="计划库底清理 ")
actual_reservoir_bottom_cleanup: float = Field(..., description="实际库底清理 ")
@field_validator(
"plan_urban_comp",
"actual_urban_comp",
"plan_rural_comp",
"actual_rural_comp",
"plan_spec_project_reconstr",
"actual_spec_project_reconstr",
"plan_reservoir_bottom_cleanup",
"actual_reservoir_bottom_cleanup",
mode="before",
)
def check_amount_field(cls, v):
from fastapi import HTTPException
if v is None or v == "":
raise HTTPException(detail='金额字段不能为空', status_code=400)
if float(v) < 0:
raise HTTPException(detail='金额字段需大于等于0', status_code=400)
return v
class LarResettlementSchema(BaseResponseWithID):
id: str = Field(..., description="移民征地id")
project_id: str = Field(..., description="项目id")
plan_resettlement_pop: int = Field(..., description="计划搬迁人数")
completed_resettlement_pop: int = Field(..., description="完成搬迁人数")
@field_validator(
"plan_resettlement_pop",
"completed_resettlement_pop",
mode="before",
)
def check_amount_field(cls, v):
from fastapi import HTTPException
if v is None or v == "":
raise HTTPException(detail='搬迁人数不能为空', status_code=400)
if float(v) < 0:
raise HTTPException(detail='搬迁人数需大于等于0', status_code=400)
return v
class LarInvestmentSchema(BaseResponseWithID):
id: str = Field(..., description="移民征地id")
project_id: str = Field(..., description="项目id")
plan_investment_amt: float = Field(..., description="计划投资金额")
completed_investment_amt: float = Field(..., description="完成投资金额")
@field_validator(
"plan_investment_amt",
"completed_investment_amt",
mode="before",
)
def check_amount_field(cls, v):
from fastapi import HTTPException
if v is None or v == "":
raise HTTPException(detail='金额字段不能为空', status_code=400)
if float(v) < 0:
raise HTTPException(detail='金额字段数需大于等于0', status_code=400)
return v
class LarChangeDetailSchema(BaseResponseWithID):
project_id: str = Field(..., description="项目ID")
title: Optional[str] = Field(None, description="变更事项")
reason: Optional[str] = Field(None, description="变更理由")
before_amount: Optional[float] = Field(None, description="变更前金额")
after_amount: Optional[float] = Field(None, description="变更后金额")
change_time: Optional[str] = Field(None, description="变更时间")
attachment_ids: List[str] = Field(default_factory=list, description="附件ID列表", examples=[[]])
created_at: Optional[str] = Field(None, description="创建时间")
created_by_name: Optional[str] = Field(None, description="创建人")
change_amount: Optional[float] = Field(None, description="变更金额")
change_percentage: Optional[str] = Field('', description="变更比例")
approval_status: Optional[str] = Field(None, description="审批状态")
attachment_info: List[dict] = Field(
default_factory=list, description="文件信息", examples=[[
{
"id": "54a0daf903a04c8e87cea1175458459d",
"name": "54a0daf903a04c8e87cea1175458459d.pdf",
"original_name": "新用户注册申请表-项目公司.pdf",
"url": "/api/file_download/54a0daf903a04c8e87cea1175458459d"
}
]]
)
class LarChangeInfoSchema(BaseResponseWithID):
title: Optional[str] = Field(None, description="变更事项")
reason: Optional[str] = Field(None, description="变更理由")
before_amount: Optional[float] = Field(None, description="变更前金额")
after_amount: Optional[float] = Field(None, description="变更后金额")
change_time: Optional[str] = Field(None, description="变更时间")
attachment_ids: List[str] = Field(default_factory=list, description="附件ID列表", examples=[[]])
approval_status: Optional[str] = Field(None, description="审批状态")
before_amount_is_edit: Optional[bool] = Field(None, description="变更前金额是否可编辑")
changed_count: Optional[int] = Field(None, description="已办更次数")
class LarChangeListSchema(BaseModel):
page: int
page_size: int
total: int
list: List[LarChangeDetailSchema]
class LarChangeCreateSchema(BaseModel):
title: str = Field(..., description="变更事项")
reason: str = Field(..., description="变更理由")
before_amount: float = Field(..., description="变更前金额")
after_amount: float = Field(..., description="变更后金额")
change_time: DateStr = Field(..., description="变更时间")
attachment_ids: List[str] = Field(default_factory=list, description="附件ID列表", examples=[[]])
project_id: str = Field(..., description="项目id")
@field_validator(
"before_amount",
"after_amount",
mode="before",
)
def check_amount_field(cls, v):
from fastapi import HTTPException
if v is None or v == "":
raise HTTPException(detail='金额字段不能为空', status_code=400)
if float(v) < 0:
raise HTTPException(detail='金额字段需大于等于0', status_code=400)
return v
@field_validator(
"change_time",
mode="before",
)
def check_actual_time_field(cls, v):
from fastapi import HTTPException
if v is None or v == "":
raise HTTPException(detail='移民变更时间必填', status_code=400)
if v > get_current_date():
raise HTTPException(detail='移民变更时间不能大于当前日期', status_code=400)
return v
class LarChangeUpdateSchema(LarChangeCreateSchema):
id: str = Field(..., description="移民变更id")
+39
View File
@@ -0,0 +1,39 @@
from pydantic import BaseModel, Field,field_validator
from typing import Optional
from ..common.base import BaseResponseWithID
from ..common.datetime import datetime_date,get_current_date
from fastapi import HTTPException
from ..common.validators import DateStr
class PostPreparation(BaseModel):
id:str = Field(..., description="里程碑ID")
code: str= Field(..., description="里程碑的节点编码")
acquire_date: DateStr|None = Field(None,description="取得时间")
is_exist: bool= Field(..., description="是否取得")
@field_validator('acquire_date')
def validate_acquire_date(cls, v: Optional[DateStr]) -> Optional[DateStr]:
if v is None:
return None
try:
actual = datetime_date(v)
except ValueError:
raise HTTPException(status_code=400,detail='日期格式为(YYYY-MM-DD)')
today = datetime_date(get_current_date())
if actual > today:
raise HTTPException(status_code=400,detail='取得时间不能晚于今天')
return v
class PreparationDetail(BaseResponseWithID):
id: Optional[str] = Field(None, description="里程碑ID")
project_id: Optional[str] = Field(None, description="项目ID")
code: Optional[str] = Field(None, description="里程碑节点编码")
acquire_date: Optional[DateStr] = Field(None, description="取得时间")
is_exist: Optional[bool] = Field(None, description="是否取得")
approval_status: Optional[str] = Field(None, description="审批状态")
name: Optional[str] = Field(None, description="里程碑名称")
group: Optional[str] = Field(None, description="分组条件", examples=['KGTJLS'])
is_acquired: Optional[int] = Field(0, description="是否取得")
+39
View File
@@ -0,0 +1,39 @@
from pydantic import BaseModel, Field, field_validator
from typing import Optional
from ..common.base import BaseResponseWithID
from ..common.datetime import datetime_date, get_current_date
from fastapi import HTTPException
from ..common.validators import DateStr
class PostProduction(BaseModel):
id: str = Field(..., description="投产节点ID")
code: str = Field(..., description="节点编码")
acquire_date: DateStr | None = Field(None, description="取得时间")
is_exist: bool = Field(..., description="是否取得")
@field_validator('acquire_date')
def validate_acquire_date(cls, v: Optional[DateStr]) -> Optional[DateStr]:
if v is None:
return None
try:
actual = datetime_date(v)
except ValueError:
raise HTTPException(status_code=400, detail='日期格式为(YYYY-MM-DD)')
today = datetime_date(get_current_date())
if actual > today:
raise HTTPException(status_code=400, detail='取得时间不能晚于今天')
return v
class ProductionDetail(BaseResponseWithID):
id: Optional[str] = Field(None, description="投产节点ID")
project_id: Optional[str] = Field(None, description="项目ID")
code: Optional[str] = Field(None, description="投产节点编码")
acquire_date: Optional[DateStr] = Field(None, description="取得时间")
is_exist: Optional[bool] = Field(None, description="是否取得")
approval_status: Optional[str] = Field(None, description="审批状态")
name: Optional[str] = Field(None, description="投产节点名称")
is_acquired: Optional[int] = Field(0, description="是否取得")
@@ -0,0 +1,33 @@
"""
项目容量信息schema
只提供返回,不提供更新接口
"""
from enum import Enum
from pydantic import BaseModel, Field
from typing import Optional, List
from ..common.base import BaseResponseWithID
class ProjectDetailKey(str, Enum):
"""项目子类类型枚举"""
PLAN_CONSTRUCTION = "plan_construction" # 计划在建容量
ACTUAL_CONSTRUCTION = "actual_construction" # 实际在建容量
PRODUCTION = "production" # 投产容量
ENSURE_PRODUCTION = "ensure_production" # 确保容量
CLIMB_PRODUCTION = "climb_production" # 登高容量
CHALLENGE_PRODUCTION = "challenge_production" # 揭榜容量
COMPLETE_INVESTMENT = "complete_investment" # 完成投资容量
class CapacityItem(BaseModel):
year: Optional[int] = Field(None, description='')
date: Optional[str] = Field(None, description="日期,格式YYYY-MM-DD")
capacity: float = Field(..., description='容量')
is_audit: Optional[bool] = Field(False, description='是否可编辑')
class ProjectDetailProject(BaseResponseWithID):
project_id:str= Field(..., description="所属项目ID", examples=["project123"])
key: Optional[ProjectDetailKey] = Field(None, description='项目子类类型')
value: Optional[List[CapacityItem]] = Field(None, description='项目子类类型值')
+7
View File
@@ -0,0 +1,7 @@
from .photovotaic import PhotovoltaicExtension
from .onshore_wind import OnShoreWindExtension
from .offshore_wind import OffShoreWindExtension
from .hydropower import HydropowerExtension
from .pumped_storage import PumpedStorageExtension
from .gas_turbine import GasTurbineExtension
from .thermal_power import ThermalPowerExtension
@@ -0,0 +1,9 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from ..common.validators import DateStr
class GasTurbineExtension(BaseModel):
"""
燃气轮机项目特定字段
"""
pass
+26
View File
@@ -0,0 +1,26 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from ..common.validators import DateStr
class HydropowerExtension(BaseModel):
"""
抽蓄项目特定字段
"""
dam_type: Optional[str] = Field(None, description="大坝坝型")
storage_capacity: Optional[float] = Field(None, description="总库容")
normal_storage_level: Optional[float] = Field(None, description="正常蓄水位")
regulation_performance: Optional[str] = Field(None, description="调节性能")
workshop_type: Optional[str] = Field(None, description="厂房类型")
precon_start_date: Optional[str] = Field(None, description="筹建准备期起始时间")
precon_end_date: Optional[str] = Field(None, description="筹建准备期终止时间")
maincon_start_date: Optional[str] = Field(None, description="主体施工期起始时间")
maincon_end_date: Optional[str] = Field(None, description="主体施⼯期终止时间")
comm_start_date: Optional[str] = Field(None, description="完建期开始起始时间")
comm_end_date: Optional[str] = Field(None, description="完建期终止时间")
implementation_estimate: Optional[float] = Field(None, description="执行概算")
static_total_investment: Optional[float] = Field(None, description="静态总投资")
completed_investment: Optional[float] = Field(None, description="完成投资")
total_contract_quantity: Optional[float] = Field(None, description="总合同数量")
completed_settlement_quantity: Optional[float] = Field(None, description="完成结算数量")
@@ -0,0 +1,17 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from ..common.validators import DateStr
class OffShoreWindExtension(BaseModel):
"""
完整的抽蓄项⽬模型,继承了所有通⽤字段和特定字段
"""
syxs: Optional[str] = Field(None, description="升压站型式")
group_filing_form_id: Optional[str] = Field(None, examples=["集团备案表文件id"])
partial_production_filing_form_id: Optional[str] = Field(None, examples=["部分投产备案表文件id"])
all_production_filing_form_id: Optional[str] = Field(None, examples=["全部投产备案表文件id"])
ic_plan_first_production_date: Optional[DateStr] = Field(None, description="区域公司计划首批投产日期",
examples=["2024-08-01"])
ic_plan_full_production_date: Optional[DateStr] = Field(None, description="区域公司计划全投日期",
examples=["2024-09-01"])
@@ -0,0 +1,18 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from ..common.validators import DateStr
class OnShoreWindExtension(BaseModel):
"""
ONSHORE_WIND_EXTENSION
陆风项目扩展信息
"""
group_filing_form_id: Optional[str] = Field(None, examples=["集团备案表文件id"])
partial_production_filing_form_id: Optional[str] = Field(None, examples=["部分投产备案表文件id"])
all_production_filing_form_id: Optional[str] = Field(None, examples=["全部投产备案表文件id"])
ic_plan_first_production_date: Optional[DateStr] = Field(None, description="区域公司计划首批投产日期",
examples=["2024-08-01"])
ic_plan_full_production_date: Optional[DateStr] = Field(None, description="区域公司计划全投日期",
examples=["2024-09-01"])
+15
View File
@@ -0,0 +1,15 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from ..common.validators import DateStr
class PhotovoltaicExtension(BaseModel):
"""
完整的光伏项⽬模型,继承了所有通⽤字段和特定字段
"""
group_filing_form_id: Optional[str] = Field(None, examples=["集团备案表文件id"])
partial_production_filing_form_id: Optional[str] = Field(None, examples=["部分投产备案表文件id"])
all_production_filing_form_id: Optional[str] = Field(None, examples=["全部投产备案表文件id"])
ic_plan_first_production_date: Optional[DateStr] = Field(None, description="区域公司计划首批投产日期",
examples=["2024-08-01"])
ic_plan_full_production_date: Optional[DateStr] = Field(None, description="区域公司计划全投日期",
examples=["2024-09-01"])
@@ -0,0 +1,29 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from ..common.validators import DateStr
class PumpedStorageExtension(BaseModel):
"""
抽蓄项目特定字段
"""
upper_dam_type: Optional[str] = Field(None, description="上水库坝型")
upper_storage_capacity: Optional[float] = Field(None, description="上库库容(万m³)")
lower_dam_type: Optional[str] = Field(None, description="下水库坝型")
lower_storage_capacity: Optional[float] = Field(None, description="下库库容(万m³)")
rated_head: Optional[float] = Field(None, description="额定水头")
distance_to_head_ratio: Optional[float] = Field(None, description="距高比")
continuous_full_load_hours: Optional[float] = Field(None, description="连续满发小时数")
kwh_static_cost: Optional[float] = Field(None, description="单位千瓦静态投资")
precon_start_date: Optional[str] = Field(None, description="筹建准备期起始时间")
precon_end_date: Optional[str] = Field(None, description="筹建准备期终止时间")
maincon_start_date: Optional[str] = Field(None, description="主体施工期起始时间")
maincon_end_date: Optional[str] = Field(None, description="主体施⼯期终止时间")
comm_start_date: Optional[str] = Field(None, description="完建期开始起始时间")
comm_end_date: Optional[str] = Field(None, description="完建期终止时间")
implementation_estimate:Optional[float] = Field(None, description="执行概算")
static_total_investment: Optional[float] = Field(None, description="静态总投资")
completed_investment: Optional[float] = Field(None, description="完成投资")
total_contract_quantity: Optional[float] = Field(None, description="总合同数量")
completed_settlement_quantity: Optional[float] = Field(None, description="完成结算数量")
@@ -0,0 +1,10 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Literal
from ..common.validators import DateStr
class ThermalPowerExtension(BaseModel):
"""
THERMAL_POWER_EXTENSION
火电项目扩展信息
"""
pass
+43
View File
@@ -0,0 +1,43 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from ..common.base import BaseResponseWithID
class SupplierDetailResponse(BaseResponseWithID):
"""
供应商详情模型
"""
id: str = Field(..., description="供应商ID", examples=["company:001"])
name: str = Field(..., description="供应商名称", examples=["腾讯集团"])
short_name: str = Field(..., description="供应商简称", examples=["腾讯"])
code: str = Field(..., description="供应商编码", examples=["TX001"])
company_type: str = Field(..., description="供应商类型", examples=["other"])
parent_id: Optional[str] = Field(None, description="上级供应商ID", examples=[None])
path: List[str] = Field(..., description="供应商所处路径", examples=[["company:001"]])
status: str = Field(..., description="供应商状态", examples=["active"])
sort: int = Field(..., description="排序", examples=[1])
credit_code: Optional[str] = Field(None, description="信用代码", examples=["914403007109310121"])
created_by: str = Field(..., description="创建者ID", examples=["user:001"])
created_by_name: str = Field(..., description="创建者名称", examples=["张三"])
created_at: str = Field(..., description="创建时间", examples=["2023-10-01T12:00:00Z"])
updated_by: str = Field(..., description="更新者ID", examples=["user:001"])
updated_by_name: str = Field(..., description="更新者名称", examples=["张三"])
updated_at: str = Field(..., description="更新时间", examples=["2023-10-01T12:00:00Z"])
province: Optional[str] = Field(None, description="省份编码", examples=[None])
city: Optional[str] = Field(None, description="城市编码", examples=[None])
company_nature: List[Optional[str]] = Field(None, description="供应商性质数组", examples=[['supplier']])
city_name: Optional[str] = Field(None, description="城市名称", examples=[None])
province_name: Optional[str] = Field(None, description="省份名称", examples=[None])
region_path: Optional[list] = Field(default_factory=list, description="供应商国家城市地址编码列表")
country: Optional[str] = Field(None, description="国家编码", examples=[None])
country_name: Optional[str] = Field(None, description="国家名称", examples=[None])
class SupplierListResponse(BaseModel):
"""
供应商list
"""
page: int
page_size: int
total: int
list: List[SupplierDetailResponse] = []
+49
View File
@@ -0,0 +1,49 @@
from typing import List, Optional, Dict
from pydantic import BaseModel, EmailStr, Field
from ..common.base import BaseResponseWithID
class UserListDetailItem(BaseResponseWithID):
"""
用户列表内明细模型
"""
id: Optional[str] = Field(default=None, description="唯一标识符")
username: Optional[str] = Field(None, description="用户名")
name: Optional[str] = Field(None, description="姓名")
phone: Optional[str] = Field(None, description="手机号")
phone_country: Optional[str] = Field(None, description="手机号国家前缀")
country_code: Optional[str] = Field(None, description="国家编码")
id_card: Optional[str] = Field(None, description="身份证号")
status: Optional[str] = Field(None, description="状态")
email: Optional[EmailStr] = Field(None, description="邮箱地址")
user_type: str = Field(..., description="用户类型")
last_login_at: Optional[str] = Field(None, description="最后登录时间")
updated_at: Optional[str] = Field(None, description="更新时间")
company_id: Optional[str] = Field(None, description="公司id")
company_name: Optional[str] = Field(None, description="公司名称")
created_at: Optional[str] = Field(None, description="创建时间")
approval_status: Optional[str] = Field(None, description="审批状态")
approval_time: Optional[str] = Field(None, description="审批通过时间")
last_login_ip: Optional[str] = Field(None, description="最后登录ip")
project_ids: Optional[List[str]] = Field([], description="权限内项目id")
contract_ids: Optional[List[str]] = Field([], description="权限内合同id")
apply_files: Optional[List[Dict]] = Field([],
examples=[
{"id": "file:001", "name": "文件1",
"url": "https://example.com/file1.pdf"}
],
description='申请文件'
)
class UserListResponse(BaseModel):
"""
用户列表响应模型
"""
page: int
page_size: int
total: int
list: List[UserListDetailItem]