ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python资产管理系统:支持折旧计算、调拨联动与审计导出

Python资产管理系统:支持折旧计算、调拨联动与审计导出

简介:本资源是一套基于Python开发的轻量级资产管理系统完整源码,面向Python初学者、Web开发入门者及企业IT资产管理员,解决组织内部资产全生命周期(购置、使用、维护、报废)的数字化跟踪与管理问题。压缩包共21个文件,含2个核心Python后端脚本(实现业务逻辑与数据库交互)、6个Word文档(含系统设计说明、部署指南与用户手册)、3个PDF技术文档(涵盖架构图与API说明)、2个Excel资产模板(用于批量导入导出),以及JPG界面截图、TXT配置示例等,整体6.88MB,结构清晰、开箱即用。已有441人学习下载,读者可直接运行调试Flask/Django风格的Web服务,深入理解SQLAlchemy ORM建模、用户权限控制、RESTful接口设计及Jinja2模板渲染等关键实践环节,同时获得可二次开发的工程化项目骨架与规范化的文档配套。

1. 这不是个“增删改查”Demo:一个能真正在中小团队跑通资产台账、折旧计算、责任人联动和导出审计的Python系统

你手头那个用Flask搭了三个路由、前端套了个Bootstrap表格、数据库只存了“名称/型号/数量”的“资产管理系统”,大概率撑不过财务年审前的交叉核验。真实场景里,一台2021年采购的戴尔笔记本,要自动按直线法计提月折旧(残值率5%,使用年限36个月),在调拨给新部门时同步更新责任人、领用日期、存放位置,并触发邮件通知原负责人;当某台设备报废时,系统得生成带唯一编号的《固定资产处置单》,PDF可打印、字段可追溯、操作留痕不可删——这些不是功能点列表,而是财务合规与IT运维的硬性交界线。本项目源码包(assetsystem_v2.3.zip)正是为这类需求而生:它不依赖Docker或K8s,纯Python 3.9+环境即可启动;核心逻辑全部封装在core/目录下,含折旧引擎、状态机驱动、Excel模板渲染器;所有数据库操作经由SQLModel抽象,支持SQLite开箱即用、PostgreSQL平滑迁移。适合刚接手IT资产管理模块的开发同学、需要快速交付内部工具的运维工程师,以及想把纸质台账数字化但又不愿买SaaS服务的行政主管。它解决的不是“能不能显示数据”,而是“财务来查账时,你敢不敢把数据库直连给他们看”。


2. 从解压到可运行:本地最小化部署四步走

2.1 解压与环境隔离:为什么必须用venv而不是直接pip install

不要跳过这一步。我见过太多人直接pip install -r requirements.txt后,因全局环境中已存在旧版Pydantic(<2.0)导致SQLModel初始化失败,报错信息却指向core/models.py第47行的Field(default_factory=...)——实际是Pydantic v1不支持该语法。正确做法是:

# 创建独立虚拟环境(Python 3.9+) python -m venv venv_assets source venv_assets/bin/activate # Linux/macOS # venv_assets\Scripts\activate.bat # Windows # 升级pip并安装依赖(注意:requirements.txt中已锁定版本) pip install --upgrade pip pip install -r requirements.txt

提示:requirements.txt中关键依赖已严格锁定:sqlmodel==0.0.16(适配Pydantic v2)、openpyxl==3.1.2(避免Excel公式解析异常)、weasyprint==62.2(PDF生成稳定版)。若手动升级其中任一包,极可能触发连锁兼容问题。

2.2 初始化数据库:SQLite路径、表结构与初始数据注入

系统默认使用SQLite,数据库文件路径由环境变量ASSET_DB_PATH控制。首次运行前需执行初始化脚本,它会创建6张核心表(asset,department,employee,depreciation_record,maintenance_log,disposal_record)并注入3条测试资产、2个部门、4名员工:

# 设置数据库路径(建议放在项目根目录下,便于备份) export ASSET_DB_PATH=./data/assets.db # 执行初始化(会自动创建data/目录及assets.db文件) python scripts/init_db.py # 验证表是否创建成功 sqlite3 ./data/assets.db ".tables" # 应输出:asset department depreciation_record disposal_record employee maintenance_log

init_db.py的核心逻辑是调用core/db_init.py中的init_database()函数,该函数不仅建表,还预置了折旧策略枚举(StraightLine,DecliningBalance)、资产状态机(InStock,InUse,UnderRepair,Disposed)及部门树形结构父ID约束。关键参数说明:--sample-data开关决定是否注入测试数据(生产环境部署时务必关闭);--force-recreate会删除现有db文件重来(仅调试用)。

2.3 启动Web服务:Flask开发服务器配置要点

系统使用Flask作为Web框架,但未采用默认的flask run命令,而是通过app.py入口统一管理配置。启动前需确认两个环境变量:

# 必须设置:指定运行模式(dev/test/prod) export FLASK_ENV=dev # 必须设置:Secret Key用于session加密(生产环境请换为32位随机字符串) export SECRET_KEY="dev-secret-key-change-in-prod" # 启动服务(默认监听127.0.0.1:5000) python app.py

此时访问http://127.0.0.1:5000将看到登录页。默认账号密码为admin / admin123(首次登录后强制修改)。注意:app.py中create_app()函数会根据FLASK_ENV加载不同配置类——DevConfig启用调试模式、SQLAlchemy echo日志;ProdConfig则关闭调试、启用Werkzeug超时保护、强制HTTPS重定向(需Nginx前置)。若需修改端口,在app.py第28行调整app.run(port=5000)即可。

2.4 登录与首屏验证:检查核心功能链路是否打通

登录后首页(/dashboard)会展示4个关键指标卡片:

  • 在用资产总数(SELECT COUNT(*) FROM asset WHERE status = 'InUse')
  • 本月折旧总额(SELECT SUM(monthly_depreciation) FROM depreciation_record WHERE month = '2024-06')
  • 待处理维修单(SELECT COUNT(*) FROM maintenance_log WHERE status = 'Pending')
  • 近30天调拨记录(SELECT COUNT(*) FROM asset_history WHERE action = 'Transfer' AND created_at > datetime('now', '-30 days'))

点击任一卡片应跳转至对应列表页,且列表页URL含查询参数(如/assets?status=InUse)。若卡片数字为0但数据库中有测试数据,请立即检查core/services/dashboard_service.py中get_dashboard_stats()函数的SQL查询条件——常见错误是status字段值写成'in_use'(小写)而非枚举定义的'InUse'(驼峰)。


3. 资产全生命周期操作:从录入到报废的代码级实现逻辑

3.1 录入新资产:表单验证、折旧策略绑定与自动生成编码

新增资产页面(/assets/new)提交后,请求由routes/asset_routes.py中的create_asset()函数处理。其核心流程分三步:

  1. 表单解析与业务校验:
    使用Pydantic模型AssetCreateSchema进行强类型校验,关键约束包括:

    • purchase_date不能晚于当前日期(@field_validator('purchase_date'))
    • unit_price必须大于0(ge=0.01)
    • depreciation_method必须是枚举值(StraightLine或DecliningBalance)
  2. 资产编码生成:
    编码规则为ASSET-{YYYY}{MM}-{3位序列号}(如ASSET-202406-001)。序列号从asset_sequence表获取并原子递增:

    # core/services/asset_service.py 第89行 def generate_asset_code() -> str: with Session(engine) as session: stmt = text("UPDATE asset_sequence SET counter = counter + 1 RETURNING counter") result = session.execute(stmt) seq_num = result.scalar_one() return f"ASSET-{datetime.now().strftime('%Y%m')}-{seq_num:03d}"
  3. 折旧计划预计算:
    根据depreciation_method、useful_life_months、residual_value_rate,调用core/calculators/depreciation_calculator.py生成未来36个月的折旧记录(存入depreciation_record表),避免每次查询实时计算。

参数说明:residual_value_rate默认0.05(5%),可在AssetCreateSchema中覆盖;useful_life_months若为空,则按资产类别(category字段)查core/config/asset_category_rules.json映射表自动填充(如"Laptop"→36个月)。

3.2 资产调拨:状态变更、历史记录与责任人联动

调拨操作(/assets/{id}/transfer)是状态机驱动的关键场景。AssetTransferSchema要求必填to_department_id和to_employee_id,后端执行:

# core/services/asset_service.py transfer_asset() 函数节选 def transfer_asset(asset_id: int, to_dept_id: int, to_emp_id: int): with Session(engine) as session: # 1. 查询原资产记录 asset = session.get(Asset, asset_id) if not asset: raise HTTPException(status_code=404, detail="Asset not found") # 2. 状态机校验:仅允许从 InUse/UnderRepair 调拨 if asset.status not in ["InUse", "UnderRepair"]: raise HTTPException(status_code=400, detail=f"Cannot transfer asset in status {asset.status}") # 3. 更新资产主表 asset.department_id = to_dept_id asset.employee_id = to_emp_id asset.status = "InUse" # 调拨后默认为在用 asset.last_transfer_date = datetime.now() # 4. 写入历史记录表(asset_history) history = AssetHistory( asset_id=asset_id, action="Transfer", from_department_id=asset.department_id, # 注意:此处是调拨前的旧值 from_employee_id=asset.employee_id, to_department_id=to_dept_id, to_employee_id=to_emp_id, notes="Auto-generated by system" ) session.add(history) session.commit()

关键设计:asset_history表不存储冗余字段,所有关联ID均引用主表外键,确保数据一致性。调拨成功后,系统自动向to_employee_id发送站内信(非邮件),内容为“您已接收资产【{asset.code}】,请于3个工作日内确认”。

3.3 报废处置:多级审批流模拟与PDF单据生成

报废流程(/assets/{id}/dispose)需模拟三级审批:申请人提交 → 部门负责人审批 → 财务部终审。源码中通过disposal_status字段(Draft/ApprovedByDept/ApprovedByFinance/Rejected)实现状态流转。PDF单据生成由core/reports/disposal_report.py完成:

# core/reports/disposal_report.py 第42行 def generate_disposal_pdf(disposal_id: int) -> bytes: with Session(engine) as session: disposal = session.get(DisposalRecord, disposal_id) # 1. 查询关联资产、申请人、审批人信息 asset = session.get(Asset, disposal.asset_id) applicant = session.get(Employee, disposal.applicant_id) dept_approver = session.get(Employee, disposal.dept_approver_id) if disposal.dept_approver_id else None # 2. 渲染HTML模板(templates/reports/disposal.html) html_content = render_template( "reports/disposal.html", disposal=disposal, asset=asset, applicant=applicant, dept_approver=dept_approver, # 生成唯一防伪码(基于disposal_id+timestamp+secret_key的HMAC) anti_fraud_code=generate_anti_fraud_code(disposal_id) ) # 3. 转PDF(WeasyPrint) pdf_bytes = HTML(string=html_content).write_pdf() return pdf_bytes

anti_fraud_code使用hmac.new()生成,确保每份PDF单据具备可验证性。财务人员可通过系统内“单据验真”功能输入防伪码反查原始记录。


4. 避坑指南:生产环境部署必踩的5个深坑与血泪解法

4.1 现象:Excel导入后中文字段全变成乱码(如“戴尔”→“戴尔)

原因:openpyxl读取.xlsx文件时默认使用utf-8编码,但Windows用户用Excel另存的文件常为gbk编码,且openpyxl不提供编码指定参数。
解决:在core/importers/excel_importer.py中,改用pandas.read_excel()替代原生openpyxl.load_workbook(),并显式指定engine='openpyxl'和encoding='gbk'(自动检测编码的方案见下文补丁):

# 替换原load_workbook逻辑 df = pd.read_excel(file_path, engine='openpyxl', dtype=str) # 若仍乱码,加编码探测(需安装chardet) import chardet with open(file_path, 'rb') as f: raw_data = f.read(10000) encoding = chardet.detect(raw_data)['encoding'] or 'utf-8' df = pd.read_excel(file_path, engine='openpyxl', encoding=encoding)

4.2 现象:定时任务cron_job.py在Linux后台运行时,折旧计算结果为空

原因:cron环境变量缺失ASSET_DB_PATH,且python路径未用绝对路径,导致找不到数据库文件。
解决:在crontab中写绝对路径并显式导出变量:

# 编辑crontab(crontab -e) 0 2 * * * cd /opt/assetsystem && /opt/assetsystem/venv_assets/bin/python scripts/cron_job.py >> /var/log/assetsystem_cron.log 2>&1 # 或在cron_job.py开头强制设置 import os os.environ.setdefault("ASSET_DB_PATH", "/opt/assetsystem/data/assets.db")

4.3 现象:PDF导出的中文不显示(方块或空白)

原因:WeasyPrint默认字体不支持中文,需指定中文字体路径。
解决:在core/reports/base_report.py中注册字体:

from weasyprint import CSS from weasyprint.fonts import FontConfiguration font_config = FontConfiguration() css = CSS(string=''' @font-face { font-family: 'SimSun'; src: url('file:///usr/share/fonts/truetype/simsun.ttc'); } body { font-family: 'SimSun', sans-serif; } ''', font_config=font_config) # 生成PDF时传入css参数 HTML(string=html_content).write_pdf(stylesheets=[css], font_config=font_config)

注意:Linux服务器需提前安装simsun.ttc字体(sudo apt-get install fonts-wqy-zenhei),并确认路径正确。

4.4 现象:多人同时提交资产报废,出现重复审批单或状态错乱

原因:disposal_status更新未加数据库行锁,导致并发更新丢失。
解决:在core/services/disposal_service.py的update_disposal_status()中,使用SELECT ... FOR UPDATE:

# 修改前(危险) stmt = update(DisposalRecord).where(DisposalRecord.id == disposal_id).values(status=new_status) # 修改后(安全) with Session(engine) as session: # 先锁定记录 disposal = session.execute( select(DisposalRecord).where(DisposalRecord.id == disposal_id).with_for_update() ).scalar_one() disposal.status = new_status session.commit()

4.5 现象:搜索框输入“笔记本”返回空结果,但数据库明确存在

原因:全文检索使用SQLite的LIKE '%笔记本%',但未对搜索词做%包裹,且未处理前后空格。
解决:在core/search/asset_search.py中,修正build_search_condition()函数:

def build_search_condition(keyword: str) -> BinaryExpression: keyword = keyword.strip() # 去除空格 if not keyword: return true() # 返回True条件,避免空搜索报错 # 使用ILIKE(SQLite不支持,改用LOWER + LIKE) return or_( func.lower(Asset.name).like(f"%{keyword.lower()}%"), func.lower(Asset.model).like(f"%{keyword.lower()}%"), Asset.code.like(f"%{keyword}%") # 编码通常为英文数字,不转小写 )

5. 审计就绪:让财务和内审人员愿意点开你的数据库

5.1 导出符合《企业会计准则第4号——固定资产》的折旧明细表

财务最关心的不是界面有多炫,而是“这个月计提的12,345.67元,到底怎么算出来的”。系统提供/reports/depreciation_detail?month=2024-06接口,返回标准CSV格式,字段严格对应准则要求:

资产编码资产名称入账日期原值累计折旧本月折旧折旧方法使用年限(月)残值率期末账面价值
ASSET-202401-001戴尔XPS132024-01-158999.001249.86249.97直线法360.057749.14

生成逻辑在core/reports/depreciation_report.py中,关键点:

  • 期末账面价值 = 原值 - 累计折旧(非实时计算,取depreciation_record表中截至当月的SUM)
  • 累计折旧字段为子查询结果,确保与财务系统一致
  • CSV使用utf-8-sig编码(BOM头),保障Excel双击打开不乱码

实操技巧:将此URL加入Nginx反向代理,配置/finance/export/路径,财务人员直接浏览器访问下载,无需登录系统。

5.2 数据库审计视图:用视图固化“谁在何时干了什么”

内审人员需要快速定位操作痕迹,但asset_history表字段分散。系统预建了审计视图v_audit_asset_actions:

-- 在init_db.py中执行 CREATE VIEW v_audit_asset_actions AS SELECT h.id AS history_id, h.action, h.notes, h.created_at, a.code AS asset_code, a.name AS asset_name, e1.name AS operator_name, e2.name AS target_employee_name, d1.name AS from_department, d2.name AS to_department FROM asset_history h LEFT JOIN asset a ON h.asset_id = a.id LEFT JOIN employee e1 ON h.operator_id = e1.id LEFT JOIN employee e2 ON h.to_employee_id = e2.id LEFT JOIN department d1 ON h.from_department_id = d1.id LEFT JOIN department d2 ON h.to_department_id = d2.id;

审计时只需执行:

SELECT * FROM v_audit_asset_actions WHERE created_at >= '2024-06-01' AND action IN ('Transfer', 'Dispose') ORDER BY created_at DESC LIMIT 100;

视图将所有关联表JOIN固化,避免内审人员写错JOIN条件导致漏查。

5.3 敏感操作二次确认与操作留痕强化

财务最反感“误点删除”。系统对DELETE类操作强制二次确认:

  • 前端:/assets/{id}/delete按钮点击后,弹出Modal要求输入当前登录用户名(非密码)
  • 后端:routes/asset_routes.py中delete_asset()函数校验request.form['confirm_username'] == current_user.username
  • 留痕:无论删除成功与否,均写入audit_log表,字段含ip_address(request.client.host)、user_agent、operation_result(success/failed)

我的习惯:每周五下班前,我会手动执行一次SELECT COUNT(*) FROM audit_log WHERE operation_type = 'DELETE' AND DATE(created_at) = CURDATE();,如果结果非0,立刻查日志确认是否为误操作——这比等财务打电话来问强十倍。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进