Race-Inventorytools 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include requirements.txt
3
+ recursive-include src/flask_crud_competition/templates *.html
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: Race-Inventorytools
3
+ Version: 0.1.0
4
+ Summary: A Flask-Admin inventory CRUD demo application.
5
+ Author: Competition Demo Team
6
+ Keywords: flask,crud,inventory,admin,sqlite
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Framework :: Flask
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: Flask==3.0.0
18
+ Requires-Dist: Flask-SQLAlchemy==3.1.1
19
+ Requires-Dist: Flask-Admin==1.6.1
20
+ Requires-Dist: WTForms==3.1.2
21
+ Requires-Dist: SQLAlchemy>=2.0
22
+ Requires-Dist: Flask-Babel==4.0.0
23
+
24
+ # Race Inventory CRUD Demo
25
+
26
+ 这是一个用于演示的 Flask-Admin 库存 CRUD 应用,包含产品管理和出入库流水管理。
27
+
28
+ ## 安装运行
29
+
30
+ ```powershell
31
+ python -m pip install Race-Inventorytools
32
+ race-inventory-admin
33
+ ```
34
+
35
+ 默认访问地址:
36
+
37
+ ```text
38
+ http://127.0.0.1:5000/admin
39
+ ```
40
+
41
+ 可选环境变量:
42
+
43
+ ```powershell
44
+ $env:FLASK_RUN_HOST = "127.0.0.1"
45
+ $env:FLASK_RUN_PORT = "5000"
46
+ $env:FLASK_DEBUG = "1"
47
+ $env:RACE_DEMO_SECRET_KEY = "replace-with-your-secret"
48
+ $env:RACE_INVENTORY_DB = "D:\path\to\data.db"
49
+ ```
50
+
51
+ ## 本地打包
52
+
53
+ ```powershell
54
+ python -m pip install --upgrade build twine
55
+ python -m build
56
+ python -m twine check dist/*
57
+ ```
58
+
59
+ 构建产物会生成在 `dist/` 目录。
60
+
61
+ ## 准备离线依赖
62
+
63
+ ```powershell
64
+ .\scripts\download_dependencies.ps1
65
+ ```
66
+
67
+ 脚本会把依赖包下载到 `wheelhouse/`,并在已经构建 `dist/` 的情况下把本项目 wheel 一起复制进去。比赛方可用下面命令离线安装:
68
+
69
+ ```powershell
70
+ python -m pip install --no-index --find-links wheelhouse Race-Inventorytools
71
+ ```
72
+
73
+ ## 上传 PyPI
74
+
75
+ ```powershell
76
+ python -m twine upload dist/*
77
+ ```
78
+
79
+ 建议先上传 TestPyPI 验证:
80
+
81
+ ```powershell
82
+ python -m twine upload --repository testpypi dist/*
83
+ ```
@@ -0,0 +1,60 @@
1
+ # Race Inventory CRUD Demo
2
+
3
+ 这是一个用于演示的 Flask-Admin 库存 CRUD 应用,包含产品管理和出入库流水管理。
4
+
5
+ ## 安装运行
6
+
7
+ ```powershell
8
+ python -m pip install Race-Inventorytools
9
+ race-inventory-admin
10
+ ```
11
+
12
+ 默认访问地址:
13
+
14
+ ```text
15
+ http://127.0.0.1:5000/admin
16
+ ```
17
+
18
+ 可选环境变量:
19
+
20
+ ```powershell
21
+ $env:FLASK_RUN_HOST = "127.0.0.1"
22
+ $env:FLASK_RUN_PORT = "5000"
23
+ $env:FLASK_DEBUG = "1"
24
+ $env:RACE_DEMO_SECRET_KEY = "replace-with-your-secret"
25
+ $env:RACE_INVENTORY_DB = "D:\path\to\data.db"
26
+ ```
27
+
28
+ ## 本地打包
29
+
30
+ ```powershell
31
+ python -m pip install --upgrade build twine
32
+ python -m build
33
+ python -m twine check dist/*
34
+ ```
35
+
36
+ 构建产物会生成在 `dist/` 目录。
37
+
38
+ ## 准备离线依赖
39
+
40
+ ```powershell
41
+ .\scripts\download_dependencies.ps1
42
+ ```
43
+
44
+ 脚本会把依赖包下载到 `wheelhouse/`,并在已经构建 `dist/` 的情况下把本项目 wheel 一起复制进去。比赛方可用下面命令离线安装:
45
+
46
+ ```powershell
47
+ python -m pip install --no-index --find-links wheelhouse Race-Inventorytools
48
+ ```
49
+
50
+ ## 上传 PyPI
51
+
52
+ ```powershell
53
+ python -m twine upload dist/*
54
+ ```
55
+
56
+ 建议先上传 TestPyPI 验证:
57
+
58
+ ```powershell
59
+ python -m twine upload --repository testpypi dist/*
60
+ ```
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "Race-Inventorytools"
7
+ version = "0.1.0"
8
+ description = "A Flask-Admin inventory CRUD demo application."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [
12
+ { name = "Competition Demo Team" }
13
+ ]
14
+ keywords = ["flask", "crud", "inventory", "admin", "sqlite"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: Flask",
18
+ "Intended Audience :: Education",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+ dependencies = [
26
+ "Flask==3.0.0",
27
+ "Flask-SQLAlchemy==3.1.1",
28
+ "Flask-Admin==1.6.1",
29
+ "WTForms==3.1.2",
30
+ "SQLAlchemy>=2.0",
31
+ "Flask-Babel==4.0.0",
32
+ ]
33
+
34
+ [project.scripts]
35
+ race-inventory-admin = "flask_crud_competition.app:main"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.setuptools.package-data]
41
+ flask_crud_competition = ["templates/*.html"]
@@ -0,0 +1,6 @@
1
+ Flask==3.0.0
2
+ Flask-SQLAlchemy==3.1.1
3
+ Flask-Admin==1.6.1
4
+ WTForms==3.1.2
5
+ SQLAlchemy>=2.0
6
+ Flask-Babel==4.0.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: Race-Inventorytools
3
+ Version: 0.1.0
4
+ Summary: A Flask-Admin inventory CRUD demo application.
5
+ Author: Competition Demo Team
6
+ Keywords: flask,crud,inventory,admin,sqlite
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Framework :: Flask
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: Flask==3.0.0
18
+ Requires-Dist: Flask-SQLAlchemy==3.1.1
19
+ Requires-Dist: Flask-Admin==1.6.1
20
+ Requires-Dist: WTForms==3.1.2
21
+ Requires-Dist: SQLAlchemy>=2.0
22
+ Requires-Dist: Flask-Babel==4.0.0
23
+
24
+ # Race Inventory CRUD Demo
25
+
26
+ 这是一个用于演示的 Flask-Admin 库存 CRUD 应用,包含产品管理和出入库流水管理。
27
+
28
+ ## 安装运行
29
+
30
+ ```powershell
31
+ python -m pip install Race-Inventorytools
32
+ race-inventory-admin
33
+ ```
34
+
35
+ 默认访问地址:
36
+
37
+ ```text
38
+ http://127.0.0.1:5000/admin
39
+ ```
40
+
41
+ 可选环境变量:
42
+
43
+ ```powershell
44
+ $env:FLASK_RUN_HOST = "127.0.0.1"
45
+ $env:FLASK_RUN_PORT = "5000"
46
+ $env:FLASK_DEBUG = "1"
47
+ $env:RACE_DEMO_SECRET_KEY = "replace-with-your-secret"
48
+ $env:RACE_INVENTORY_DB = "D:\path\to\data.db"
49
+ ```
50
+
51
+ ## 本地打包
52
+
53
+ ```powershell
54
+ python -m pip install --upgrade build twine
55
+ python -m build
56
+ python -m twine check dist/*
57
+ ```
58
+
59
+ 构建产物会生成在 `dist/` 目录。
60
+
61
+ ## 准备离线依赖
62
+
63
+ ```powershell
64
+ .\scripts\download_dependencies.ps1
65
+ ```
66
+
67
+ 脚本会把依赖包下载到 `wheelhouse/`,并在已经构建 `dist/` 的情况下把本项目 wheel 一起复制进去。比赛方可用下面命令离线安装:
68
+
69
+ ```powershell
70
+ python -m pip install --no-index --find-links wheelhouse Race-Inventorytools
71
+ ```
72
+
73
+ ## 上传 PyPI
74
+
75
+ ```powershell
76
+ python -m twine upload dist/*
77
+ ```
78
+
79
+ 建议先上传 TestPyPI 验证:
80
+
81
+ ```powershell
82
+ python -m twine upload --repository testpypi dist/*
83
+ ```
@@ -0,0 +1,16 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ requirements.txt
5
+ src/Race_Inventorytools.egg-info/PKG-INFO
6
+ src/Race_Inventorytools.egg-info/SOURCES.txt
7
+ src/Race_Inventorytools.egg-info/dependency_links.txt
8
+ src/Race_Inventorytools.egg-info/entry_points.txt
9
+ src/Race_Inventorytools.egg-info/requires.txt
10
+ src/Race_Inventorytools.egg-info/top_level.txt
11
+ src/flask_crud_competition/__init__.py
12
+ src/flask_crud_competition/__main__.py
13
+ src/flask_crud_competition/app.py
14
+ src/flask_crud_competition/app1.py
15
+ src/flask_crud_competition/demo.py
16
+ src/flask_crud_competition/templates/index.html
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ race-inventory-admin = flask_crud_competition.app:main
@@ -0,0 +1,6 @@
1
+ Flask==3.0.0
2
+ Flask-SQLAlchemy==3.1.1
3
+ Flask-Admin==1.6.1
4
+ WTForms==3.1.2
5
+ SQLAlchemy>=2.0
6
+ Flask-Babel==4.0.0
@@ -0,0 +1 @@
1
+ flask_crud_competition
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .app import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,388 @@
1
+ import os
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ from flask import Flask, flash, redirect
6
+ from flask_babel import Babel
7
+ from flask_sqlalchemy import SQLAlchemy
8
+ from flask_admin import Admin, AdminIndexView
9
+ from flask_admin.contrib.sqla import ModelView
10
+ from flask_admin.form import SecureForm
11
+ from wtforms import validators
12
+
13
+ # ===================== 初始化Flask与数据库 =====================
14
+ app = Flask(__name__, instance_relative_config=True)
15
+ Path(app.instance_path).mkdir(parents=True, exist_ok=True)
16
+
17
+ # 密钥必须配置,表单校验、flash提示依赖;正式环境可通过环境变量覆盖
18
+ app.config["SECRET_KEY"] = os.environ.get(
19
+ "RACE_DEMO_SECRET_KEY",
20
+ "race_demo_local_dev_key_change_me"
21
+ )
22
+
23
+ database_url = os.environ.get("DATABASE_URL")
24
+ if database_url:
25
+ app.config["SQLALCHEMY_DATABASE_URI"] = database_url
26
+ else:
27
+ database_path = Path(os.environ.get("RACE_INVENTORY_DB", Path(app.instance_path) / "data.db"))
28
+ if not database_path.is_absolute():
29
+ database_path = Path.cwd() / database_path
30
+ database_path.parent.mkdir(parents=True, exist_ok=True)
31
+ app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{database_path.as_posix()}"
32
+ app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
33
+ # Flask-Admin / WTForms 中文化
34
+ app.config["BABEL_DEFAULT_LOCALE"] = "zh_Hans_CN"
35
+ app.config["BABEL_DEFAULT_TIMEZONE"] = "Asia/Shanghai"
36
+
37
+ db = SQLAlchemy(app)
38
+ # 固定使用简体中文,后台按钮/提示全部中文化
39
+ babel = Babel(app, locale_selector=lambda: "zh_Hans_CN")
40
+
41
+ # ===================== 数据库模型(产品表) =====================
42
+ class Product(db.Model):
43
+ __tablename__ = "product"
44
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment="主键ID")
45
+ code = db.Column(db.String(30), unique=True, nullable=False, comment="产品编号(唯一)")
46
+ name = db.Column(db.String(100), nullable=False, comment="产品名称")
47
+ count = db.Column(db.Integer, nullable=False, comment="库存数量")
48
+ price = db.Column(db.Float, nullable=False, comment="单价")
49
+ remark = db.Column(db.Text, comment="备注信息")
50
+ stock_records = db.relationship(
51
+ "StockRecord",
52
+ back_populates="product",
53
+ cascade="all, delete-orphan"
54
+ )
55
+
56
+ def __repr__(self):
57
+ return f"{self.code} - {self.name}"
58
+
59
+
60
+ # ===================== 数据库模型(出入库流水表) =====================
61
+ class StockRecord(db.Model):
62
+ __tablename__ = "stock_record"
63
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment="主键ID")
64
+ product_id = db.Column(db.Integer, db.ForeignKey("product.id"), nullable=False, comment="产品ID")
65
+ type = db.Column(db.String(10), nullable=False, comment="操作类型")
66
+ quantity = db.Column(db.Integer, nullable=False, comment="出入库数量")
67
+ before_count = db.Column(db.Integer, nullable=False, default=0, comment="操作前库存")
68
+ after_count = db.Column(db.Integer, nullable=False, default=0, comment="操作后库存")
69
+ operator = db.Column(db.String(50), nullable=False, default="管理员", comment="操作人")
70
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.now, comment="操作时间")
71
+ remark = db.Column(db.Text, comment="备注信息")
72
+ product = db.relationship("Product", back_populates="stock_records")
73
+
74
+ def __repr__(self):
75
+ product_name = self.product.name if self.product else "未知产品"
76
+ return f"{product_name} {self.type} {self.quantity}"
77
+
78
+ # ===================== 自定义Admin视图 + 表单校验核心 =====================
79
+ class HiddenAdminIndexView(AdminIndexView):
80
+ def is_visible(self):
81
+ return False
82
+
83
+
84
+ class ProductAdmin(ModelView):
85
+ # 开启CSRF安全表单
86
+ form_base_class = SecureForm
87
+
88
+ # 列表页弹窗新增/编辑,不跳转新页面
89
+ create_modal = True
90
+ edit_modal = True
91
+
92
+ # 补充中文标题(Flask-Babel 覆盖 Create/Save/Delete 等按钮)
93
+ create_modal_title = "新建产品"
94
+ edit_modal_title = "编辑产品"
95
+ details_modal_title = "详情"
96
+
97
+ # 页面展示配置
98
+ column_list = ["id", "code", "name", "count", "price", "remark"] # 列表显示字段
99
+ column_searchable_list = ["code", "name"] # 可搜索字段
100
+ column_sortable_list = ["id", "count", "price"] # 可排序
101
+ column_labels = {
102
+ "id": "序号",
103
+ "code": "产品编号",
104
+ "name": "产品名称",
105
+ "count": "库存数量",
106
+
107
+ "price": "单价(元)",
108
+ "remark": "备注"
109
+ }
110
+
111
+ # 表单字段顺序
112
+ form_columns = ["code", "name", "count", "price", "remark"]
113
+
114
+ # 全局基础校验规则
115
+ form_args = {
116
+ "code": {
117
+ "validators": [
118
+ validators.DataRequired(message="产品编号不能为空"),
119
+ validators.Length(min=3, max=30, message="编号长度3~30位")
120
+ ]
121
+ },
122
+ "name": {
123
+ "validators": [
124
+ validators.DataRequired(message="产品名称不能为空"),
125
+ validators.Length(min=2, max=100, message="名称长度2~100位")
126
+ ]
127
+ },
128
+ "count": {
129
+ "validators": [validators.InputRequired(message="库存不能为空")]
130
+ },
131
+ "price": {
132
+ "validators": [validators.DataRequired(message="价格不能为空")]
133
+ }
134
+ }
135
+
136
+ # 【重点:自定义业务校验】仅新增/编辑执行;删除表单无业务字段
137
+ def validate_form(self, form):
138
+ # 删除确认表单(DeleteForm)只有 id/csrf,没有 count/code 等字段
139
+ if not hasattr(form, "code") or not hasattr(form, "count") or not hasattr(form, "price"):
140
+ return super().validate_form(form)
141
+
142
+ # 1. 校验库存不能小于0,出库后允许库存归零
143
+ if form.count.data is not None and form.count.data < 0:
144
+ flash("库存数量不能小于0!", "error")
145
+ return False
146
+
147
+ # 2. 校验价格必须大于0
148
+ if form.price.data is not None and form.price.data <= 0:
149
+ flash("商品单价必须大于0!", "error")
150
+ return False
151
+
152
+ # 3. 校验产品编号唯一性(区分新增和编辑)
153
+ code = (form.code.data or "").strip()
154
+ if not code:
155
+ return super().validate_form(form)
156
+
157
+ # 获取当前编辑对象(编辑时存在,新增为None)
158
+ model_id = form._obj.id if getattr(form, "_obj", None) else None
159
+ exist = Product.query.filter_by(code=code).first()
160
+
161
+ # 新增:编号已存在
162
+ if model_id is None and exist:
163
+ flash(f"编号 {code} 已存在,不可重复!", "error")
164
+ return False
165
+ # 编辑:修改为其他已存在编号
166
+ if model_id is not None and exist and exist.id != model_id:
167
+ flash(f"编号 {code} 已被其他产品占用!", "error")
168
+ return False
169
+
170
+ return super().validate_form(form)
171
+
172
+
173
+ # ===================== 自定义Admin视图(出入库管理) =====================
174
+ class StockRecordAdmin(ModelView):
175
+ form_base_class = SecureForm
176
+
177
+ create_modal = True
178
+ edit_modal = True
179
+ can_view_details = True
180
+
181
+ create_modal_title = "新建出入库记录"
182
+ edit_modal_title = "编辑出入库记录"
183
+ details_modal_title = "出入库详情"
184
+
185
+ column_list = [
186
+ "id",
187
+ "product",
188
+ "type",
189
+ "quantity",
190
+ "before_count",
191
+ "after_count",
192
+ "operator",
193
+ "created_at",
194
+ "remark"
195
+ ]
196
+ column_searchable_list = ["operator", "remark"]
197
+ column_sortable_list = ["id", "type", "quantity", "before_count", "after_count", "created_at"]
198
+ column_filters = ["type", "operator", "created_at"]
199
+ column_default_sort = ("id", True)
200
+ column_labels = {
201
+ "id": "序号",
202
+ "product": "产品",
203
+ "type": "类型",
204
+ "quantity": "数量",
205
+ "before_count": "操作前库存",
206
+ "after_count": "操作后库存",
207
+ "operator": "操作人",
208
+ "created_at": "操作时间",
209
+ "remark": "备注"
210
+ }
211
+ column_formatters = {
212
+ "created_at": lambda view, context, model, name: (
213
+ model.created_at.strftime("%Y-%m-%d %H:%M:%S") if model.created_at else ""
214
+ )
215
+ }
216
+
217
+ form_columns = ["product", "type", "quantity", "operator", "remark"]
218
+ form_choices = {
219
+ "type": [("入库", "入库"), ("出库", "出库")]
220
+ }
221
+ form_args = {
222
+ "product": {
223
+ "validators": [validators.DataRequired(message="产品不能为空")]
224
+ },
225
+ "type": {
226
+ "validators": [
227
+ validators.DataRequired(message="类型不能为空"),
228
+ validators.AnyOf(["入库", "出库"], message="类型只能选择入库或出库")
229
+ ]
230
+ },
231
+ "quantity": {
232
+ "validators": [validators.DataRequired(message="数量不能为空")]
233
+ },
234
+ "operator": {
235
+ "validators": [
236
+ validators.DataRequired(message="操作人不能为空"),
237
+ validators.Length(min=2, max=50, message="操作人长度2~50位")
238
+ ]
239
+ }
240
+ }
241
+
242
+ @staticmethod
243
+ def _stock_effect(record_type, quantity):
244
+ return quantity if record_type == "入库" else -quantity
245
+
246
+ def _get_old_stock_info(self, form):
247
+ old_model = getattr(form, "_obj", None)
248
+ if not old_model or not old_model.id:
249
+ return None
250
+ return {
251
+ "product_id": old_model.product_id,
252
+ "type": old_model.type,
253
+ "quantity": old_model.quantity
254
+ }
255
+
256
+ def _preview_stock_change(self, product, record_type, quantity, old_info=None):
257
+ old_effect = 0
258
+ if old_info and old_info["product_id"] == product.id:
259
+ old_effect = self._stock_effect(old_info["type"], old_info["quantity"])
260
+
261
+ before_count = (product.count or 0) - old_effect
262
+ after_count = before_count + self._stock_effect(record_type, quantity)
263
+ return before_count, after_count
264
+
265
+ def _preview_old_product_after_rollback(self, old_info, new_product_id):
266
+ if not old_info or old_info["product_id"] == new_product_id:
267
+ return None, None
268
+
269
+ old_product = Product.query.get(old_info["product_id"])
270
+ if not old_product:
271
+ return None, None
272
+
273
+ old_effect = self._stock_effect(old_info["type"], old_info["quantity"])
274
+ return old_product, (old_product.count or 0) - old_effect
275
+
276
+ def validate_form(self, form):
277
+ if not super().validate_form(form):
278
+ return False
279
+
280
+ if not hasattr(form, "product") or not hasattr(form, "type") or not hasattr(form, "quantity"):
281
+ return True
282
+
283
+ product = form.product.data
284
+ record_type = form.type.data
285
+ quantity = form.quantity.data
286
+ old_info = self._get_old_stock_info(form)
287
+ form._old_stock_info = old_info
288
+
289
+ if not product:
290
+ flash("请选择产品!", "error")
291
+ return False
292
+
293
+ if quantity is None or quantity <= 0:
294
+ flash("出入库数量必须大于0!", "error")
295
+ return False
296
+
297
+ old_product, old_product_after = self._preview_old_product_after_rollback(old_info, product.id)
298
+ if old_product and old_product_after < 0:
299
+ flash(f"调整后 {old_product.name} 的库存会小于0,请先调整相关出库记录!", "error")
300
+ return False
301
+
302
+ before_count, after_count = self._preview_stock_change(product, record_type, quantity, old_info)
303
+ if after_count < 0:
304
+ flash(f"库存不足,当前可出库数量为 {before_count}!", "error")
305
+ return False
306
+
307
+ return True
308
+
309
+ def on_model_change(self, form, model, is_created):
310
+ old_info = getattr(form, "_old_stock_info", None)
311
+ product = model.product
312
+ before_count, after_count = self._preview_stock_change(
313
+ product,
314
+ model.type,
315
+ model.quantity,
316
+ old_info
317
+ )
318
+
319
+ if old_info and old_info["product_id"] != product.id:
320
+ old_product = Product.query.get(old_info["product_id"])
321
+ if old_product:
322
+ old_product.count = (old_product.count or 0) - self._stock_effect(
323
+ old_info["type"],
324
+ old_info["quantity"]
325
+ )
326
+
327
+ model.before_count = before_count
328
+ model.after_count = after_count
329
+ product.count = after_count
330
+
331
+ return super().on_model_change(form, model, is_created)
332
+
333
+ def delete_model(self, model):
334
+ product = model.product
335
+ after_count = (product.count or 0) - self._stock_effect(model.type, model.quantity)
336
+ if after_count < 0:
337
+ flash("删除该入库记录会导致库存小于0,请先调整相关出库记录!", "error")
338
+ return False
339
+
340
+ try:
341
+ product.count = after_count
342
+ self.session.delete(model)
343
+ self.session.commit()
344
+ flash("出入库记录删除成功,库存已同步回滚。", "success")
345
+ return True
346
+ except Exception:
347
+ self.session.rollback()
348
+ raise
349
+
350
+ # ===================== 初始化后台管理 =====================
351
+ admin = Admin(
352
+ app,
353
+ name="管理后台",
354
+ template_mode="bootstrap3",
355
+ index_view=HiddenAdminIndexView(url="/admin", endpoint="admin")
356
+ )
357
+ admin.add_view(ProductAdmin(Product, db.session, name="产品管理", category="库存管理"))
358
+ admin.add_view(StockRecordAdmin(StockRecord, db.session, name="出入库管理", category="库存管理"))
359
+
360
+
361
+ @app.route("/")
362
+ def index():
363
+ return redirect("/admin")
364
+
365
+
366
+ # ===================== 创建数据库表(程序启动自动执行) =====================
367
+ def init_database():
368
+ """Create demo tables if they do not exist."""
369
+ with app.app_context():
370
+ db.create_all()
371
+
372
+
373
+ init_database()
374
+
375
+
376
+ def main():
377
+ host = os.environ.get("FLASK_RUN_HOST", "127.0.0.1")
378
+ port = int(os.environ.get("FLASK_RUN_PORT", "5000"))
379
+ debug = os.environ.get("FLASK_DEBUG", "1").lower() in {"1", "true", "yes", "on"}
380
+
381
+ init_database()
382
+ print(f"数据库表初始化完成,访问 http://{host}:{port}/admin 使用后台")
383
+ app.run(debug=debug, host=host, port=port)
384
+
385
+
386
+ # ===================== 启动入口 =====================
387
+ if __name__ == "__main__":
388
+ main()
@@ -0,0 +1,98 @@
1
+ # 1. 先写 Flask 基础结构:app = Flask(__name__)
2
+ # 2. 再写数据库:数据库文件名、建表函数 init_db()
3
+ # 3. 再写数据库操作函数:查、新增、修改、删除
4
+ # 4. 前端页面已拆分到 templates/index.html
5
+ # 5. 最后写路由:/、/edit/<id>、/save、/del/<id>
6
+
7
+ # 可以记成一句话:先数据表,再操作函数,再页面,最后接路由。
8
+
9
+ from flask import Flask, render_template, request, redirect, url_for # 从 Flask 导入需要用到的工具:创建网站、渲染模板、读取表单、跳转页面、生成路由地址
10
+ import sqlite3 # 导入 Python 自带的 sqlite3 模块,用来操作本地 SQLite 数据库文件
11
+
12
+ app = Flask(__name__) # 创建一个 Flask 应用对象,__name__ 表示当前这个 Python 文件
13
+ DB_FILE = "user.db" # 定义数据库文件名,后面所有数据库操作都连接这个文件
14
+
15
+
16
+ # 初始化数据表:程序启动时先保证 user 表存在
17
+ def init_db(): # 定义一个函数,专门负责创建数据库表
18
+ conn = sqlite3.connect(DB_FILE) # 连接数据库;如果 user.db 不存在,SQLite 会自动创建
19
+ c = conn.cursor() # 创建游标对象,游标可以执行 SQL 语句
20
+ c.execute('''CREATE TABLE IF NOT EXISTS user
21
+ (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, phone TEXT)''') # 如果 user 表不存在,就创建一个包含 id、name、phone 的表
22
+ conn.commit() # 提交更改,让建表操作真正保存到数据库文件
23
+ conn.close() # 关闭数据库连接,避免资源一直占用
24
+
25
+
26
+ # 查询全部用户:给首页表格使用
27
+ def get_all_users(): # 定义查询函数,返回数据库里所有用户数据
28
+ conn = sqlite3.connect(DB_FILE) # 打开数据库连接
29
+ data = conn.execute("SELECT * FROM user").fetchall() # 执行查询语句,并用 fetchall 取出全部结果
30
+ conn.close() # 查询完成后关闭连接
31
+ return data # 把查询到的数据返回给调用者,格式类似 [(1, "张三", "138..."), ...]
32
+
33
+
34
+ # 新增用户:把表单里的姓名和电话插入数据库
35
+ def add_user(name, phone): # 定义新增函数,需要传入 name 和 phone
36
+ conn = sqlite3.connect(DB_FILE) # 打开数据库连接
37
+ conn.execute("INSERT INTO user(name,phone) VALUES (?,?)", (name, phone)) # 用占位符 ? 插入数据,避免直接拼接字符串
38
+ conn.commit() # 提交事务,让新增的数据真正写入数据库
39
+ conn.close() # 关闭数据库连接
40
+
41
+
42
+ # 修改用户:根据 id 更新姓名和电话
43
+ def edit_user(uid, name, phone): # 定义修改函数,uid 是要修改的用户编号
44
+ conn = sqlite3.connect(DB_FILE) # 打开数据库连接
45
+ conn.execute("UPDATE user SET name=?,phone=? WHERE id=?", (name, phone, uid)) # 按 id 找到对应用户,并更新 name 和 phone
46
+ conn.commit() # 提交事务,让修改结果保存
47
+ conn.close() # 关闭数据库连接
48
+
49
+
50
+ # 删除用户:根据 id 删除一条记录
51
+ def del_user(uid): # 定义删除函数,uid 是要删除的用户编号
52
+ conn = sqlite3.connect(DB_FILE) # 打开数据库连接
53
+ conn.execute("DELETE FROM user WHERE id=?", (uid,)) # 删除 id 等于 uid 的用户;单个参数也要写成元组,所以这里有逗号
54
+ conn.commit() # 提交事务,让删除操作生效
55
+ conn.close() # 关闭数据库连接
56
+
57
+
58
+ # 首页路由:展示全部数据
59
+ @app.route('/') # 当浏览器访问 / 时,Flask 会执行下面的 index 函数
60
+ def index(): # 定义首页处理函数
61
+ return render_template("index.html", list_data=get_all_users(), edit_row=None) # 渲染前端模板,把用户列表传进去;首页没有编辑对象,所以 edit_row 是 None
62
+
63
+
64
+ # 编辑路由:点击编辑按钮后,回填对应用户的数据到表单
65
+ @app.route('/edit/<int:uid>') # <int:uid> 表示从网址里接收一个整数参数,例如 /edit/3
66
+ def edit(uid): # uid 会自动接收网址里的用户 id
67
+ rows = get_all_users() # 先查询全部用户,表格还要继续展示这些数据
68
+ target = None # 先假设没有找到要编辑的那一行
69
+ for r in rows: # 遍历所有用户记录
70
+ if r[0] == uid: # r[0] 是用户 id,如果等于网址里的 uid,说明找到了
71
+ target = r # 把这条记录保存到 target,用来回填表单
72
+ break # 找到后就停止循环,没必要继续找
73
+ return render_template("index.html", list_data=rows, edit_row=target) # 渲染同一个页面,但把 target 传给 edit_row,实现表单回填
74
+
75
+
76
+ # 保存路由:新增和修改共用这一个接口
77
+ @app.route('/save', methods=["POST"]) # 只有 POST 请求才能访问这个地址,因为它负责接收表单提交
78
+ def save(): # 定义保存处理函数
79
+ uid = request.form.get("id") # 从表单中取隐藏字段 id;有 id 表示编辑,没有 id 表示新增
80
+ name = request.form["name"] # 从表单中取姓名;因为姓名 required,所以这里直接用 [] 读取
81
+ phone = request.form["phone"] # 从表单中取电话
82
+ if uid: # 如果 uid 有值,说明这次提交是修改已有用户
83
+ edit_user(uid, name, phone) # 调用修改函数,按 id 更新数据
84
+ else: # 如果 uid 为空,说明这次提交是新增用户
85
+ add_user(name, phone) # 调用新增函数,插入一条新数据
86
+ return redirect(url_for("index")) # 保存完成后跳转回首页,避免刷新页面重复提交表单
87
+
88
+
89
+ # 删除路由:根据网址里的 id 删除用户
90
+ @app.route('/del/<int:uid>') # 访问 /del/用户id 时,执行下面的 delete 函数
91
+ def delete(uid): # uid 自动接收网址里的用户 id
92
+ del_user(uid) # 调用删除函数,从数据库删除对应用户
93
+ return redirect(url_for("index")) # 删除后跳转回首页,重新显示最新列表
94
+
95
+
96
+ if __name__ == '__main__': # 只有直接运行这个文件时,下面的代码才会执行;被别的文件导入时不会执行
97
+ init_db() # 启动网站前先初始化数据库表,确保 user 表存在
98
+ app.run(debug=True) # 启动 Flask 开发服务器;debug=True 方便开发时看到错误并自动重载
@@ -0,0 +1,31 @@
1
+ from flask import Flask
2
+ from flask_admin import Admin
3
+ from flask_admin.contrib.sqla import ModelView
4
+ from flask_sqlalchemy import SQLAlchemy
5
+
6
+ app = Flask(__name__)
7
+ app.config['SECRET_KEY'] = 'race_demo_2026_secret_key_123456'
8
+ # 数据库配置
9
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///demo.db'
10
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
11
+
12
+ db = SQLAlchemy(app)
13
+
14
+ class User(db.Model):
15
+ id = db.Column(db.Integer, primary_key=True)
16
+ name = db.Column(db.String(128))
17
+
18
+ # 首页路由
19
+ @app.route('/')
20
+ def index():
21
+ return '<h3>首页</h3><p>后台管理地址:<a href="/admin">/admin</a></p>'
22
+
23
+ admin = Admin(app, name='My Admin', template_mode='bootstrap3')
24
+ admin.add_view(ModelView(User, db.session))
25
+
26
+ # 创建数据表
27
+ with app.app_context():
28
+ db.create_all()
29
+
30
+ if __name__ == '__main__':
31
+ app.run(debug=True)
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html> <!-- 声明这是 HTML5 页面 -->
2
+ <html> <!-- HTML 文档开始 -->
3
+ <head> <!-- head 里放网页编码、标题、样式等信息 -->
4
+ <meta charset="utf-8"> <!-- 设置网页字符编码,避免中文乱码 -->
5
+ <title>网页CRUD</title> <!-- 浏览器标签页显示的标题 -->
6
+ <style> /* style 标签里写 CSS,用来控制页面外观 */
7
+ table{border-collapse: collapse;width:100%;margin:20px 0;} /* 表格边框合并、宽度占满、上下留间距 */
8
+ td,th{border:1px solid #999;padding:8px 12px} /* 单元格和表头设置边框与内边距 */
9
+ .form{margin:15px 0;} /* 表单区域上下留一点距离 */
10
+ input{padding:6px;margin:0 5px;} /* 输入框内部留白,并和旁边元素拉开距离 */
11
+ button{padding:6px 12px;cursor:pointer} /* 按钮设置大小,鼠标悬停时显示手型 */
12
+ </style> <!-- CSS 样式结束 -->
13
+ </head> <!-- head 结束 -->
14
+ <body> <!-- body 里放用户能看到的页面内容 -->
15
+ <h2>人员信息管理(增删改查)</h2> <!-- 页面主标题 -->
16
+
17
+ <!-- 新增/编辑表单:新增和编辑共用同一个表单 -->
18
+ <div class="form"> <!-- 给表单外面套一层 div,方便用 CSS 控制间距 -->
19
+ <form method="post" action="/save"> <!-- 表单提交方式是 POST,提交地址是 /save -->
20
+ <input type="hidden" name="id" value="{{ edit_row[0] if edit_row else '' }}"> <!-- 隐藏字段保存用户 id;编辑时有 id,新增时为空 -->
21
+ 姓名:<input name="name" required value="{{ edit_row[1] if edit_row else '' }}"> <!-- 姓名输入框;required 表示必填;编辑时自动回填姓名 -->
22
+ 电话:<input name="phone" value="{{ edit_row[2] if edit_row else '' }}"> <!-- 电话输入框;编辑时自动回填电话 -->
23
+ <button type="submit">提交保存</button> <!-- 点击后提交表单到 /save -->
24
+ <a href="/"><button type="button">清空</button></a> <!-- 回到首页,相当于清空编辑状态和输入框 -->
25
+ </form> <!-- 表单结束 -->
26
+ </div> <!-- 表单区域结束 -->
27
+
28
+ <!-- 数据列表:把数据库里的用户数据展示成表格 -->
29
+ <table> <!-- 表格开始 -->
30
+ <tr> <!-- 表头行开始 -->
31
+ <th>编号</th> <!-- 第一列:用户编号 -->
32
+ <th>姓名</th> <!-- 第二列:用户姓名 -->
33
+ <th>电话</th> <!-- 第三列:用户电话 -->
34
+ <th>操作</th> <!-- 第四列:编辑和删除按钮 -->
35
+ </tr> <!-- 表头行结束 -->
36
+ {% for item in list_data %} <!-- Jinja 循环:list_data 里有几条数据,就生成几行表格 -->
37
+ <tr> <!-- 一条用户数据对应一行 -->
38
+ <td>{{ item[0] }}</td> <!-- 显示用户 id;item[0] 对应数据库查询结果的第一列 -->
39
+ <td>{{ item[1] }}</td> <!-- 显示用户姓名;item[1] 对应第二列 -->
40
+ <td>{{ item[2] }}</td> <!-- 显示用户电话;item[2] 对应第三列 -->
41
+ <td> <!-- 操作列开始 -->
42
+ <a href="/edit/{{ item[0] }}"><button>编辑</button></a> <!-- 点击编辑,访问 /edit/用户id,让表单回填这条数据 -->
43
+ <a href="/del/{{ item[0] }}" onclick="return confirm('确定删除?')"> <!-- 点击删除前先弹出确认框,确认后访问 /del/用户id -->
44
+ <button style="background:#f44;color:#fff;border:0">删除</button> <!-- 删除按钮用红色强调危险操作 -->
45
+ </a> <!-- 删除链接结束 -->
46
+ </td> <!-- 操作列结束 -->
47
+ </tr> <!-- 当前用户这一行结束 -->
48
+ {% endfor %} <!-- Jinja 循环结束 -->
49
+ </table> <!-- 表格结束 -->
50
+ </body> <!-- 页面主体结束 -->
51
+ </html> <!-- HTML 文档结束 -->