Yue 3 月之前
父節點
當前提交
8bb950c258
共有 30 個文件被更改,包括 1077 次插入330 次删除
  1. 12 12
      SourceCode/IntelligentRailwayCosting/.script/init.sql
  2. 10 10
      SourceCode/IntelligentRailwayCosting/.script/init_sqlserver.sql
  3. 1 1
      SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py
  4. 2 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/__init__.py
  5. 183 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py
  6. 25 48
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py
  7. 5 3
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py
  8. 6 6
      SourceCode/IntelligentRailwayCosting/app/core/dtos/quota_input.py
  9. 2 0
      SourceCode/IntelligentRailwayCosting/app/core/enum/__init__.py
  10. 52 0
      SourceCode/IntelligentRailwayCosting/app/core/enum/task.py
  11. 9 10
      SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py
  12. 1 0
      SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py
  13. 18 11
      SourceCode/IntelligentRailwayCosting/app/executor/__init__.py
  14. 23 10
      SourceCode/IntelligentRailwayCosting/app/executor/sender.py
  15. 303 0
      SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py
  16. 10 10
      SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py
  17. 41 13
      SourceCode/IntelligentRailwayCosting/app/routes/project_task.py
  18. 19 19
      SourceCode/IntelligentRailwayCosting/app/services/project_quota.py
  19. 133 64
      SourceCode/IntelligentRailwayCosting/app/services/project_task.py
  20. 11 0
      SourceCode/IntelligentRailwayCosting/app/stores/budget.py
  21. 38 0
      SourceCode/IntelligentRailwayCosting/app/stores/chapter.py
  22. 21 23
      SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py
  23. 3 3
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_quota.py
  24. 3 3
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_quota.py
  25. 32 1
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py
  26. 1 1
      SourceCode/IntelligentRailwayCosting/app/test/sqlserver_test.py
  27. 1 1
      SourceCode/IntelligentRailwayCosting/app/views/login.py
  28. 4 1
      SourceCode/IntelligentRailwayCosting/app/views/project.py
  29. 98 79
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js
  30. 10 1
      SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html

+ 12 - 12
SourceCode/IntelligentRailwayCosting/.script/init.sql

@@ -5,6 +5,7 @@ USE iwb_railway_costing_v1;
 CREATE TABLE IF NOT EXISTS project_task (
     id INT AUTO_INCREMENT PRIMARY KEY,
     task_name VARCHAR(255) NOT NULL COMMENT '任务名称',
+    task_sort TINYINT DEFAULT 0 COMMENT '任务排序',
     task_desc VARCHAR(1000) COMMENT '任务描述',
     project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
     budget_id int NOT NULL DEFAULT 0 COMMENT '概算序号',
@@ -34,25 +35,24 @@ CREATE TABLE IF NOT EXISTS project_task (
     INDEX idx_created_at (created_at)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目任务表';
 
--- 创建项目任务
+-- 创建项目定额
 CREATE TABLE IF NOT EXISTS project_quota (
     id INT AUTO_INCREMENT PRIMARY KEY,
     task_id int NOT NULL COMMENT '任务编号',
     project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
     budget_id int NOT NULL COMMENT '概算序号',
+    budget_code VARCHAR(50) COMMENT '概算编号',
     item_id int NOT NULL COMMENT '条目序号',
     item_code VARCHAR(255) NOT NULL COMMENT '条目编号',
     quota_code VARCHAR(50) COMMENT '定额编号',
-    project_name VARCHAR(255) COMMENT '工程或费用项目名称',
-    unit VARCHAR(20) COMMENT '单位',
-    project_quantity FLOAT COMMENT '工程数量',
-    project_quantity_input VARCHAR(1000) COMMENT '工程数量输入',
-    quota_adjustment VARCHAR(1000) COMMENT '定额调整',
-    unit_price FLOAT COMMENT '单价',
-    total_price FLOAT COMMENT '合价',
-    unit_weight FLOAT COMMENT '单重',
-    total_weight FLOAT COMMENT '合重',
-    labor_cost FLOAT COMMENT '人工费',
+    entry_name VARCHAR(255) COMMENT '工程或费用项目名称',
+    units VARCHAR(20) COMMENT '单位',
+    amount FLOAT COMMENT '数量',
+    ex_file VARCHAR(50) COMMENT 'excel文件',
+    ex_cell VARCHAR(50) COMMENT '数量单元格位置,例如"C17"',
+    ex_row VARCHAR(1000) COMMENT '该行内容,由逗号连接多个单元格得到',
+    ex_unit VARCHAR(50) COMMENT 'excel中给出的单位',
+    ex_amount FLOAT COMMENT 'excel中给出的数量',
     process_status TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)',
     process_time DATETIME COMMENT '处理时间',
     process_error VARCHAR(1000) COMMENT '处理错误信息',
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS project_quota (
     INDEX idx_item_id (item_id),
     INDEX idx_item_code (item_code),
     INDEX idx_created_at (created_at)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目任务表';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目定额表';
 
 
 

+ 10 - 10
SourceCode/IntelligentRailwayCosting/.script/init_sqlserver.sql

@@ -38,6 +38,7 @@ BEGIN
 CREATE TABLE [dbo].[project_task] (
     [id] INT IDENTITY(1,1) PRIMARY KEY,
     [task_name] NVARCHAR(255) NOT NULL,
+    [task_sort] INT DEFAULT 0,
     [task_desc] NVARCHAR(1000) NULL,
     [project_id] NVARCHAR(50) NOT NULL,
     [budget_id] INT DEFAULT 0,
@@ -78,19 +79,18 @@ CREATE TABLE [dbo].[project_quota] (
     [task_id] INT NOT NULL,
     [project_id] NVARCHAR(50) NOT NULL,
     [budget_id] INT NOT NULL,
+    [budget_code] NVARCHAR(50) NULL,
     [item_id] INT NOT NULL,
     [item_code] NVARCHAR(255) NOT NULL,
     [quota_code] NVARCHAR(50) NULL,
-    [project_name] NVARCHAR(255) NULL,
-    [unit] NVARCHAR(20) NULL,
-    [project_quantity] FLOAT NULL,
-    [project_quantity_input] NVARCHAR(1000) NULL,
-    [quota_adjustment] NVARCHAR(1000) NULL,
-    [unit_price] FLOAT NULL,
-    [total_price] FLOAT NULL,
-    [unit_weight] FLOAT NULL,
-    [total_weight] FLOAT NULL,
-    [labor_cost] FLOAT NULL,
+    [entry_name] NVARCHAR(255) NULL,
+    [units] NVARCHAR(20) NULL,
+    [amount] FLOAT NULL,
+    [ex_file] NVARCHAR(50) NULL,
+    [ex_cell] NVARCHAR(50) NULL,
+    [ex_row] NVARCHAR(1000) NULL,
+    [ex_unit] NVARCHAR(50) NULL,
+    [ex_amount] FLOAT NULL,
     [process_status] INT NOT NULL DEFAULT 0,
     [process_time] DATETIME NULL,
     [process_error] NVARCHAR(1000) NULL,

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py

@@ -12,7 +12,7 @@ class AppConfig:
 
     @property
     def version(self):
-        return self._version
+        return self._version or "2020"
 
     @property
     def use_version(self)->bool:

+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/__init__.py

@@ -9,4 +9,6 @@ from .total_budget_item import TotalBudgetItemDto
 from .chapter import ChapterDto
 from .quota_input import QuotaInputDto
 
+from .excel_parse import ExcelParseDto,ExcelParseZgsDto,ExcelParseItemDto,ExcelParseFileDto,ExcelParseResultDataDto,ExcelParseResultDto
+
 

+ 183 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py

@@ -0,0 +1,183 @@
+from core.dtos import TotalBudgetInfoDto, ChapterDto
+import base64
+
+
+class ExcelParseZgsDto:
+    def __init__(self,
+                 zgs_id:int,
+                 zgs_name:str):
+        self.zgs_id = zgs_id
+        self.zgs_name = zgs_name
+
+    @classmethod
+    def from_dto(cls,dto: TotalBudgetInfoDto):
+        return cls(
+            zgs_id=dto.zgs_id,
+            zgs_name=dto.zgs_name
+        )
+    def to_dict(self):
+        return {
+            "zgs_id":self.zgs_id,
+            "zgs_name":self.zgs_name
+        }
+
+class ExcelParseItemDto:
+    def __init__(self,
+                 item_id:int,
+                 item_code:str,
+                 item_name:str
+                 ):
+        self.item_id = item_id
+        self.item_code = item_code
+        self.item_name = item_name
+
+    @classmethod
+    def from_dto(cls,dto:ChapterDto):
+        return cls(
+            item_id=dto.item_id,
+            item_code=dto.item_code,
+            item_name=dto.item_name
+        )
+    def to_dict(self):
+        return {
+            "item_id":self.item_id,
+            "item_code":self.item_code,
+            "item_name":self.item_name
+        }
+
+class ExcelParseFileDto:
+    def __init__(self,
+                 file_id:str,
+                 content:str
+                 ):
+        self.file_id = file_id
+        self.content = content
+
+    def to_dict(self):
+        return {
+            "file_id": base64.b64encode(self.file_id.encode()).decode(),
+            "content": self.content
+        }
+
+class ExcelParseDto:
+    def __init__(self,
+                 task_id:int,
+                 version:str,
+                 project_id:str,
+                 project_name:str,
+                 project_stage:str,
+                 selected_zgs_id:int,
+                 zgs_list:list[ExcelParseZgsDto],
+                 hierarchy:list[ExcelParseItemDto],
+                 components:list[ExcelParseItemDto],
+                 file_excel:list[ExcelParseFileDto]):
+        self.task_id = task_id
+        self.version = version
+        self.project_id = project_id
+        self.project_name = project_name
+        self.project_stage = project_stage
+        self.selected_zgs_id = -1 if selected_zgs_id == 0 else selected_zgs_id
+        self.zgs_list = zgs_list
+        self.hierarchy = hierarchy
+        self.components = components
+        self.file_excel = file_excel
+
+    def to_dict(self):
+        return {
+            "task_id":self.task_id,
+            "version":self.version,
+            "project_id":self.project_id,
+            "project_name":self.project_name,
+            "project_stage":self.project_stage,
+            "selected_zgs_id":self.selected_zgs_id,
+            "zgs_list":[zgs.to_dict() for zgs in self.zgs_list],
+            "hierarchy":[item.to_dict() for item in self.hierarchy],
+            "components":[item.to_dict() for item in self.components],
+            "file_excel":[file.to_dict() for file in self.file_excel]
+        }
+
+class ExcelParseResultDataDto:
+    # {
+    #     "zgs_id": 总概算id, // int
+    #     "zgs_code": 概算编号,
+    #     "item_id": 条⽬序号, // int
+    #     "item_code": 条⽬编码,
+    #     "dinge_code": 定额编号,
+    #     "entry_name": ⼯程或费⽤项⽬名称,来⾃于定额表,
+    #     "units": 单位,
+    #     "amount": 数量, // number
+    #     "ex_file_id": excel⽂件id, // str
+    #     "ex_cell": 数量单元格位置,例如 "C17",
+    #     "ex_row": 该⾏内容,由逗号连接多个单元格得到,
+    #     "ex_unit": excel中给出的单位,
+    #     "ex_amount": excel中给出的数量, // number
+    # }
+    def __init__(self,
+                 zgs_id:int, # 总概算id
+                 zgs_code:str, # 总概算编号
+                 item_id:int, # 条⽬序号
+                 item_code:str, # 条⽬编码
+                 entry_name:str, # ⼯程或费⽤项⽬名称,来⾃于定额表,
+                 dinge_code:str, # 定额编号,
+                 units:str, # 单位,
+                 amount:float, # 数量,
+                 ex_file_id:str, # excel⽂件id,
+                 ex_cell:str, # 数量单元格位置,例如 "C17",
+                 ex_row:str, # 该⾏内容,由逗号连接多个单元格得到,
+                 ex_unit:str, # excel中给出的单位,
+                 ex_amount:float # excel中给出的数量,
+                 ):
+        self.zgs_id = zgs_id
+        self.zgs_code = zgs_code
+        self.item_id = item_id
+        self.item_code = item_code
+        self.entry_name = entry_name
+        self.dinge_code = dinge_code
+        self.units = units
+        self.amount = amount
+        self.ex_file_id = ex_file_id
+        self.ex_cell = ex_cell
+        self.ex_row = ex_row
+        self.ex_unit = ex_unit
+        self.ex_amount = ex_amount
+
+
+    @classmethod
+    def from_dict(cls, data: dict):
+        return cls(
+            zgs_id=data.get('zgs_id', 0),
+            zgs_code=data.get('zgs_code', ''),
+            item_id=data.get('item_id', 0),
+            item_code=data.get('item_code', ''),
+            entry_name=data.get('name', ''),
+            dinge_code=data.get('dinge_code', ''),
+            units=data.get('units', ''),
+            amount=data.get('amount', 0.0),
+            ex_file_id= base64.b64decode(data.get('ex_file_id', '').encode()).decode() if data.get('ex_file_id', '') else '',
+            ex_cell=data.get('ex_cell', ''),
+            ex_row=data.get('ex_row', ''),
+            ex_unit=data.get('ex_unit', ''),
+            ex_amount=data.get('ex_amount', 0.0)
+        )
+
+class ExcelParseResultDto:
+    def __init__(self,
+                 task_id:int,
+                 result:int,
+                 data:list[ExcelParseResultDataDto],
+                 reason:str
+    ):
+        self.task_id = task_id
+        self.result = result  #-1-失败;0-运行中;1-成功
+        self.data = data
+        self.reason = reason
+
+    @classmethod
+    def from_dict(cls, response: dict):
+        data = [ExcelParseResultDataDto.from_dict(item) for item in response.get('data', [])]
+        return cls(
+            task_id=response.get('task_id', 0),
+            result=response.get('result', 0),
+            data=data,
+            reason=response.get('reason', '')
+        )

+ 25 - 48
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py

@@ -1,6 +1,8 @@
 from pydantic import BaseModel
 from typing import Optional
 from datetime import datetime
+
+from sqlalchemy import update
 from core.models import ProjectQuotaModel
 
 class ProjectQuotaDto(BaseModel):
@@ -9,32 +11,23 @@ class ProjectQuotaDto(BaseModel):
     task_id:Optional[int] = None
     project_id: str
     budget_id: int
+    budget_code: str
     item_id: int
     item_code: str
     quota_code: Optional[str] = None
-    project_name: Optional[str] = None
-    unit: Optional[str] = None
-    project_quantity: Optional[float] = None
-    project_quantity_input: Optional[str] = None
-    quota_adjustment: Optional[str] = None
-    unit_price: Optional[float] = None
-    total_price: Optional[float] = None
-    unit_weight: Optional[float] = None
-    total_weight: Optional[float] = None
-    labor_cost: Optional[float] = None
-    process_status: Optional[int] = 0
-    process_time: Optional[datetime] = None
-    process_error: Optional[str] = None
-    send_status: Optional[int] = 0
-    send_time: Optional[datetime] = None
-    send_error: Optional[str] = None
-    is_del: Optional[int] = 0
-    deleted_by: Optional[str] = None
-    deleted_at: Optional[datetime] = None
-    created_by: Optional[str] = None
+    entry_name: Optional[str] = None
+    units: Optional[str] = None
+    amount: Optional[float] = None
+    ex_file: Optional[str] = None
+    ex_cell: Optional[str] = None
+    ex_row: Optional[str] = None
+    ex_unit: Optional[str] = None
+    ex_amount: Optional[float] = None
     created_at: Optional[datetime] = None
-    updated_by: Optional[str] = None
+    created_by: Optional[str] = None
     updated_at: Optional[datetime] = None
+    updated_by: Optional[str] = None
+
 
     @classmethod
     def from_model(cls, model: ProjectQuotaModel) -> 'ProjectQuotaDto':
@@ -44,32 +37,18 @@ class ProjectQuotaDto(BaseModel):
             task_id=model.task_id,
             project_id=model.project_id,
             budget_id=model.budget_id,
+            budget_code=model.budget_code,
             item_id=model.item_id,
             item_code=model.item_code,
             quota_code=model.quota_code,
-            project_name=model.project_name,
-            unit=model.unit,
-            project_quantity=model.project_quantity,
-            project_quantity_input=model.project_quantity_input,
-            quota_adjustment=model.quota_adjustment,
-            unit_price=model.unit_price,
-            total_price=model.total_price,
-            unit_weight=model.unit_weight,
-            total_weight=model.total_weight,
-            labor_cost=model.labor_cost,
-            process_status=model.process_status,
-            process_time=model.process_time,
-            process_error=model.process_error,
-            send_status=model.send_status,
-            send_time=model.send_time,
-            send_error=model.send_error,
-            is_del=model.is_del,
-            deleted_by=model.deleted_by,
-            deleted_at=model.deleted_at,
-            created_by=model.created_by,
-            created_at=model.created_at,
-            updated_by=model.updated_by,
-            updated_at=model.updated_at
+            entry_name=model.entry_name,
+            units = model.units,
+            amount=model.amount,
+            ex_file=model.ex_file,
+            ex_cell=model.ex_cell,
+            ex_row=model.ex_row,
+            ex_unit=model.ex_unit,
+            ex_amount=model.ex_amount,
         )
 
 
@@ -107,15 +86,13 @@ class ProjectQuotaDto(BaseModel):
             item_code="",
             id=data.get("i",0),
             quota_code=data.get("q",""),
-            unit=data.get("u",""),
-            unit_price=data.get("p",0),
-            total_price=data.get("tp",0)
+            units=data.get("u",""),
         )
 
     def to_ai_dict(self):
         return {
             "i": self.id,
-            "n": self.project_name,
+            "n": self.entry_name,
             "u": self.unit,
             "q": self.project_quantity,
         }

+ 5 - 3
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py

@@ -7,6 +7,7 @@ class ProjectTaskDto(BaseModel):
     """项目任务DTO"""
     id: Optional[int] = None
     task_name: str
+    task_sort: int = 0
     task_desc: Optional[str] = None
     project_id: str
     budget_id: Optional[int] = 0
@@ -36,6 +37,7 @@ class ProjectTaskDto(BaseModel):
         return cls(
             id=model.id,
             task_name=model.task_name,
+            task_sort=model.task_sort,
             task_desc=model.task_desc,
             project_id=model.project_id,
             budget_id=model.budget_id,
@@ -63,8 +65,8 @@ class ProjectTaskDto(BaseModel):
     @classmethod
     def from_dict(cls, data: dict) -> 'ProjectTaskDto':
         """从字典创建DTO对象"""
-        if 'budget_id' in data and 'budget_id' not in data:
-            data['budget_id'] = data.pop('budget_id')
+        # if 'budget_id' in data and 'budget_id' not in data:
+        #     data['budget_id'] = data.pop('budget_id')
         return cls(**data)
 
     def to_dict(self) -> dict:
@@ -72,6 +74,6 @@ class ProjectTaskDto(BaseModel):
         return self.model_dump()
 
     def get_path(self):
-        return f"{self.project_id}_{self.item_id}({self.item_code})_{self.id}"
+        return f"{self.project_id}_{self.item_id}({self.item_code})_{self.id}/{datetime.now().strftime('%Y%m%d%H%M%S')}"
     class Config:
         from_attributes = True

+ 6 - 6
SourceCode/IntelligentRailwayCosting/app/core/dtos/quota_input.py

@@ -62,9 +62,9 @@ class QuotaInputDto(BaseModel):
             project_quantity=model.project_quantity,
             # project_quantity_input=model.project_quantity_input,
             # quota_adjustment=model.quota_adjustment,
-            unit_price=model.unit_price,
+            # unit_price=model.unit_price,
             # compilation_unit_price=model.compilation_unit_price,
-            total_price=model.total_price,
+            # total_price=model.total_price,
             # compilation_total_price=model.compilation_total_price,
             # unit_weight=model.unit_weight,
             # total_weight=model.total_weight,
@@ -103,14 +103,14 @@ class QuotaInputDto(BaseModel):
             item_id=quota_dto.item_id,
             quota_code=quota_dto.quota_code,
             # sequence_number=quota_dto.sequence_number,
-            project_name=quota_dto.project_name,
+            project_name=quota_dto.entry_name,
             unit=quota_dto.unit,
-            project_quantity=quota_dto.project_quantity,
+            project_quantity=quota_dto.amount,
             # project_quantity_input=quota_dto.project_quantity_input,
             # quota_adjustment=quota_dto.quota_adjustment,
-            unit_price=quota_dto.unit_price,
+            # unit_price=quota_dto.unit_price,
             # compilation_unit_price=quota_dto.compilation_unit_price,
-            total_price=quota_dto.total_price,
+            # total_price=quota_dto.total_price,
             # compilation_total_price=quota_dto.compilation_total_price,
             # unit_weight=quota_dto.unit_weight,
             # total_weight=quota_dto.total_weight,

+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/core/enum/__init__.py

@@ -1,2 +1,4 @@
 from .log import OperationType,OperationModule,OperationResult
+from .task import TaskStatusEnum,SendStatusEnum
+
 

+ 52 - 0
SourceCode/IntelligentRailwayCosting/app/core/enum/task.py

@@ -0,0 +1,52 @@
+from enum import Enum
+
+class TaskStatusEnum(Enum):
+    NEW = 0
+    WAIT = 1
+    PROCESSING = 2
+    SUCCESS = 3
+    CANCELED = 4
+    FAILURE = 5
+    CHANGE = 6
+
+
+    @classmethod
+    def get_name(cls, status: int):
+        for k, v in cls.get_dict().items():
+            if v == status:
+                return k
+        return None
+
+    @classmethod
+    def get_dict(cls):
+        return {
+            "草稿": cls.NEW,
+            "等待运行": cls.WAIT,
+            "运行中": cls.PROCESSING,
+            "运行成功": cls.SUCCESS,
+            "运行失败": cls.FAILURE,
+            "已取消": cls.CANCELED,
+            "已修改": cls.CHANGE
+        }
+
+class SendStatusEnum(Enum):
+    NEW = 0
+    PROCESSING = 1
+    SUCCESS = 2
+    FAILURE = 3
+
+    @classmethod
+    def get_name(cls, status: int):
+        for k, v in cls.get_dict().items():
+            if v == status:
+                return k
+        return None
+
+    @classmethod
+    def get_dict(cls):
+        return {
+            "未发送": cls.NEW,
+            "发送中": cls.PROCESSING,
+            "发送成功": cls.SUCCESS,
+            "发送失败": cls.FAILURE
+        }

+ 9 - 10
SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py

@@ -10,19 +10,18 @@ class ProjectQuotaModel(Base):
     task_id = Column(Integer, nullable=False, comment='任务编号')
     project_id = Column(String(50), nullable=False, comment='项目编号')
     budget_id = Column(Integer, nullable=False, comment='概算序号')
+    budget_code = Column(String(50), comment='概算编号')
     item_id = Column(Integer, nullable=False, comment='条目序号')
     item_code = Column(String(255), nullable=False, comment='条目编号')
     quota_code = Column(String(50), comment='定额编号')
-    project_name = Column(String(255), comment='工程或费用项目名称')
-    unit = Column(String(20), comment='单位')
-    project_quantity = Column(Float, comment='工程数量')
-    project_quantity_input = Column(String(1000), comment='工程数量输入')
-    quota_adjustment = Column(String(1000), comment='定额调整')
-    unit_price = Column(Float, comment='单价')
-    total_price = Column(Float, comment='合价')
-    unit_weight = Column(Float, comment='单重')
-    total_weight = Column(Float, comment='合重')
-    labor_cost = Column(Float, comment='人工费')
+    entry_name = Column(String(255), comment='工程或费用项目名称')
+    units = Column(String(20), comment='单位')
+    amount = Column(Float, comment='数量')
+    ex_file = Column(String(50), comment='excel⽂件')
+    ex_cell = Column(String(50), comment='数量单元格位置,例如"C17"')
+    ex_row = Column(String(1000), comment='该⾏内容,由逗号连接多个单元格得到')
+    ex_unit = Column(String(50), comment='excel中给出的单位')
+    ex_amount = Column(Float, comment='excel中给出的数量')
     process_status = Column(Integer, nullable=False, default=0, comment='处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)')
     process_time = Column(DateTime, comment='处理时间')
     process_error = Column(String(1000), comment='处理错误信息')

+ 1 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py

@@ -8,6 +8,7 @@ class ProjectTaskModel(Base):
 
     id = Column(Integer, primary_key=True, autoincrement=True)
     task_name = Column(String(255), nullable=False, comment='任务名称')
+    task_sort = Column(Integer, nullable=False, default=0, comment='任务排序')
     task_desc = Column(String(1000), comment='任务描述')
     project_id = Column(String(50), nullable=False, comment='项目编号')
     budget_id = Column(Integer, comment='概算序号')

+ 18 - 11
SourceCode/IntelligentRailwayCosting/app/executor/__init__.py

@@ -1,28 +1,35 @@
-from .collector import Collector
+from .task_runner import TaskRunner
 from .processor import Processor
 from .sender import Sender
 from core.dtos import ProjectTaskDto, ProjectQuotaDto
 
 
-def collect_task(task:ProjectTaskDto):
-    return Collector().collect(task)
+def run_task(task:ProjectTaskDto):
+    return TaskRunner.run(task)
 
-def process_task(task:ProjectTaskDto):
-    return Processor().process(task)
+def cancel_task(task:ProjectTaskDto):
+    return TaskRunner.cancel(task)
+
+def project_is_running(project_id:str):
+    return TaskRunner.get_project_running_state(project_id)
+
+# def process_task(task:ProjectTaskDto):
+#     return Processor().process(task)
+# def process_quota(quota:ProjectQuotaDto):
+#     return Processor().process_quota(quota)
 
 def send_task(task:ProjectTaskDto):
     return Sender().send(task)
 
-def process_quota(quota:ProjectQuotaDto):
-    return Processor().process_quota(quota)
 
 def send_quota(quota:ProjectQuotaDto):
     return Sender().send_quota(quota)
 
+
 __all__=[
-    'collect_task',
-    'process_task',
+    'run_task',
+    'cancel_task',
     'send_task',
-    'process_quota',
-    'send_quota'
+    'send_quota',
+    'project_is_running'
  ]

+ 23 - 10
SourceCode/IntelligentRailwayCosting/app/executor/sender.py

@@ -1,5 +1,6 @@
 import tools.utils as utils
 from core.dtos import ProjectTaskDto, ProjectQuotaDto, QuotaInputDto
+from core.enum import SendStatusEnum
 from stores import ProjectQuotaStore, ProjectTaskStore,QuotaInputStore
 
 
@@ -13,37 +14,49 @@ class Sender:
     def send(self,task:ProjectTaskDto):
         try:
             self._logger.info(f"开始发送任务:{task.task_name}")
-            self._task_store.update_send_status(task.id,1)
+            self._task_store.update_send_status(task.id, SendStatusEnum.PROCESSING.value)
 
             error_count = 0
             data_list = self._quota_store.get_quotas_by_task_id(task.id,True)
             for data in data_list:
-                msg = self._quota_store.update_send_status(data.id,1)
+                msg = self._quota_store.update_send_status(data.id,SendStatusEnum.PROCESSING.value)
                 if msg:
                     error_count+=1
                     continue
 
-            self._task_store.update_send_status(task.id,2)
+            self._task_store.update_send_status(task.id,SendStatusEnum.SUCCESS.value)
             self._logger.info(f"发送任务:{task.task_name}完成,{error_count}项错误/共{len(data_list)}项")
             return None
         except Exception as e:
             msg = f"任务发送失败,原因:{e}"
             self._logger.error(f"发送任务:{task.task_name},{msg}")
-            self._task_store.update_send_status(task.id, 3, msg)
+            self._task_store.update_send_status(task.id, SendStatusEnum.FAILURE.value, msg)
             return msg
 
-
     def send_quota(self,quota:ProjectQuotaDto):
         try:
             self._logger.info(f"开始发送定额:{quota.id}")
-            self._quota_store.update_send_status(quota.id, 1)
+            self._quota_store.update_send_status(quota.id, SendStatusEnum.PROCESSING.value)
             quota_input = QuotaInputDto.from_quota_dto(quota)
-            self._quota_input_store.create_quota(quota_input)
-            self._quota_store.update_send_status(quota.id, 2)
+            self._save_quota(quota_input,quota.project_id)
+            self._quota_store.update_send_status(quota.id, SendStatusEnum.SUCCESS.value)
             self._logger.info(f"发送定额:{quota.id}完成")
             return None
         except Exception as e:
             msg = f"定额发送失败,原因:{e}"
             self._logger.error(f"发送定额:{quota.id},{msg}")
-            self._quota_store.update_send_status(quota.id, 3, msg)
-            return msg
+            self._quota_store.update_send_status(quota.id, SendStatusEnum.FAILURE.value, msg)
+            return msg
+
+    def _save_quota(self,quota:QuotaInputDto,project_id:str):
+        try:
+            self._logger.info(f"开始保存定额:{quota.id}")
+            data = self._quota_input_store.get_quota(project_id, quota.budget_id,quota.item_id,quota.quota_code)
+            if data:
+                self._quota_input_store.update_quota(project_id, quota)
+            else:
+                self._quota_input_store.create_quota(project_id, quota)
+        except Exception as e:
+            msg = f"保存定额失败,原因:{e}"
+            self._logger.error(f"保存定额:{quota.id},{msg}")
+            raise Exception(msg)

+ 303 - 0
SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py

@@ -0,0 +1,303 @@
+from time import sleep
+
+import requests,tools.utils as utils, core.configs as configs
+from core.dtos import ProjectDto,ProjectTaskDto, ProjectQuotaDto, ChapterDto, TotalBudgetInfoDto, ExcelParseResultDataDto
+from core.enum import TaskStatusEnum
+from stores import ProjectStore,ProjectTaskStore, ProjectQuotaStore, ChapterStore, BudgetStore
+from core.dtos import ExcelParseDto,ExcelParseZgsDto,ExcelParseItemDto,ExcelParseFileDto,ExcelParseResultDto
+import threading
+
+class TaskRunner:
+    _is_running = {}
+    _task_wait_list = {}
+    _running_projects = set()
+    _max_concurrent_projects = 10
+    _logger = utils.get_logger()
+    _project_store = ProjectStore()
+    _budget_store = BudgetStore()
+    _chapter_store = ChapterStore()
+    _task_store = ProjectTaskStore()
+    _quota_store = ProjectQuotaStore()
+    _task_submit_url = "/task_submit"
+    _task_status_url = "/task_status"
+    _task_cancel_url = "/cancel_task"
+    _lock = threading.Lock()
+
+    @classmethod
+    def run(cls, task:ProjectTaskDto=None):
+        if task:
+            project_id = task.project_id
+            if project_id not in cls._task_wait_list:
+                cls._task_wait_list[project_id] = []
+            cls._logger.info(f"添加到待运行队列:{task.task_name}")
+            cls._task_wait_list[project_id].append(task)
+        
+        if task and task.project_id in cls._is_running and cls._is_running[task.project_id]:
+            return
+
+        # 如果有新任务但并发数已满,直接返回等待
+        if task and len(cls._running_projects) >= cls._max_concurrent_projects and task.project_id not in cls._running_projects:
+            return
+        if not task:
+            if  cls._task_wait_list and len(cls._task_wait_list) > 0:
+                for project_id in cls._task_wait_list.keys():
+                    cls._execute_project_tasks(project_id)
+            else:
+                cls._sync_wait_list()
+                sleep(60*10)
+                return
+        else:
+            cls._execute_project_tasks(task.project_id)
+
+
+    @classmethod
+    def _execute_project_tasks(cls, project_id: str):
+        try:
+            # 如果项目不在运行集合中且并发数已满,直接返回等待
+            with cls._lock:
+                if project_id not in cls._running_projects and len(cls._running_projects) >= cls._max_concurrent_projects:
+                    return
+                cls._is_running[project_id] = True
+                cls._running_projects.add(project_id)
+            
+            def execute_tasks():
+                try:
+                    while project_id in cls._task_wait_list and len(cls._task_wait_list[project_id]) > 0:
+                        current_task = cls._task_wait_list[project_id].pop(0)
+                        try:
+                            cls._submit_task(current_task)
+                        except Exception as e:
+                            cls._logger.error(f"运行任务失败:{current_task.task_name}, {str(e)}")
+                finally:
+                    with cls._lock:
+                        if project_id in cls._is_running:
+                            cls._is_running[project_id] = False
+                        if project_id in cls._running_projects:
+                            cls._running_projects.remove(project_id)
+                        if project_id in cls._task_wait_list and len(cls._task_wait_list[project_id]) == 0:
+                            del cls._task_wait_list[project_id]
+                    cls._sync_wait_list()
+            
+            # 创建新线程执行任务
+            thread = threading.Thread(target=execute_tasks)
+            thread.start()
+        except Exception as e:
+            cls._logger.error(f"执行项目任务失败:{project_id}, {str(e)}")
+            with cls._lock:
+                if project_id in cls._is_running:
+                    cls._is_running[project_id] = False
+                if project_id in cls._running_projects:
+                    cls._running_projects.remove(project_id)
+                if project_id in cls._task_wait_list and len(cls._task_wait_list[project_id]) == 0:
+                    del cls._task_wait_list[project_id]
+            cls._sync_wait_list()
+        
+    @classmethod
+    def _sync_wait_list(cls,project_id:str=None):
+        try:
+            cls._logger.info(f"开始同步待运行队列")
+            tasks = cls._task_store.get_wait_tasks(project_id)
+            for task in tasks:
+                if task.project_id not in cls._task_wait_list:
+                    cls._task_wait_list[task.project_id] = []
+                if task not in cls._task_wait_list[task.project_id]:
+                    cls._task_wait_list[task.project_id].append(task)
+            total_tasks = sum(len(tasks) for tasks in cls._task_wait_list.values())
+            cls._logger.info(f"同步待运行队列完成,同步了{total_tasks}条数据")
+            if total_tasks > 0:
+                cls.run()
+            return None
+        except Exception as e:
+            msg = f"同步待运行队列失败,原因:{e}"
+            cls._logger.error(f"同步待运行队列失败,原因:{e}")
+            return msg
+
+    @classmethod
+    def cancel(cls, task:ProjectTaskDto):
+        try:
+            cls._logger.info(f"开始取消运行任务:{task.id}")
+            res = cls._call_api(cls._task_cancel_url,{"task_id":task.id})
+            if res.result==-1:
+                cls._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
+                return res.reason
+            project = cls._project_store.get(task.project_id)
+            if not project:
+                cls._logger.error(f"取消运行任务:{task.id}失败,原因:项目不存在")
+                return "项目不存在"
+            if res.data and len(res.data)>0:
+                cls._insert_data(task,project,res.data)
+            if res.result == 0 :
+                cls._logger.info(f"取消运行任务:{task.id}成功")
+                cls._task_store.update_task_status(task.id, TaskStatusEnum.CANCELED.value)
+            elif res.result == 1:
+                cls._logger.info(f"取消运行任务失败:{task.id}已运行完成")
+                cls._task_store.update_task_status(task.id, TaskStatusEnum.SUCCESS.value)
+                return f"取消失败,任务已运行完成"
+            return None
+        except Exception as e:
+            msg = f"取消运行任务失败,原因:{e}"
+            cls._logger.error(f"取消运行任务失败,原因:{e}")
+            return msg
+
+    @classmethod
+    def _submit_task(cls, task:ProjectTaskDto):
+        try:
+            cls._logger.info(f"开始运行任务:{task.task_name}")
+            cls._task_store.update_task_status(task.id, TaskStatusEnum.PROCESSING.value)
+            if not task.file_path:
+                raise Exception("任务文件不存在")
+            files, msg = cls._read_files(task.file_path)
+            if not files or len(files)==0:
+                raise Exception(msg)
+            project = cls._project_store.get(task.project_id)
+            if not project:
+                raise Exception("项目不存在")
+            budget_models = cls._budget_store.get_budget_info(task.project_id)
+            budgets = [TotalBudgetInfoDto.from_model(budget) for budget in budget_models]
+            parents = cls._chapter_store.get_all_parents_chapter_items(task.project_id, task.item_code)
+            children = cls._chapter_store.get_all_children_chapter_items(task.project_id, task.item_code)
+            data,msg = cls._build_api_body(task,project,budgets,parents,children,files)
+            if not data:
+                raise Exception(msg)
+            res = cls._call_api(cls._task_submit_url,data)
+
+            if res.result==-1:
+                cls._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
+                return res.reason
+
+            if res.data and len(res.data)>0:
+                cls._insert_data(task,project,res.data)
+            if res.result == 1 :
+                cls._logger.info(f"运行任务:{task.task_name}完成")
+                cls._task_store.update_task_status(task.id,TaskStatusEnum.SUCCESS.value)
+            else:
+                cls._logger.info(f"运行任务:{task.task_name}请求中,等待结果")
+                cls._query_task(task,project)
+            return None
+        except Exception as e:
+            msg = f"任务运行失败,原因:{e}"
+            cls._logger.error(f"运行任务:{task.task_name}, {msg}")
+            cls._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, msg)
+            return msg
+
+    @classmethod
+    def _query_task(cls, task:ProjectTaskDto,project:ProjectDto):
+        try:
+            import time
+            while True:
+                time.sleep(30)
+                res = cls._call_api(cls._task_status_url,{"task_id":task.id})
+                if res.result==-1:
+                    cls._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
+                    return res.reason
+                if res.data and len(res.data) > 0:
+                    cls._insert_data(task, project, res.data)
+                if res.result == 1:
+                    cls._logger.info(f"运行任务:{task.task_name}完成")
+                    cls._task_store.update_task_status(task.id, TaskStatusEnum.SUCCESS.value)
+                    break
+                else:
+                    cls._logger.info(f"运行任务:{task.task_name}请求中,等待结果")
+                    cls._query_task(task, project)
+        except Exception as e:
+            msg = f"任务状态查询失败,原因:{e}"
+            cls._logger.error(f"任务状态查询失败,原因:{e}")
+            raise Exception(msg)
+
+    @classmethod
+    def _read_files(cls,paths:str)->(list[ExcelParseFileDto],str):
+        try:
+            files=[]
+            cls._logger.debug(f"开始读取文件:{paths}")
+            path_list= paths.split(",")
+            for path in path_list:
+                file = utils.encode_file(path)
+                files.append(ExcelParseFileDto(file_id=path, content=file))
+            cls._logger.debug(f"读取文件完成:{paths}")
+            return files, ''
+        except Exception as e:
+            msg = f"读取文件失败,原因:{e}"
+            cls._logger.error(f"读取文件失败,原因:{e}")
+            return None,msg
+
+    @classmethod
+    def _build_api_body(cls,task:ProjectTaskDto,project:ProjectDto,budgets:list[TotalBudgetInfoDto],parents:list[ChapterDto],children:list[ChapterDto],files:list[ExcelParseFileDto]):
+        try:
+            budgets_data = [ExcelParseZgsDto.from_dto(budget) for budget in budgets]
+            parents_data = [ExcelParseItemDto.from_dto(parent) for parent in parents]
+            children_data = [ExcelParseItemDto.from_dto(child) for child in children]
+            data = ExcelParseDto(
+                task_id=task.id or 0,
+                version=configs.app.version or "2020",
+                project_id=task.project_id,
+                project_name=project.project_name,
+                project_stage=project.project_stage,
+                selected_zgs_id=task.selected_zgs_id or 0,
+                zgs_list=budgets_data,
+                hierarchy=parents_data,
+                components=children_data,
+                file_excel=files
+            )
+            return data, ""
+        except Exception as e:
+            msg = f"解析文件失败,原因:{e}"
+            cls._logger.error(f"解析文件失败,原因:{e}")
+            return None,msg
+
+    @classmethod
+    def _call_api(cls,api_url,data)->ExcelParseResultDto:
+       try:
+            url = f"{configs.app.collect_api_url}{api_url}"
+            response = requests.post(url, headers={"Content-Type": "application/json"}, json=data)
+            if response.status_code == 200:
+                result = response.json()
+                result_dto = ExcelParseResultDto.from_dict(result)
+                cls._logger.debug(f"调用接口成功:{result_dto}")
+                return result_dto
+            else:
+                cls._logger.error(f"调用接口失败,原因:{response.text}")
+                raise Exception(response.text)
+       except Exception as e:
+            msg = f"调用接口失败,原因:{e}"
+            cls._logger.error(f"调用接口:{msg}")
+            raise Exception(msg)
+
+    @classmethod
+    def _insert_data(cls,task:ProjectTaskDto,project:ProjectDto,data:list[ExcelParseResultDataDto]):
+        try:
+            cls._logger.debug(f"开始插入数据:{task.task_name}")
+
+            for item in data:
+                quota =  ProjectQuotaDto(
+                    budget_id=item.zgs_id,
+                    budget_code=item.zgs_code,
+                    project_id=project.project_id,
+                    item_code=item.item_code,
+                    item_id=item.item_id,
+                    quota_code=item.dinge_code,
+                    entry_name=item.entry_name,
+                    units=item.units,
+                    amount=item.amount,
+                    ex_file=item.ex_file_id,
+                    ex_cell=item.ex_cell,
+                    ex_row=item.ex_row,
+                    ex_unit=item.ex_unit,
+                    ex_amount=item.ex_amount,
+                    created_by=task.created_by,
+                )
+                cls._quota_store.create_quota(quota)
+            cls._logger.debug(f"插入数据完成:{task.task_name}")
+            return True
+        except Exception as e:
+            msg = f"插入数据失败,原因:{e}"
+            cls._logger.error(f"插入数据失败,原因:{e}")
+            return False,msg
+
+    @classmethod
+    def get_project_running_state(cls, project_id: str) -> bool:
+        return project_id in cls._is_running and cls._is_running[project_id] or len(cls._running_projects) >= cls._max_concurrent_projects
+
+
+
+
+

+ 10 - 10
SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py

@@ -53,16 +53,16 @@ def delete_quota(quota_id:int):
     except Exception as e:
         return ResponseBase.error(f'删除定额条目失败:{str(e)}')
 
-@project_quota_api.route('/start_process/<int:quota_id>', methods=['POST'])
-@Permission.authorize
-def start_process(quota_id:int):
-    try:
-        msg = quota_service.start_process(quota_id)
-        if msg:
-            return ResponseBase.error(msg)
-        return ResponseBase.success()
-    except Exception as e:
-        return ResponseBase.error(f'启动定额条目失败:{str(e)}')
+# @project_quota_api.route('/start_process/<int:quota_id>', methods=['POST'])
+# @Permission.authorize
+# def start_process(quota_id:int):
+#     try:
+#         msg = quota_service.start_process(quota_id)
+#         if msg:
+#             return ResponseBase.error(msg)
+#         return ResponseBase.success()
+#     except Exception as e:
+#         return ResponseBase.error(f'启动定额条目失败:{str(e)}')
 
 @project_quota_api.route('/start_send/<int:quota_id>', methods=['POST'])
 @Permission.authorize

+ 41 - 13
SourceCode/IntelligentRailwayCosting/app/routes/project_task.py

@@ -55,12 +55,14 @@ def save_task(task_id:int):
     try:
         # 从请求中获取表单数据
         form_data = request.form.to_dict()
+        budget_id = int(form_data.get('budget_id')) if form_data.get('budget_id') else 0
         item_id = int(form_data.get('item_id')) if form_data.get('item_id') else None
         project_id = form_data.get('project_id')
         item_code = form_data.get('item_code')
         task_name = form_data.get('task_name')
         task_desc = form_data.get('task_desc')
-        delete_old = form_data.get('delete_old', 'false').lower() == 'true'
+        run_now = form_data.get('run_now')=='true'
+        # delete_old = form_data.get('delete_old', 'false').lower() == 'true'
         # 获取上传的文件
         files = request.files.getlist('files')
         # 验证必要参数
@@ -69,6 +71,7 @@ def save_task(task_id:int):
         # 构建任务DTO
         task_dto = ProjectTaskDto(
             item_id=item_id,
+            budget_id=budget_id,
             project_id=project_id,
             item_code=item_code,
             task_name=task_name,
@@ -77,7 +80,9 @@ def save_task(task_id:int):
         )
         
         # 保存任务
-        task = task_service.save_task(task_id, task_dto, files, delete_old)
+        task = task_service.save_task(task_id, task_dto, files)
+        if run_now:
+            task_service.start_run_task(task.id)
         return ResponseBase.success(task.to_dict())
     except ValueError as ve:
         return ResponseBase.error(f'参数格式错误:{str(ve)}')
@@ -114,33 +119,56 @@ def download_file():
         return ResponseBase.error(f'非法文件路径{str(e)}')
     return send_from_directory(upload_folder.replace('\\', '/'), safe_filename, as_attachment=True)
 
-@project_task_api.route('/start_collect/<int:task_id>', methods=['POST'])
+# @project_task_api.route('/start_collect/<int:task_id>', methods=['POST'])
+# @Permission.authorize
+# def start_collect(task_id:int):
+#     try:
+#         msg = task_service.start_run_task(task_id)
+#         if msg:
+#             return ResponseBase.error(msg)
+#         return ResponseBase.success('启动采集成功')
+#     except Exception as e:
+#         return ResponseBase.error(f'启动采集失败:{str(e)}')
+
+# @project_task_api.route('/start_process/<int:task_id>', methods=['POST'])
+# @Permission.authorize
+# def start_process(task_id:int):
+#     try:
+#         msg = task_service.start_process(task_id)
+#         if msg:
+#             return ResponseBase.error(msg)
+#         return ResponseBase.success('启动处理成功')
+#     except Exception as e:
+#         return ResponseBase.error(f'启动处理失败:{str(e)}')
+
+@project_task_api.route('/start_task/<int:task_id>', methods=['POST'])
 @Permission.authorize
-def start_collect(task_id:int):
+def start_task(task_id:int):
     try:
-        msg = task_service.start_collect(task_id)
+        msg = task_service.start_run_task(task_id)
         if msg:
             return ResponseBase.error(msg)
-        return ResponseBase.success('启动采集成功')
+        return ResponseBase.success('运行成功')
     except Exception as e:
-        return ResponseBase.error(f'启动采集失败:{str(e)}')
+        return ResponseBase.error(f'运行失败:{str(e)}')
 
-@project_task_api.route('/start_process/<int:task_id>', methods=['POST'])
+@project_task_api.route('/cancel_task/<int:task_id>', methods=['POST'])
 @Permission.authorize
-def start_process(task_id:int):
+def cancel_task(task_id:int):
     try:
-        msg = task_service.start_process(task_id)
+        msg = task_service.cancel_run_task(task_id)
         if msg:
             return ResponseBase.error(msg)
-        return ResponseBase.success('启动处理成功')
+        return ResponseBase.success('启动采集成功')
     except Exception as e:
-        return ResponseBase.error(f'启动处理失败:{str(e)}')
+        return ResponseBase.error(f'启动采集失败:{str(e)}')
+
 
 @project_task_api.route('/start_send/<int:task_id>', methods=['POST'])
 @Permission.authorize
 def start_send(task_id:int):
     try:
-        msg = task_service.start_send(task_id)
+        msg = task_service.start_send_task(task_id)
         if msg:
             return ResponseBase.error(msg)
         return ResponseBase.success('启动发送成功')

+ 19 - 19
SourceCode/IntelligentRailwayCosting/app/services/project_quota.py

@@ -171,25 +171,25 @@ class ProjectQuotaService:
             self._logger.error(f"更新项目定额发送状态失败: {str(e)}")
             raise
 
-    def start_process(self, quota_id: int) -> Optional[str]:
-        """启动处理"""
-        quota = self.get_quota_dto(quota_id)
-        if quota:
-            self.update_process_status(quota_id, 1)
-            thread = threading.Thread(target=self._process_quota, args=(quota,))
-            thread.start()
-        else:
-            return "定额条目没有查询到"
-
-    def _process_quota(self, quota: ProjectQuotaDto):
-        try:
-           msg = executor.process_quota(quota)
-           if not msg:
-               self.start_send(quota.id)
-        except Exception as e:
-            self._logger.error(f"处理定额条目失败: {str(e)}")
-            self.update_process_status(quota.id, 3, str(e))
-            raise
+    # def start_process(self, quota_id: int) -> Optional[str]:
+    #     """启动处理"""
+    #     quota = self.get_quota_dto(quota_id)
+    #     if quota:
+    #         self.update_process_status(quota_id, 1)
+    #         thread = threading.Thread(target=self._process_quota, args=(quota,))
+    #         thread.start()
+    #     else:
+    #         return "定额条目没有查询到"
+    #
+    # def _process_quota(self, quota: ProjectQuotaDto):
+    #     try:
+    #        msg = executor.process_quota(quota)
+    #        if not msg:
+    #            self.start_send(quota.id)
+    #     except Exception as e:
+    #         self._logger.error(f"处理定额条目失败: {str(e)}")
+    #         self.update_process_status(quota.id, 3, str(e))
+    #         raise
 
     def start_send(self, quota_id: int) -> Optional[str]:
         """启动发送"""

+ 133 - 64
SourceCode/IntelligentRailwayCosting/app/services/project_task.py

@@ -2,7 +2,7 @@ from typing import Optional
 
 import tools.utils as utils, core.configs as configs, os, threading
 from core.log.log_record import LogRecordHelper
-from core.enum import OperationModule,OperationType
+from core.enum import OperationModule, OperationType, TaskStatusEnum
 from core.dtos import ProjectTaskDto
 from core.models import ProjectTaskModel
 from stores import ProjectTaskStore
@@ -12,6 +12,78 @@ class ProjectTaskService:
     def __init__(self):
         self.store = ProjectTaskStore()
         self._logger = utils.get_logger()
+        self._task_locks = {}
+        self._lock = threading.Lock()
+
+    def _get_task_lock(self, task_id: int) -> threading.Lock:
+        with self._lock:
+            if task_id not in self._task_locks:
+                self._task_locks[task_id] = threading.Lock()
+            return self._task_locks[task_id]
+
+    def start_run_task(self, task_id: int):
+        task_lock = self._get_task_lock(task_id)
+        with task_lock:
+            task = self.store.get_task_dto(task_id)
+            if not task:
+                return '没有查询到任务'
+            if not task.file_path or task.file_path.strip() == '':
+                return '没有上传文件'
+            if task.process_status == TaskStatusEnum.PROCESSING:
+                return '正在运行中'
+
+            try:
+                thread = threading.Thread(target=self._run_task, args=(task,))
+                thread.start()
+                if executor.project_is_running(task.project_id):
+                    return '项目有正在运行的任务'
+                return None
+            except Exception as e:
+                self._logger.error(f"启动任务失败: {str(e)}")
+                self.update_process_status(task_id, TaskStatusEnum.FAILURE.value, str(e))
+                raise
+
+    def _run_task(self, task: ProjectTaskDto):
+        task_lock = self._get_task_lock(task.id)
+        with task_lock:
+            try:
+                msg = executor.run_task(task)
+                if not msg:
+                    self.start_send_task(task.id)
+            except Exception as e:
+                self._logger.error(f"采集项目任务失败: {str(e)}")
+                self.update_process_status(task.id, TaskStatusEnum.FAILURE.value, str(e))
+                raise
+
+    def start_send_task(self, task_id: int):
+        task_lock = self._get_task_lock(task_id)
+        with task_lock:
+            task = self.store.get_task_dto(task_id)
+            if not task:
+                return '没有查询到任务'
+            if task.collect_status != 2:
+                return '还未采集处理完成'
+            if task.send_status == 1:
+                return '正在发送中'
+
+            try:
+                thread = threading.Thread(target=self._send_task, args=(task,))
+                thread.start()
+                return None
+            except Exception as e:
+                self._logger.error(f"启动发送任务失败: {str(e)}")
+                self.update_send_status(task_id, TaskStatusEnum.FAILURE.value, str(e))
+                raise
+
+    def _send_task(self, task: ProjectTaskDto):
+        task_lock = self._get_task_lock(task.id)
+        with task_lock:
+            try:
+                executor.send_task(task)
+            except Exception as e:
+                self._logger.error(f"发送项目任务失败: {str(e)}")
+                self.update_send_status(task.id, TaskStatusEnum.FAILURE.value, str(e))
+                raise
 
     def get_tasks_paginated(self, project_id: str, item_code: str, page: int = 1, page_size: int = 10,
                             keyword: Optional[str] = None, collect_status: Optional[int] = None,
@@ -106,12 +178,14 @@ class ProjectTaskService:
             self._logger.error(f"获取项目任务失败: {str(e)}")
             raise
 
-    def save_task(self, task_id: int, task_dto: ProjectTaskDto, files:list, delete_old: bool = False):
+    def save_task(self, task_id: int, task_dto: ProjectTaskDto, files:list):
         log_data=""
         if task_id == 0:
             task = self.store.create_task(task_dto)
         else:
             task = self.get_task_dto(task_id)
+            if task.process_status != TaskStatusEnum.NEW.value:
+                raise Exception("项目任务提交处理过,不能再修改。")
             log_data = utils.to_str(task.to_dict())
             if task is None:
                 LogRecordHelper.log_fail(OperationType.UPDATE, OperationModule.TASK,
@@ -119,12 +193,11 @@ class ProjectTaskService:
                 raise Exception("项目任务不存在")
             task_dto.id = task_id
             task = self.store.update_task(task_dto)
-
         try:
             if task:
-                paths = self._process_file_upload(task, files, delete_old)
+                paths = self._process_file_upload(task, files)
                 if paths != task.file_path:
-                    self.store.update_task_files(task.id,paths)
+                    task = self.store.update_task_files(task.id,paths)
                 if task_id == 0:
                     LogRecordHelper.log_success(OperationType.CREATE, OperationModule.TASK,
                                                 f"创建任务成功 任务:{task.task_name}", utils.to_str(task.to_dict()))
@@ -149,11 +222,12 @@ class ProjectTaskService:
                                utils.to_str(task_dto.to_dict()))
             raise e
 
-    def _process_file_upload(self, task: ProjectTaskDto, files: list, delete_old: bool) -> str:
+    def _process_file_upload(self, task: ProjectTaskDto, files: list) -> str:
         """处理文件上传流程"""
         task_dir = os.path.join(configs.app.source_path, f'upload_files/{task.get_path()}')
         os.makedirs(task_dir, exist_ok=True)
         self._logger.info(f"保存处理文件,项目ID:{task.project_id},任务ID:{task.id}")
+        delete_old = task.collect_status==0
         if delete_old:
             if os.path.exists(task_dir):
                 delete_paths = []
@@ -175,7 +249,8 @@ class ProjectTaskService:
                 if len(delete_paths) > 0:
                     LogRecordHelper.log_success(OperationType.DELETE, OperationModule.TASK,
                                           f"删除任务文件:{task.task_name}", utils.to_str(delete_paths))
-        file_paths = [] if delete_old or not task.file_path else task.file_path.split(',')
+        # file_paths = [] if delete_old or not task.file_path else task.file_path.split(',')
+        file_paths = []
         if files and len(files) > 0:
             for file in files:
                 if not file.filename:
@@ -237,11 +312,13 @@ class ProjectTaskService:
 
         Args:
             task_id: 任务ID
-
         Returns:
             bool: 删除是否成功
         """
         try:
+            task = self.store.get_task(task_id)
+            if task.process_status == TaskStatusEnum.PROCESSING.value:
+                raise Exception("任务正在进行中,请先取消任务")
             return self.store.delete_task(task_id)
         except Exception as e:
             self._logger.error(f"删除项目任务失败: {str(e)}")
@@ -298,64 +375,56 @@ class ProjectTaskService:
             self._logger.error(f"更新项目任务发送状态失败: {str(e)}")
             raise
 
-    def start_collect(self,task_id:int):
-        task = self.store.get_task_dto(task_id)
-        if task:
-            if not task.file_path or  task.file_path.strip() == '':
-                return '没有上传文件'
-            if task.collect_status == 1:
-                return  '正在采集中'
-            thread = threading.Thread(target=self._collect_task, args=(task,))
-            thread.start()
-            return None
-        else:
-            return '没有查询到任务'
-
-    def _collect_task(self,task:ProjectTaskDto):
-        try:
-            msg = executor.collect_task(task)
-            if not msg:
-                self.start_process(task.id)
-        except Exception as e:
-            self._logger.error(f"采集项目任务失败: {str(e)}")
-            raise
-
-    def start_process(self,task_id:int):
+    def cancel_run_task(self, task_id:int):
         task = self.store.get_task_dto(task_id)
         if task:
-            if task.collect_status != 2:
-                return '还未采集完成'
-            if task.process_status == 1:
-                return  '正在处理中'
-            thread = threading.Thread(target=self._process_task, args=(task,))
-            thread.start()
-            return None
+            if task.process_status == TaskStatusEnum.PROCESSING:
+                return executor.cancel_task(task)
+            else:
+                return '任务状态不正确'
         else:
             return '没有查询到任务'
 
-    def _process_task(self,task:ProjectTaskDto):
-        try:
-           msg = executor.process_task(task)
-           if not msg:
-                self.start_send(task.id)
-        except Exception as e:
-            self._logger.error(f"处理项目任务失败: {str(e)}")
-            raise
-    def start_send(self,task_id:int):
-        task = self.store.get_task_dto(task_id)
-        if task:
-            if task.process_status != 2:
-                return '还未处理完成'
-            if task.send_status == 1:
-                return  '正在发送中'
-            thread = threading.Thread(target=self._send_task, args=(task,))
-            thread.start()
-            return None
-        else:
-            return '没有查询到任务'
-    def _send_task(self,task:ProjectTaskDto):
-        try:
-            executor.send_task(task)
-        except Exception as e:
-            self._logger.error(f"发送项目任务失败: {str(e)}")
-            raise
+    # def start_collect(self,task_id:int):
+    #     task = self.store.get_task_dto(task_id)
+    #     if task:
+    #         if not task.file_path or  task.file_path.strip() == '':
+    #             return '没有上传文件'
+    #         if task.collect_status == 1:
+    #             return  '正在采集中'
+    #         thread = threading.Thread(target=self._collect_task, args=(task,))
+    #         thread.start()
+    #         return None
+    #     else:
+    #         return '没有查询到任务'
+    #
+    # def _collect_task(self,task:ProjectTaskDto):
+    #     try:
+    #         msg = executor.collect_task(task)
+    #         if not msg:
+    #             self.start_send(task.id)
+    #     except Exception as e:
+    #         self._logger.error(f"采集项目任务失败: {str(e)}")
+    #         raise
+    #
+    # def start_process(self,task_id:int):
+    #     task = self.store.get_task_dto(task_id)
+    #     if task:
+    #         if task.collect_status != 2:
+    #             return '还未采集完成'
+    #         if task.process_status == 1:
+    #             return  '正在处理中'
+    #         thread = threading.Thread(target=self._process_task, args=(task,))
+    #         thread.start()
+    #         return None
+    #     else:
+    #         return '没有查询到任务'
+    #
+    # def _process_task(self,task:ProjectTaskDto):
+    #     try:
+    #        msg = executor.process_task(task)
+    #        if not msg:
+    #             self.start_send(task.id)
+    #     except Exception as e:
+    #         self._logger.error(f"处理项目任务失败: {str(e)}")
+    #         raise

+ 11 - 0
SourceCode/IntelligentRailwayCosting/app/stores/budget.py

@@ -20,6 +20,17 @@ class BudgetStore:
                 return None
             return budgets
 
+    def get_budget_items(self, project_id: str, budget_id: int):
+        self._database=project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            if budget_id != 0:
+                budget_items = db_session.query(TotalBudgetItemModel).all()
+            else:
+                budget_items = db_session.query(TotalBudgetItemModel).filter(TotalBudgetItemModel.budget_id == budget_id).all()
+            if budget_items is None:
+                return None
+            return budget_items
+
     def get_budget_item_by_item_code(self, project_id: str, budget_id: int,item_code: str):
         self._database=project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:

+ 38 - 0
SourceCode/IntelligentRailwayCosting/app/stores/chapter.py

@@ -94,6 +94,44 @@ class ChapterStore:
             items = query.all()
             return items
 
+    def get_all_children_chapter_items(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = self._build_chapter_items_query() \
+                .filter(ChapterModel.item_code.like(f'{item_code}%')) \
+                .order_by(ChapterModel.item_code)
+            items = query.all()
+            return items
+    def get_all_parents_chapter_items(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            # 获取所有可能的父节点编码
+            parent_codes = []
+            current_code = item_code
+            
+            # 将带连字符的编码转换为纯数字格式
+            pure_code = current_code.replace('-', '')
+            
+            # 每两位截取一次获得父节点编码
+            for i in range(2, len(pure_code), 2):
+                code = pure_code[:i]
+                # 如果原始编码包含连字符,则将纯数字格式转回带连字符格式
+                if '-' in current_code:
+                    parts = [code[j:j+2] for j in range(0, len(code), 2)]
+                    parent_codes.append('-'.join(parts))
+                else:
+                    parent_codes.append(code)
+            
+            if not parent_codes:
+                return []
+            print(parent_codes)
+            # 查询所有父节点
+            query = self._build_chapter_items_query() \
+                .filter(ChapterModel.item_code.in_(parent_codes)) \
+                .order_by(ChapterModel.item_code)
+            items = query.all()
+            return items
+
     def get_all_chapter_items_not_children(self, project_id: str, item_code: str):
         self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as self.db_session:

+ 21 - 23
SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py

@@ -19,7 +19,7 @@ class QuotaInputStore:
         return self._current_user
 
     def get_quotas_paginated(
-        self,
+        self, project_id:str,
         budget_id: int,
         item_id: int,
         page: int = 1,
@@ -31,12 +31,14 @@ class QuotaInputStore:
         Args:
             page: 页码,从1开始
             page_size: 每页数量
+            project_id: 总概算序号
             budget_id: 总概算序号
             item_id: 条目序号
             keyword: 关键字
 
         Returns:
         """
+        self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:
             query = db_session.query(QuotaInputModel)
 
@@ -70,35 +72,30 @@ class QuotaInputStore:
                 'data': quotas
             }
 
-    def get_quota_by_id(self, quota_id: int) -> Optional[QuotaInputDto]:
-        """根据ID获取定额输入
-
-        Args:
-            quota_id: 定额序号
-
-        Returns:
-            QuotaInputDto or None
-        """
+    def get_quota(self, project_id:str,budget_id: int, item_id: int, quota_code: str):
+        self._database = project_id
         with db_helper.sqlserver_query_session(self._database) as db_session:
             model = db_session.query(QuotaInputModel).filter(
-                QuotaInputModel.quota_id == quota_id
+                QuotaInputModel.budget_id == budget_id,
+                QuotaInputModel.item_id == item_id,
+                QuotaInputModel.quota_code == quota_code
             ).first()
-
             if model is None:
                 return None
-
             return QuotaInputDto.from_model(model)
 
-    def create_quota(self, dto: QuotaInputDto) -> QuotaInputDto:
+    def create_quota(self, project_id:str, dto: QuotaInputDto) -> QuotaInputDto:
         """创建定额输入
 
         Args:
+            project_id
             dto: 定额输入DTO
 
         Returns:
             QuotaInputDto
         """
-        with db_helper.mysql_transaction_session(self._database) as db_session:
+        self._database = project_id
+        with db_helper.sqlserver_session(self._database) as db_session:
             model = QuotaInputModel(
                 budget_id=dto.budget_id,
                 item_id=dto.item_id,
@@ -136,7 +133,7 @@ class QuotaInputStore:
                 # mechanical_workday_salary=dto.mechanical_workday_salary,
                 # compilation_mechanical_workday_salary=dto.compilation_mechanical_workday_salary,
                 compiler=dto.compiler,
-                modify_date=datetime.now(),
+                modify_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                 # quota_consumption=dto.quota_consumption,
                 # basic_quota=dto.basic_quota,
                 # quota_comprehensive_unit_price=dto.quota_comprehensive_unit_price,
@@ -148,35 +145,36 @@ class QuotaInputStore:
 
             return QuotaInputDto.from_model(model)
 
-    def update_quota(self, dto: QuotaInputDto) -> Optional[QuotaInputDto]:
+    def update_quota(self, project_id:str, dto: QuotaInputDto) -> Optional[QuotaInputDto]:
         """更新定额输入
 
         Args:
+            project_id:
             dto: 定额输入DTO
 
         Returns:
             QuotaInputDto or None
         """
-        with db_helper.mysql_transaction_session(self._database) as db_session:
+        self._database = project_id
+        with db_helper.sqlserver_session(self._database) as db_session:
             model = db_session.query(QuotaInputModel).filter(
                 QuotaInputModel.quota_id == dto.quota_id
             ).first()
 
             if model is None:
                 return None
-
             model.budget_id = dto.budget_id
             model.item_id = dto.item_id
             model.quota_code = dto.quota_code
             # model.sequence_number = dto.sequence_number
-            model.project_name = dto.project_name
+            model.entry_name = dto.project_name
             model.unit = dto.unit
             model.project_quantity = dto.project_quantity
             # model.project_quantity_input = dto.project_quantity_input
             # model.quota_adjustment = dto.quota_adjustment
-            model.unit_price = dto.unit_price
+            # model.unit_price = dto.unit_price
             # model.compilation_unit_price = dto.compilation_unit_price
-            model.total_price = dto.total_price
+            # model.total_price = dto.total_price
             # model.compilation_total_price = dto.compilation_total_price
             # model.unit_weight = dto.unit_weight
             # model.total_weight = dto.total_weight
@@ -220,7 +218,7 @@ class QuotaInputStore:
         Returns:
             bool
         """
-        with db_helper.mysql_transaction_session(self._database) as db_session:
+        with db_helper.sqlserver_session(self._database) as db_session:
             model = db_session.query(QuotaInputModel).filter(
                 QuotaInputModel.quota_id == quota_id
             ).first()

+ 3 - 3
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/project_quota.py

@@ -62,7 +62,7 @@ class ProjectQuotaStore:
             if keyword:
                 conditions.append(or_(
                     ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
-                    ProjectQuotaModel.project_name.like(f"%{keyword}%"),
+                    ProjectQuotaModel.entry_name.like(f"%{keyword}%"),
                 ))
             query = query.filter(and_(*conditions))
 
@@ -141,7 +141,7 @@ class ProjectQuotaStore:
                 item_id=quota_dto.item_id,
                 item_code=quota_dto.item_code,
                 quota_code=quota_dto.quota_code,
-                project_name=quota_dto.project_name,
+                project_name=quota_dto.entry_name,
                 unit=quota_dto.unit,
                 project_quantity=quota_dto.project_quantity,
                 project_quantity_input=quota_dto.project_quantity_input,
@@ -180,7 +180,7 @@ class ProjectQuotaStore:
             return None
         with db_helper.mysql_session(self._database) as db_session:
             quota.quota_code = quota_dto.quota_code
-            quota.project_name = quota_dto.project_name
+            quota.entry_name = quota_dto.entry_name
             quota.unit = quota_dto.unit
             quota.project_quantity = quota_dto.project_quantity
             quota.project_quantity_input = quota_dto.project_quantity_input

+ 3 - 3
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_quota.py

@@ -63,7 +63,7 @@ class ProjectQuotaStore:
             if keyword:
                 conditions.append(or_(
                     ProjectQuotaModel.quota_code.like(f"%{keyword}%"),
-                    ProjectQuotaModel.project_name.like(f"%{keyword}%"),
+                    ProjectQuotaModel.entry_name.like(f"%{keyword}%"),
                 ))
             query = query.filter(and_(*conditions))
 
@@ -142,7 +142,7 @@ class ProjectQuotaStore:
                 item_id=quota_dto.item_id,
                 item_code=quota_dto.item_code,
                 quota_code=quota_dto.quota_code,
-                project_name=quota_dto.project_name,
+                project_name=quota_dto.entry_name,
                 unit=quota_dto.unit,
                 project_quantity=quota_dto.project_quantity,
                 project_quantity_input=quota_dto.project_quantity_input,
@@ -181,7 +181,7 @@ class ProjectQuotaStore:
             return None
         with db_helper.sqlserver_session(self._database) as db_session:
             quota.quota_code = quota_dto.quota_code
-            quota.project_name = quota_dto.project_name
+            quota.entry_name = quota_dto.entry_name
             quota.unit = quota_dto.unit
             quota.project_quantity = quota_dto.project_quantity
             quota.project_quantity_input = quota_dto.project_quantity_input

+ 32 - 1
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py

@@ -4,6 +4,7 @@ from typing import Optional
 
 import tools.db_helper as db_helper
 from core.dtos import ProjectTaskDto
+from core.enum import TaskStatusEnum
 from core.models import ProjectTaskModel
 from core.user_session import UserSession
 
@@ -163,6 +164,25 @@ class ProjectTaskStore:
         task_dto = self.get_task(task_id)
         return ProjectTaskDto.from_model(task_dto) if task_dto else None
 
+    def get_wait_tasks(self,project_id:str=None):
+        """查询待处理的任务
+
+        Args:
+            project_id: 项目编号
+
+        Returns:
+
+        """
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            query = db_session.query(ProjectTaskModel)
+            query = query.filter(
+                and_(
+                    ProjectTaskModel.is_del == 0,
+                    ProjectTaskModel.process_status == TaskStatusEnum.PROCESSING.value))
+            if project_id:
+                query = query.filter(ProjectTaskModel.project_id == project_id)
+            tasks = query.all()
+            return [ProjectTaskDto.from_model(task) for task in tasks]
     def create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
         """创建任务
 
@@ -205,8 +225,8 @@ class ProjectTaskStore:
         with db_helper.sqlserver_session(self._database) as db_session:
             task.task_name = task_dto.task_name
             task.task_desc = task_dto.task_desc
+            task.budget_id = task_dto.budget_id
             # task.project_id = task_dto.project_id
-            # task.budget_id = task_dto.budget_id
             # task.item_id = task_dto.item_id
             # task.item_code = task_dto.item_code
             # task.file_path = task_dto.file_path
@@ -251,6 +271,17 @@ class ProjectTaskStore:
             task.deleted_at = datetime.now()
             db_session.merge(task)
             return True
+    def update_task_status(self,task_id:int, status:int, err:str = None):
+        task = self.get_task(task_id)
+        if not task:
+            return False
+        with db_helper.sqlserver_session(self._database) as db_session:
+            task.process_status = status
+            if err:
+                task.process_error = err
+            task.process_time = datetime.now()
+            db_session.merge(task)
+            return True
 
     def update_collect_status(self,task_id:int, status:int, err:str = None):
         task = self.get_task(task_id)

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/test/sqlserver_test.py

@@ -120,7 +120,7 @@ class TestSQLServerHelper(unittest.TestCase):
             with self.db_helper.session_scope(self.database) as session:
                 result = session.query(TestTable).filter_by(name="test_commit").first()
                 self.assertIsNotNone(result, "事务提交失败")
-                self.assertEqual(result.name, "test_commit")
+                self.assertEqual(result.entry_name, "test_commit")
 
         except Exception as e:
             self.fail(f"会话管理测试失败: {str(e)}")

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/views/login.py

@@ -12,4 +12,4 @@ def login():
     #     return redirect(url_for('project.index'))
     logout_user()
     UserSession.clear_user()
-    return render_template('account/login.html',app_name=config.app.name)
+    return render_template('account/login.html', app_name=config.app.entry_name)

+ 4 - 1
SourceCode/IntelligentRailwayCosting/app/views/project.py

@@ -1,9 +1,11 @@
 from flask import Blueprint, render_template
 from core.user_session import Permission
+from stores import BudgetStore
 from stores.project import ProjectStore
 
 project_bp = Blueprint('project', __name__, template_folder='templates')
 project_store = ProjectStore()
+budget_store = BudgetStore()
 @project_bp.route('/', methods=['GET'])
 @Permission.authorize
 def index():
@@ -13,4 +15,5 @@ def index():
 @Permission.authorize
 def budget_info(project_id:str):
     project = project_store.get(project_id)
-    return render_template('project/budget_info.html',page_active='project',project=project)
+    budgets = budget_store.get_budget_info(project_id)
+    return render_template('project/budget_info.html',page_active='project',project=project,budgets=budgets)

+ 98 - 79
SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js

@@ -32,14 +32,14 @@ const nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 									<form class="search-box d-flex">
 										<div class="d-flex">
 											{2}
-											<select class="form-select form-select-sm me-5" name="process_status">
-												<option value="">全部处理状态</option>
-												<option value="0">未处理</option>
-												<option value="1">处理中</option>
-												<option value="2">已处理</option>
-												<option value="3">处理失败</option>
-												<option value="4">数据变更</option>
-											</select>
+<!--											<select class="form-select form-select-sm me-5" name="process_status">-->
+<!--												<option value="">全部处理状态</option>-->
+<!--												<option value="0">未处理</option>-->
+<!--												<option value="1">处理中</option>-->
+<!--												<option value="2">已处理</option>-->
+<!--												<option value="3">处理失败</option>-->
+<!--												<option value="4">数据变更</option>-->
+<!--											</select>-->
 											<select class="form-select form-select-sm me-5" name="send_status">
 												<option value="">全部发送状态</option>
 												<option value="0">未发送</option>
@@ -249,6 +249,20 @@ function RenderRightBox(data){
 						data: 'task_name',
 						width: '240px',
 					},
+					{
+						title: '任务排序',
+						data: 'sort',
+						width: '100',
+					},
+					{
+						title: '概算单元',
+						data: 'budget_id',
+						width: '130px',
+						render: (row) => {
+							const budget_name = $modal.find(`[name="budget_id"] option[value='${row.budget_id}']`)
+							return budget_name.length ? budget_name.text() : '-'
+						}
+					},
 					{
 						title: '文件数据',
 						data: 'file_data',
@@ -272,7 +286,7 @@ function RenderRightBox(data){
 					{
 						title: '任务状态',
 						data: 'task_status',
-						width: '220px',
+						width: '180px',
 						render: (row) => {
 							let str = ``
 							if (row.collect_status === 0) {
@@ -281,24 +295,25 @@ function RenderRightBox(data){
 								str += `<span class="badge badge-light-warning">采集中</span>`
 							} else if (row.collect_status === 2){
 								str += `<span class="badge badge-light-success">采集完成</span>`
-								if (row.process_status === 0) {
-									str += `<span class="badge badge-light-primary ms-3">未处理</span>`
-								} else if (row.process_status === 1){
-									str += `<span class="badge badge-light-warning ms-3">处理中</span>`
-								} else if (row.process_status === 2){
-									str += `<span class="badge badge-light-success ms-3">处理完成</span>`
-									if (row.send_status === 0) {
-										str += `<span class="badge badge-light-primary ms-3">未发送</span>`
-									} else if (row.send_status === 1){
-										str += `<span class="badge badge-light-warning ms-3">发送中</span>`
-									} else if (row.send_status === 2){
-										str += `<span class="badge badge-light-success ms-3">发送完成</span>`
-									} else if (row.send_status === 3){
-										str += `<span class="badge badge-light-danger ms-3">发送失败</span>`
-									}
-								} else if (row.process_status === 3){
-									str += `<span class="badge badge-light-danger ms-3">处理失败</span>`
+								if (row.send_status === 0) {
+									str += `<span class="badge badge-light-primary ms-3">未发送</span>`
+								} else if (row.send_status === 1){
+									str += `<span class="badge badge-light-warning ms-3">发送中</span>`
+								} else if (row.send_status === 2){
+									str += `<span class="badge badge-light-success ms-3">发送完成</span>`
+								} else if (row.send_status === 3){
+									str += `<span class="badge badge-light-danger ms-3">发送失败</span>`
 								}
+								// if (row.process_status === 0) {
+								// 	str += `<span class="badge badge-light-primary ms-3">未处理</span>`
+								// } else if (row.process_status === 1){
+								// 	str += `<span class="badge badge-light-warning ms-3">处理中</span>`
+								// } else if (row.process_status === 2){
+								// 	str += `<span class="badge badge-light-success ms-3">处理完成</span>`
+								//
+								// } else if (row.process_status === 3){
+								// 	str += `<span class="badge badge-light-danger ms-3">处理失败</span>`
+								// }
 							} else if (row.collect_status === 3){
 								str += `<span class="badge badge-light-danger">采集失败</span>`
 							} else if (row.collect_status === 4){
@@ -310,30 +325,31 @@ function RenderRightBox(data){
 					{
 						title: '操作',
 						data: 'id',
-						width: '200px',
+						width: '160px',
 						render: (row) => {
 							let str = ``
 							if (row.collect_status === 0) {
 								str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始任务" onclick="StartCollectTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
 							} else if (row.collect_status === 2) {
 								str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新采集" onclick="ReStartCollectTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-								if (row.process_status === 0) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-								} else if (row.process_status === 2) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-									if (row.send_status === 0) {
-										str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-									} else if (row.send_status === 2) {
-										str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-									} else if (row.send_status === 3) {
-										str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-									} else if (row.send_status === 4) {
-										str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-									}
-								} else if (row.process_status === 3) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-								} else if (row.process_status === 4) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+								// if (row.process_status === 0) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+								// } else if (row.process_status === 2) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+								//
+								// } else if (row.process_status === 3) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+								// } else if (row.process_status === 4) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+								// }
+								if (row.send_status === 0) {
+									str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+								} else if (row.send_status === 2) {
+									str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+								} else if (row.send_status === 3) {
+									str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+								} else if (row.send_status === 4) {
+									str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
 								}
 							} else if (row.collect_status === 3) {
 								str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新采集" onclick="ReStartCollectTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
@@ -452,20 +468,20 @@ function RenderQuotaTable(budget_id,data){
 			{
 				title: '状态',
 				data: 'status',
-				width: '150px',
+				width: '100px',
 				render: (row) => {
 					let str=''
-					if(row.process_status === 0){
-						str+= `<span class="badge badge-primary">未处理</span>`
-					}else if (row.process_status === 1){
-						str+= `<span class="badge badge-warning">处理中</span>`
-					}else if (row.process_status === 2){
-						str+= `<span class="badge badge-success">已处理</span>`
-					}else if (row.process_status === 3){
-						str+= `<span class="badge badge-danger">处理失败</span>`
-					}else if (row.process_status === 4){
-						str+= `<span class="badge badge-danger">数据变更</span>`
-					}
+					// if(row.process_status === 0){
+					// 	str+= `<span class="badge badge-primary">未处理</span>`
+					// }else if (row.process_status === 1){
+					// 	str+= `<span class="badge badge-warning">处理中</span>`
+					// }else if (row.process_status === 2){
+					// 	str+= `<span class="badge badge-success">已处理</span>`
+					// }else if (row.process_status === 3){
+					// 	str+= `<span class="badge badge-danger">处理失败</span>`
+					// }else if (row.process_status === 4){
+					// 	str+= `<span class="badge badge-danger">数据变更</span>`
+					// }
 					if(row.send_status === 0){
 						str+= `<span class="badge badge-primary ms-3">未发送</span>`
 					}else if (row.send_status === 1){
@@ -484,26 +500,27 @@ function RenderQuotaTable(budget_id,data){
 			{
 				title: '操作',
 				data: 'id',
-				width: '160px',
+				width: '120px',
 				render: (row) => {
 					let str=''
-					if (row.process_status === 0) {
-						str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-					} else if (row.process_status === 2) {
-						str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-						if (row.send_status === 0) {
-							str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-						} else if (row.send_status === 2) {
-							str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-						} else if (row.send_status === 3) {
-							str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-						} else if (row.send_status === 4) {
-							str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-						}
-					} else if (row.process_status === 3) {
-						str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
-					} else if (row.process_status === 4) {
-						str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+					// if (row.process_status === 0) {
+					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+					// } else if (row.process_status === 2) {
+					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+					//
+					// } else if (row.process_status === 3) {
+					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+					// } else if (row.process_status === 4) {
+					// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
+					// }
+					if (row.send_status === 0) {
+						str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+					} else if (row.send_status === 2) {
+						str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+					} else if (row.send_status === 3) {
+						str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+					} else if (row.send_status === 4) {
+						str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
 					}
 					str+=`<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit_Quota(${row.id}, ${budget_id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
 					str+=`<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete_Quota(${row.id}, ${budget_id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
@@ -525,14 +542,14 @@ function Add() {
 	_fileUploadDropzone.removeAllFiles()
 	AddModal($modal, () => {
 		$modal.find('[name="task_id"]').val('0');
+		$modal.find('[name="budget_id"]').val('0');
 		$modal.find('#delete_file_box').hide();
-		$modal.find('[name="delete_file"]').prop('checked',false)
+		// $modal.find('[name="delete_file"]').prop('checked',false)
 		SetBudgetData($modal)
 	})
 }
 
 function Edit(id) {
-
 	_fileUploadDropzone.removeAllFiles()
     EditModal($modal,()=>{
         IwbAjax_1({
@@ -543,7 +560,7 @@ function Edit(id) {
 					return
 				}
 				const data = res.data
-				$modal.find('#delete_file_box').show();
+				// $modal.find('#delete_file_box').show();
 				// SetBudgetData(budget_id)
         		$modal.find('[name="task_id"]').val(data.id);
 				$modal.find('[name="budget_id"]').val(data.budget_id);
@@ -552,7 +569,7 @@ function Edit(id) {
 				$modal.find('[name="item_code"]').val(data.item_code);
                 $modal.find('[name="task_name"]').val(data.task_name);
                 $modal.find('[name="task_desc"]').val(data.task_desc);
-				$modal.find('[name="delete_file"]').prop('checked',false)
+				// $modal.find('[name="delete_file"]').prop('checked',false)
             }
         })
     })
@@ -567,8 +584,9 @@ function SaveProject() {
 		project_id = $modal.find('[name="project_id"]').val(),
 		task_id=  $modal.find('[name="task_id"]').val(),
 		task_name = $modal.find('[name="task_name"]').val(),
+		task_sort = $modal.find('[name="task_sort"]').val(),
 		task_desc = $modal.find('[name="task_desc"]').val(),
-		delete_file = $modal.find('[name="delete_file"]').prop('checked')? 'true':'false',
+		// delete_file = $modal.find('[name="delete_file"]').prop('checked')? 'true':'false',
 		files = _fileUploadDropzone.getAcceptedFiles();
 	// console.log("FILES",files)
 
@@ -583,6 +601,7 @@ function SaveProject() {
 	formData.append('project_id', project_id)
 	formData.append('task_id', task_id)
 	formData.append('task_name', task_name)
+	formData.append('task_sort', task_sort)
 	formData.append('task_desc', task_desc)
 	formData.append('delete_old', delete_file)
 

+ 10 - 1
SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html

@@ -26,7 +26,7 @@
 			<div class="modal-body">
 				<form>
 					<div class="form-group">
-						<input type="hidden" name="budget_id" value="">
+<!--						<input type="hidden" name="budget_id" value="">-->
 						<input type="hidden" name="project_id" value="">
 						<input type="hidden" name="item_id" value="">
 						<input type="hidden" name="item_code" value="">
@@ -35,6 +35,15 @@
 							<label for="task_name" class="form-label required">任务名称</label>
 							<input type="text" class="form-control" name="task_name" id="task_name" placeholder="请输入" required />
 						</div>
+						<div class="fv-row form-group mb-3">
+							<label for="budget_id" class="form-label">概算单元</label>
+							<select class="form-select" name="budget_id" id="budget_id">
+								<option value="0">全部概算单元</option>
+								{% for budget in budgets %}
+								<option value="{{ budget.budget_id }}">{{ budget.budget_code }}</option>
+								{% endfor %}
+							</select>
+						</div>
 						<div class="fv-row form-group mb-3">
 							<label for="task_desc" class="form-label">任务详情</label>
 							<textarea type="text" class="form-control" name="task_desc" id="task_desc" placeholder="请输入"></textarea>