feat: add standard system initialization tooling

This commit is contained in:
汤学会
2026-06-15 00:14:21 +08:00
parent 442713b4ef
commit f957899500
13 changed files with 4636 additions and 9 deletions
+4 -1
View File
@@ -1,4 +1,6 @@
from app.models.master_data import Bom, BomItem, Item, Material, Product, StockBalance, Warehouse
from app.models import miniapp as _miniapp # noqa: F401
from app.models import operations as _operations # noqa: F401
from app.models.master_data import Bom, BomItem, Item, ItemCategory, Material, Product, StockBalance, Warehouse
from app.models.document_archive import DocumentArchive
from app.models.org import Department, Employee, Permission, Role, RolePermission, User, UserRole
from app.models.planning import MaterialDemand
@@ -12,6 +14,7 @@ __all__ = [
"Bom",
"BomItem",
"Item",
"ItemCategory",
"Material",
"MaterialDemand",
"Permission",
+14
View File
@@ -17,6 +17,20 @@ class Unit(Base):
updated_at: Mapped[object] = mapped_column(DateTime)
class ItemCategory(Base):
__tablename__ = "md_item_category"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
category_code: Mapped[str] = mapped_column(String(50))
category_name: Mapped[str] = mapped_column(String(100))
parent_id: Mapped[int | None] = mapped_column(ForeignKey("md_item_category.id"), nullable=True)
item_type: Mapped[str] = mapped_column(String(32))
sort_no: Mapped[int] = mapped_column(Integer, server_default=text("0"))
status: Mapped[str] = mapped_column(String(32), server_default=text("'ACTIVE'"))
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
class Item(Base):
__tablename__ = "md_item"
+215 -8
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from sqlalchemy import BigInteger, Boolean, Date, DateTime, DECIMAL, ForeignKey, Integer, String, Text, text
from sqlalchemy import JSON, BigInteger, Boolean, Date, DateTime, DECIMAL, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
@@ -13,6 +13,16 @@ class MiniAppAttendancePoint(Base):
latitude: Mapped[float | None] = mapped_column(DECIMAL(10, 7), nullable=True)
longitude: Mapped[float | None] = mapped_column(DECIMAL(10, 7), nullable=True)
radius_meters: Mapped[int] = mapped_column(Integer, server_default=text("500"))
day_start: Mapped[str] = mapped_column(String(5), server_default=text("'08:00'"))
day_end: Mapped[str] = mapped_column(String(5), server_default=text("'17:20'"))
lunch_start: Mapped[str] = mapped_column(String(5), server_default=text("'11:40'"))
lunch_end: Mapped[str] = mapped_column(String(5), server_default=text("'12:40'"))
dinner_start: Mapped[str] = mapped_column(String(5), server_default=text("'17:20'"))
dinner_end: Mapped[str] = mapped_column(String(5), server_default=text("'18:00'"))
overtime_start: Mapped[str] = mapped_column(String(5), server_default=text("'18:00'"))
overtime_end: Mapped[str] = mapped_column(String(5), server_default=text("'20:00'"))
night_start: Mapped[str] = mapped_column(String(5), server_default=text("'20:00'"))
night_end: Mapped[str] = mapped_column(String(5), server_default=text("'06:00'"))
remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, server_default=text("1"))
created_at: Mapped[object] = mapped_column(DateTime)
@@ -49,7 +59,7 @@ class MiniAppPersonAttendancePoint(Base):
class MiniAppProduct(Base):
__tablename__ = "products"
attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, server_default=text("'宁波百华智能科技有限公司'"))
attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, server_default=text("''"))
project_no: Mapped[str] = mapped_column(String(64), primary_key=True)
product_name: Mapped[str] = mapped_column(String(255), primary_key=True)
device_no: Mapped[str] = mapped_column(String(64), primary_key=True)
@@ -64,18 +74,96 @@ class MiniAppProduct(Base):
waste_price_yuan_per_kg: Mapped[float | None] = mapped_column(DECIMAL(12, 4), nullable=True)
stamping_method: Mapped[str | None] = mapped_column(String(128), nullable=True)
operator_count: Mapped[float] = mapped_column(DECIMAL(10, 2), server_default=text("1"))
process_unit_price_yuan: Mapped[float] = mapped_column(DECIMAL(12, 4), server_default=text("0"))
standard_beat: Mapped[float] = mapped_column(DECIMAL(12, 3), server_default=text("0"))
standard_workload: Mapped[float] = mapped_column(DECIMAL(12, 2), server_default=text("0"))
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_products_device_no", "device_no"),
Index("idx_products_material_code", "material_code"),
)
class MiniAppEquipment(Base):
__tablename__ = "equipment"
attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, server_default=text("''"))
device_no: Mapped[str] = mapped_column(String(64), primary_key=True)
device_type: Mapped[str] = mapped_column(String(32), server_default=text("'冲压设备'"))
remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
class MiniAppWorkSchedule(Base):
__tablename__ = "work_schedules"
id: Mapped[int] = mapped_column(Integer, primary_key=True, server_default=text("1"))
day_start: Mapped[str] = mapped_column(String(5), server_default=text("'08:00'"))
day_end: Mapped[str] = mapped_column(String(5), server_default=text("'17:20'"))
lunch_start: Mapped[str] = mapped_column(String(5), server_default=text("'11:40'"))
lunch_end: Mapped[str] = mapped_column(String(5), server_default=text("'12:40'"))
dinner_start: Mapped[str] = mapped_column(String(5), server_default=text("'17:20'"))
dinner_end: Mapped[str] = mapped_column(String(5), server_default=text("'18:00'"))
overtime_start: Mapped[str] = mapped_column(String(5), server_default=text("'18:00'"))
overtime_end: Mapped[str] = mapped_column(String(5), server_default=text("'20:00'"))
night_start: Mapped[str] = mapped_column(String(5), server_default=text("'20:00'"))
night_end: Mapped[str] = mapped_column(String(5), server_default=text("'06:00'"))
attendance_latitude: Mapped[float | None] = mapped_column(DECIMAL(10, 7), nullable=True)
attendance_longitude: Mapped[float | None] = mapped_column(DECIMAL(10, 7), nullable=True)
attendance_radius_meters: Mapped[int] = mapped_column(Integer, server_default=text("500"))
auto_submit_hours: Mapped[float] = mapped_column(DECIMAL(10, 2), server_default=text("15"))
updated_by: Mapped[str | None] = mapped_column(String(20), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
class MiniAppWorkSession(Base):
__tablename__ = "work_sessions"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
attendance_point_name: Mapped[str] = mapped_column(String(128), server_default=text("''"))
employee_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"))
start_at: Mapped[object] = mapped_column(DateTime)
end_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
status: Mapped[str] = mapped_column(String(32), server_default=text("'active'"))
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_sessions_employee_status", "employee_phone", "status"),
)
class MiniAppWorkSessionDevice(Base):
__tablename__ = "work_session_devices"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
session_id: Mapped[int] = mapped_column(ForeignKey("work_sessions.id"))
attendance_point_name: Mapped[str] = mapped_column(String(128), server_default=text("''"))
device_no: Mapped[str] = mapped_column(String(255))
process_name: Mapped[str] = mapped_column(String(128), server_default=text("''"))
scanned_at: Mapped[object] = mapped_column(DateTime)
sort_order: Mapped[int] = mapped_column(Integer, server_default=text("0"))
released_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
released_by: Mapped[str | None] = mapped_column(String(20), nullable=True)
release_reason: Mapped[str | None] = mapped_column(String(120), nullable=True)
__table_args__ = (
Index("idx_session_devices_session", "session_id"),
Index("idx_session_devices_device", "device_no"),
Index("idx_session_devices_release", "released_at"),
)
class MiniAppProductionReport(Base):
__tablename__ = "production_reports"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
attendance_point_name: Mapped[str] = mapped_column(String(128), server_default=text("'宁波百华智能科技有限公司'"))
session_id: Mapped[int] = mapped_column(BigInteger)
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
attendance_point_name: Mapped[str] = mapped_column(String(128), server_default=text("''"))
session_id: Mapped[int] = mapped_column(ForeignKey("work_sessions.id"), unique=True)
employee_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"))
report_date: Mapped[object] = mapped_column(Date)
start_at: Mapped[object] = mapped_column(DateTime)
@@ -94,16 +182,32 @@ class MiniAppProductionReport(Base):
reviewer_phone: Mapped[str | None] = mapped_column(String(20), nullable=True)
reviewed_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
reject_reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
review_remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
submitted_at: Mapped[object] = mapped_column(DateTime)
is_system_auto_submitted: Mapped[bool] = mapped_column(Boolean, server_default=text("0"))
auto_submit_reason: Mapped[str | None] = mapped_column(String(120), nullable=True)
is_multi_person_assistant: Mapped[bool] = mapped_column(Boolean, server_default=text("0"))
multi_person_source_report_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
is_voided: Mapped[bool] = mapped_column(Boolean, server_default=text("0"))
voided_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
voided_by: Mapped[str | None] = mapped_column(ForeignKey("personnel.phone"), nullable=True)
unvoid_deadline_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
updated_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_reports_employee_date", "employee_phone", "report_date"),
Index("idx_reports_status_date", "status", "report_date"),
Index("idx_reports_reviewer", "reviewer_phone", "reviewed_at"),
Index("idx_reports_voided", "is_voided", "voided_at"),
)
class MiniAppProductionReportItem(Base):
__tablename__ = "production_report_items"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
report_id: Mapped[int] = mapped_column(ForeignKey("production_reports.id"))
attendance_point_name: Mapped[str] = mapped_column(String(128), server_default=text("'宁波百华智能科技有限公司'"))
attendance_point_name: Mapped[str] = mapped_column(String(128), server_default=text("''"))
device_no: Mapped[str] = mapped_column(String(64))
project_no: Mapped[str] = mapped_column(String(64))
product_name: Mapped[str] = mapped_column(String(255))
@@ -112,6 +216,8 @@ class MiniAppProductionReportItem(Base):
raw_material_batch_no: Mapped[str | None] = mapped_column(String(128), nullable=True)
process_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
stamping_method: Mapped[str | None] = mapped_column(String(128), nullable=True)
operator_count: Mapped[float] = mapped_column(DECIMAL(10, 2), server_default=text("1"))
process_unit_price_yuan: Mapped[float] = mapped_column(DECIMAL(12, 4), server_default=text("0"))
standard_beat: Mapped[float] = mapped_column(DECIMAL(12, 3), server_default=text("0"))
standard_workload: Mapped[float] = mapped_column(DECIMAL(12, 2), server_default=text("0"))
good_qty: Mapped[float] = mapped_column(DECIMAL(12, 2), server_default=text("0"))
@@ -119,13 +225,95 @@ class MiniAppProductionReportItem(Base):
scrap_qty: Mapped[float] = mapped_column(DECIMAL(12, 2), server_default=text("0"))
allocated_minutes: Mapped[float] = mapped_column(DECIMAL(10, 2), server_default=text("0"))
started_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_report_items_report", "report_id"),
Index("idx_report_items_product", "project_no", "product_name"),
)
class MiniAppReportAuditLog(Base):
__tablename__ = "report_audit_logs"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
report_id: Mapped[int] = mapped_column(ForeignKey("production_reports.id"))
reviewer_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"))
action: Mapped[str] = mapped_column(String(64))
before_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
after_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_audit_report", "report_id"),
Index("idx_audit_reviewer", "reviewer_phone", "created_at"),
)
class MiniAppMoldLockFeedback(Base):
__tablename__ = "mold_lock_feedbacks"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
attendance_point_name: Mapped[str] = mapped_column(String(128), server_default=text("''"))
mold_name: Mapped[str] = mapped_column(String(255))
process_name: Mapped[str] = mapped_column(String(128), server_default=text("''"))
session_device_id: Mapped[int] = mapped_column(ForeignKey("work_session_devices.id"))
reporter_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"))
occupied_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"))
status: Mapped[str] = mapped_column(String(32), server_default=text("'pending'"))
read_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
handled_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
handled_by: Mapped[str | None] = mapped_column(ForeignKey("personnel.phone"), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_mold_lock_feedback_point_read", "attendance_point_name", "read_at", "created_at"),
Index("idx_mold_lock_feedback_status", "status", "created_at"),
)
class MiniAppDeviceQRCode(Base):
__tablename__ = "device_qrcodes"
attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, server_default=text("''"))
device_no: Mapped[str] = mapped_column(String(255), primary_key=True)
process_name: Mapped[str] = mapped_column(String(128), primary_key=True, server_default=text("''"))
qr_scene: Mapped[str] = mapped_column(String(128))
qr_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_by: Mapped[str] = mapped_column(ForeignKey("personnel.phone"))
created_at: Mapped[object] = mapped_column(DateTime)
class MiniAppDeviceQRCodeBatchTask(Base):
__tablename__ = "device_qrcode_batch_tasks"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
file_name: Mapped[str] = mapped_column(String(255))
status: Mapped[str] = mapped_column(String(32), server_default=text("'pending'"))
item_count: Mapped[int] = mapped_column(Integer, server_default=text("0"))
completed_count: Mapped[int] = mapped_column(Integer, server_default=text("0"))
failed_count: Mapped[int] = mapped_column(Integer, server_default=text("0"))
items_json: Mapped[list] = mapped_column(JSON)
zip_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
error_message: Mapped[str | None] = mapped_column(String(1000), nullable=True)
created_by: Mapped[str] = mapped_column(ForeignKey("personnel.phone"))
started_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
finished_at: Mapped[object | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_device_qrcode_batch_tasks_creator", "created_by", "created_at"),
Index("idx_device_qrcode_batch_tasks_status", "status", "created_at"),
)
class MiniAppNotice(Base):
__tablename__ = "notices"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(120))
content: Mapped[str] = mapped_column(Text)
sort_order: Mapped[int] = mapped_column(Integer, server_default=text("0"))
@@ -141,3 +329,22 @@ class MiniAppNoticePoint(Base):
notice_id: Mapped[int] = mapped_column(ForeignKey("notices.id"), primary_key=True)
attendance_point_name: Mapped[str] = mapped_column(ForeignKey("attendance_points.name"), primary_key=True)
created_at: Mapped[object] = mapped_column(DateTime)
class MiniAppReconciliationLedgerEntry(Base):
__tablename__ = "reconciliation_ledger_entries"
attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, server_default=text("''"))
year: Mapped[int] = mapped_column(Integer, primary_key=True)
month: Mapped[int] = mapped_column(Integer, primary_key=True)
product_name: Mapped[str] = mapped_column(String(255), primary_key=True)
reconciled_good_qty: Mapped[float] = mapped_column(DECIMAL(14, 2), server_default=text("0"))
return_qty: Mapped[float] = mapped_column(DECIMAL(14, 2), server_default=text("0"))
updated_by: Mapped[str | None] = mapped_column(String(20), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
__table_args__ = (
Index("idx_reconciliation_product", "product_name"),
Index("idx_reconciliation_updated", "updated_at"),
)
+33
View File
@@ -559,6 +559,39 @@ class WorkOrderMaterialIssue(Base):
updated_at: Mapped[object] = mapped_column(DateTime)
class MaterialIssue(Base):
__tablename__ = "pp_material_issue"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
issue_no: Mapped[str] = mapped_column(String(50))
work_order_id: Mapped[int] = mapped_column(ForeignKey("pp_work_order.id"))
warehouse_id: Mapped[int] = mapped_column(ForeignKey("wh_warehouse.id"))
issue_time: Mapped[object] = mapped_column(DateTime)
issuer_employee_id: Mapped[int | None] = mapped_column(ForeignKey("hr_employee.id"), nullable=True)
status: Mapped[str] = mapped_column(String(32), server_default=text("'CREATED'"))
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
class MaterialIssueItem(Base):
__tablename__ = "pp_material_issue_item"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
material_issue_id: Mapped[int] = mapped_column(ForeignKey("pp_material_issue.id"))
work_order_material_id: Mapped[int] = mapped_column(ForeignKey("pp_work_order_material.id"))
material_item_id: Mapped[int] = mapped_column(ForeignKey("md_item.id"))
lot_id: Mapped[int] = mapped_column(ForeignKey("wh_stock_lot.id"))
location_id: Mapped[int | None] = mapped_column(ForeignKey("wh_location.id"), nullable=True)
issue_qty: Mapped[float] = mapped_column(DECIMAL(18, 6), server_default=text("0"))
issue_weight_kg: Mapped[float] = mapped_column(DECIMAL(18, 6), server_default=text("0"))
unit_cost: Mapped[float] = mapped_column(DECIMAL(18, 4), server_default=text("0"))
status: Mapped[str] = mapped_column(String(32), server_default=text("'POSTED'"))
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[object] = mapped_column(DateTime)
updated_at: Mapped[object] = mapped_column(DateTime)
class ProductionBatchLedger(Base):
__tablename__ = "pp_production_batch_ledger"
__table_args__ = (
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
"""Operational scripts for ForgeFlow ERP backend."""
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
import argparse
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from sqlalchemy import create_engine # noqa: E402
from app.core.config import get_settings # noqa: E402
from app.services.system_initializer import ( # noqa: E402
SystemInitializeOptions,
create_database_if_missing,
fresh_initialize_database,
reset_existing_database_with_backup,
)
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Initialize ForgeFlow ERP database for customer deployment.")
subparsers = parser.add_subparsers(dest="mode", required=True)
def add_common_arguments(subparser: argparse.ArgumentParser) -> None:
subparser.add_argument("--company-name", required=True)
subparser.add_argument("--admin-name", required=True)
subparser.add_argument("--admin-phone", required=True)
subparser.add_argument("--admin-password", required=True)
subparser.add_argument(
"--smart-operation-report",
choices=["on", "off"],
default="on",
help="Enable or disable smart miniapp operation report integration.",
)
subparser.add_argument("--delete-files", action="store_true")
subparser.add_argument("--allow-any-database", action="store_true")
fresh = subparsers.add_parser("fresh", help="Create a database if needed, create schema, and seed a clean skeleton.")
add_common_arguments(fresh)
reset = subparsers.add_parser("reset", help="Back up and reset an existing database into a clean skeleton.")
add_common_arguments(reset)
reset.add_argument("--confirm-reset", action="store_true")
return parser.parse_args(argv)
def _options_from_args(args: argparse.Namespace) -> SystemInitializeOptions:
return SystemInitializeOptions(
mode=args.mode,
company_name=args.company_name,
admin_name=args.admin_name,
admin_phone=args.admin_phone,
admin_password=args.admin_password,
smart_operation_report_enabled=args.smart_operation_report == "on",
confirm_reset=bool(getattr(args, "confirm_reset", False)),
delete_files=bool(args.delete_files),
allow_any_database=bool(args.allow_any_database),
)
def _print_result(settings: object, result: object, options: SystemInitializeOptions) -> None:
print(f"mode={options.mode}")
print(f"database={settings.mysql_host}:{settings.mysql_port}/{settings.mysql_database}")
print(f"backup_path={result.backup_path or ''}")
print(f"summary_path={result.summary_path or ''}")
print(f"admin_username={result.seeded.get('admin_username', '')}")
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
settings = get_settings()
options = _options_from_args(args)
engine = create_engine(settings.mysql_dsn, pool_pre_ping=True, future=True)
if options.mode == "fresh":
create_database_if_missing(settings)
result = fresh_initialize_database(engine, options)
else:
result = reset_existing_database_with_backup(engine, settings, options)
_print_result(settings, result, options)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,152 @@
from __future__ import annotations
import io
import unittest
from types import SimpleNamespace
from unittest.mock import patch
class SystemInitializerCliTest(unittest.TestCase):
def test_parse_fresh_arguments(self) -> None:
from scripts.system_initialize import parse_args
args = parse_args(
[
"fresh",
"--company-name",
"百华",
"--admin-name",
"超级管理员",
"--admin-phone",
"13800000000",
"--admin-password",
"secret123",
"--smart-operation-report",
"off",
"--delete-files",
"--allow-any-database",
]
)
self.assertEqual(args.mode, "fresh")
self.assertEqual(args.company_name, "百华")
self.assertEqual(args.admin_name, "超级管理员")
self.assertEqual(args.admin_phone, "13800000000")
self.assertEqual(args.admin_password, "secret123")
self.assertEqual(args.smart_operation_report, "off")
self.assertTrue(args.delete_files)
self.assertTrue(args.allow_any_database)
self.assertFalse(hasattr(args, "confirm_reset"))
def test_parse_reset_arguments(self) -> None:
from scripts.system_initialize import parse_args
args = parse_args(
[
"reset",
"--company-name",
"百华",
"--admin-name",
"超级管理员",
"--admin-phone",
"13800000000",
"--admin-password",
"secret123",
"--confirm-reset",
"--smart-operation-report",
"off",
]
)
self.assertEqual(args.mode, "reset")
self.assertEqual(args.company_name, "百华")
self.assertEqual(args.admin_phone, "13800000000")
self.assertTrue(args.confirm_reset)
self.assertEqual(args.smart_operation_report, "off")
def test_main_fresh_creates_database_then_initializes_without_real_db(self) -> None:
from scripts import system_initialize
settings = SimpleNamespace(
mysql_host="127.0.0.1",
mysql_port=3306,
mysql_database="erp_test",
mysql_dsn="mysql+pymysql://root:secret@127.0.0.1:3306/erp_test",
)
result = SimpleNamespace(
backup_path=None,
summary_path="/tmp/summary.json",
seeded={"admin_username": "13800000000"},
)
with (
patch.object(system_initialize, "get_settings", return_value=settings),
patch.object(system_initialize, "create_engine", return_value="engine") as create_engine_mock,
patch.object(system_initialize, "create_database_if_missing", return_value="erp_test") as create_db_mock,
patch.object(system_initialize, "fresh_initialize_database", return_value=result) as fresh_mock,
patch.object(system_initialize, "reset_existing_database_with_backup") as reset_mock,
patch("sys.stdout", new_callable=io.StringIO) as stdout,
):
exit_code = system_initialize.main(
[
"fresh",
"--company-name",
"百华",
"--admin-name",
"超级管理员",
"--admin-phone",
"13800000000",
"--admin-password",
"secret123",
"--smart-operation-report",
"on",
]
)
self.assertEqual(exit_code, 0)
create_db_mock.assert_called_once_with(settings)
create_engine_mock.assert_called_once_with(settings.mysql_dsn, pool_pre_ping=True, future=True)
fresh_mock.assert_called_once()
reset_mock.assert_not_called()
self.assertIn("mode=fresh", stdout.getvalue())
self.assertIn("database=127.0.0.1:3306/erp_test", stdout.getvalue())
self.assertIn("summary_path=/tmp/summary.json", stdout.getvalue())
self.assertIn("admin_username=13800000000", stdout.getvalue())
def test_main_reset_calls_backup_reset_without_swallowing_service_errors(self) -> None:
from scripts import system_initialize
settings = SimpleNamespace(
mysql_host="127.0.0.1",
mysql_port=3306,
mysql_database="erp_test",
mysql_dsn="mysql+pymysql://root:secret@127.0.0.1:3306/erp_test",
)
with (
patch.object(system_initialize, "get_settings", return_value=settings),
patch.object(system_initialize, "create_engine", return_value="engine"),
patch.object(
system_initialize,
"reset_existing_database_with_backup",
side_effect=ValueError("Refusing to reset database without --confirm-reset"),
),
):
with self.assertRaisesRegex(ValueError, "confirm-reset"):
system_initialize.main(
[
"reset",
"--company-name",
"百华",
"--admin-name",
"超级管理员",
"--admin-phone",
"13800000000",
"--admin-password",
"secret123",
]
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,97 @@
from __future__ import annotations
from pathlib import Path
from sqlalchemy import create_engine, inspect
import app.models # noqa: F401
from app.models.base import Base
MINIAPP_TABLES = {
"attendance_points",
"personnel",
"person_roles",
"person_attendance_points",
"products",
"equipment",
"work_schedules",
"work_sessions",
"work_session_devices",
"production_reports",
"production_report_items",
"report_audit_logs",
"mold_lock_feedbacks",
"device_qrcodes",
"device_qrcode_batch_tasks",
"notices",
"notice_points",
"reconciliation_ledger_entries",
}
REQUIRED_ERP_TABLES = {
"sys_department",
"hr_employee",
"sys_role",
"sys_permission",
"sys_role_permission",
"sys_user",
"sys_user_role",
"sys_system_config",
"sys_ai_assistant_config",
"md_unit",
"md_item_category",
"md_item",
"md_material",
"md_product",
"md_bom",
"md_bom_item",
"wh_warehouse",
"wh_location",
"wh_stock_lot",
"wh_stock_balance",
"wh_inventory_txn",
"pp_material_issue",
"pp_material_issue_item",
"pp_production_batch_ledger",
"pp_production_batch_ledger_txn",
"document_archives",
}
def _erp_model_table_names() -> set[str]:
models_dir = Path(__file__).resolve().parents[1] / "app" / "models"
table_names: set[str] = set()
for model_file in models_dir.glob("*.py"):
if model_file.name in {"__init__.py", "base.py", "miniapp.py"}:
continue
for line in model_file.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if stripped.startswith("__tablename__"):
table_names.add(stripped.split("=", 1)[1].strip().strip("\"'"))
return table_names
def test_app_models_registers_erp_and_miniapp_tables() -> None:
expected_tables = _erp_model_table_names() | REQUIRED_ERP_TABLES | MINIAPP_TABLES
missing_tables = expected_tables - set(Base.metadata.tables)
assert missing_tables == set()
def test_reset_table_groups_are_registered_in_metadata() -> None:
from app.services.system_initializer import ACCOUNT_RESET_TABLES, BUSINESS_RESET_TABLES, MINIAPP_RESET_TABLES, SKELETON_TABLES
expected_tables = set(BUSINESS_RESET_TABLES) | set(MINIAPP_RESET_TABLES) | set(ACCOUNT_RESET_TABLES) | set(SKELETON_TABLES)
assert expected_tables.issubset(set(Base.metadata.tables))
def test_registered_models_create_all_in_sqlite_memory() -> None:
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
created_tables = set(inspect(engine).get_table_names())
assert (MINIAPP_TABLES | REQUIRED_ERP_TABLES).issubset(created_tables)
@@ -0,0 +1,328 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from sqlalchemy import create_engine, inspect, select, text
from sqlalchemy.orm import Session, sessionmaker
import app.models # noqa: F401
from app.models.base import Base
from app.models.org import User
from app.services.auth import verify_password
def _options(*, confirm_reset: bool = False, mode: str = "reset"):
from app.services.system_initializer import SystemInitializeOptions
return SystemInitializeOptions(
mode=mode,
company_name="百华",
admin_name="超级管理员",
admin_phone="13800000000",
admin_password="secret123",
confirm_reset=confirm_reset,
)
class SystemInitializerResetTest(unittest.TestCase):
def setUp(self) -> None:
self.engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(self.engine)
self.SessionLocal = sessionmaker(bind=self.engine, autoflush=False, autocommit=False, future=True)
self.db: Session = self.SessionLocal()
def tearDown(self) -> None:
self.db.close()
def _seed_dirty_data(self) -> None:
self.db.execute(
text(
"""
INSERT INTO sys_department
(id, dept_code, dept_name, org_node_type, dept_type, status, sort_no, created_at, updated_at)
VALUES
(100, 'DIRTY_ROOT', '旧公司', 'COMPANY', 'ADMIN', 'ACTIVE', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
"""
)
)
self.db.execute(
text(
"""
INSERT INTO hr_employee
(id, employee_code, employee_name, dept_id, mobile, is_operator, is_workshop_staff, status, created_at, updated_at)
VALUES
(100, 'EMP_OLD', '旧管理员', 100, '13900000000', 0, 0, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
"""
)
)
self.db.execute(
text(
"""
INSERT INTO sys_user
(id, username, password_hash, employee_id, dept_id, nickname, is_super_admin, status, created_at, updated_at)
VALUES
(100, '13900000000', 'old', 100, 100, '旧管理员', 0, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
"""
)
)
self.db.execute(
text(
"""
INSERT INTO personnel (phone, name, is_temporary, created_at, updated_at)
VALUES ('13900000000', '旧小程序用户', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
"""
)
)
self.db.execute(
text(
"""
INSERT INTO md_customer
(id, customer_code, customer_name, status, created_at, updated_at)
VALUES
(100, 'C_OLD', '旧客户', 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
"""
)
)
self.db.commit()
def test_reset_requires_confirm_reset_and_does_not_clear_data(self) -> None:
from app.services.system_initializer import reset_existing_database
self._seed_dirty_data()
with self.assertRaisesRegex(ValueError, "confirm-reset"):
reset_existing_database(self.db, _options(confirm_reset=False))
self.db.rollback()
self.assertEqual(self.db.execute(text("SELECT COUNT(*) FROM sys_user")).scalar_one(), 1)
self.assertEqual(self.db.execute(text("SELECT COUNT(*) FROM md_customer")).scalar_one(), 1)
def test_reset_clears_business_account_and_miniapp_data_then_recreates_admin(self) -> None:
from app.services.system_initializer import reset_existing_database
self._seed_dirty_data()
result = reset_existing_database(self.db, _options(confirm_reset=True))
self.db.commit()
self.assertEqual(result.mode, "reset")
self.assertEqual(result.counts_before["sys_user"], 1)
self.assertEqual(result.counts_before["md_customer"], 1)
self.assertEqual(result.counts_before["personnel"], 1)
self.assertEqual(result.counts_after["md_customer"], 0)
self.assertEqual(result.counts_after["sys_user"], 1)
self.assertEqual(result.counts_after["personnel"], 1)
self.assertEqual(result.seeded["admin_username"], "13800000000")
self.assertEqual(self.db.execute(text("SELECT COUNT(*) FROM md_customer")).scalar_one(), 0)
self.assertIsNone(self.db.scalar(select(User).where(User.username == "13900000000")))
admin = self.db.scalar(select(User).where(User.username == "13800000000"))
self.assertIsNotNone(admin)
assert admin is not None
self.assertEqual(admin.is_super_admin, 1)
self.assertTrue(verify_password("secret123", admin.password_hash))
def test_reset_keeps_sqlite_foreign_keys_enabled(self) -> None:
from app.services.system_initializer import reset_existing_database
self.db.execute(text("PRAGMA foreign_keys = ON"))
self._seed_dirty_data()
reset_existing_database(self.db, _options(confirm_reset=True))
self.db.commit()
self.assertEqual(self.db.execute(text("PRAGMA foreign_keys")).scalar_one(), 1)
def test_write_summary_report_writes_expected_json(self) -> None:
from app.services.system_initializer import SystemInitializeResult, write_summary_report
result = SystemInitializeResult(
mode="reset",
database="erp_test",
backup_path="/tmp/backup.sql",
counts_before={"sys_user": 2},
counts_after={"sys_user": 1},
seeded={"admin_username": "13800000000"},
)
with tempfile.TemporaryDirectory() as tmpdir:
summary_path = write_summary_report(Path(tmpdir), result)
payload = json.loads(Path(summary_path).read_text(encoding="utf-8"))
self.assertEqual(payload["mode"], "reset")
self.assertEqual(payload["database"], "erp_test")
self.assertEqual(payload["backup_path"], "/tmp/backup.sql")
self.assertEqual(payload["summary_path"], summary_path)
self.assertEqual(payload["counts_before"], {"sys_user": 2})
self.assertEqual(payload["counts_after"], {"sys_user": 1})
self.assertEqual(payload["seeded"], {"admin_username": "13800000000"})
self.assertRegex(payload["created_at"], r"^\d{4}-\d{2}-\d{2}T")
def test_default_output_dir_uses_cleanup_backups_folder(self) -> None:
from app.services.system_initializer import default_output_dir
self.assertEqual(default_output_dir().name, "cleanup_backups")
self.assertEqual(default_output_dir().parent.name, "outputs")
def test_backup_mysql_database_uses_defaults_file_without_password_argument(self) -> None:
from app.services.system_initializer import backup_mysql_database
class Settings:
mysql_host = "127.0.0.1"
mysql_port = 3306
mysql_user = "root"
mysql_password = "secret"
mysql_database = "jiaheng_erp"
commands: list[list[str]] = []
def fake_run(command: list[str], check: bool) -> None:
commands.append(command)
defaults_arg = next(part for part in command if part.startswith("--defaults-extra-file="))
defaults_path = Path(defaults_arg.split("=", 1)[1])
self.assertTrue(defaults_path.exists())
self.assertIn("password=secret", defaults_path.read_text(encoding="utf-8"))
self.assertEqual(defaults_path.stat().st_mode & 0o777, 0o600)
with tempfile.TemporaryDirectory() as tmpdir:
with patch("app.services.system_initializer.subprocess.run", side_effect=fake_run):
backup_path = backup_mysql_database(Settings(), tmpdir)
self.assertTrue(backup_path.endswith(".sql"))
self.assertEqual(len(commands), 1)
self.assertFalse(any("secret" in part for part in commands[0]))
defaults_arg = next(part for part in commands[0] if part.startswith("--defaults-extra-file="))
self.assertFalse(Path(defaults_arg.split("=", 1)[1]).exists())
class SystemInitializerFreshTest(unittest.TestCase):
def test_fresh_create_all_builds_key_tables_and_seeds_skeleton(self) -> None:
from app.services.system_initializer import fresh_initialize_database
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
result = fresh_initialize_database(engine, _options(mode="fresh"))
created_tables = set(inspect(engine).get_table_names())
self.assertIn("sys_user", created_tables)
self.assertIn("md_customer", created_tables)
self.assertIn("attendance_points", created_tables)
self.assertEqual(result.mode, "fresh")
self.assertEqual(result.counts_after["sys_user"], 1)
self.assertEqual(result.counts_after["wh_warehouse"], 6)
self.assertEqual(result.seeded["admin_username"], "13800000000")
with Session(engine, future=True) as db:
self.assertIsNotNone(db.scalar(select(User).where(User.username == "13800000000")))
class SystemInitializerFileCleanupTest(unittest.TestCase):
def test_delete_managed_files_removes_only_children_and_keeps_directories(self) -> None:
from app.services.system_initializer import delete_managed_files
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
archive_dir = root / "document_archives"
photo_dir = root / "uploads" / "logistics"
nested_dir = archive_dir / "batch"
archive_dir.mkdir(parents=True)
photo_dir.mkdir(parents=True)
nested_dir.mkdir()
(archive_dir / "a.pdf").write_text("pdf", encoding="utf-8")
(nested_dir / "nested.pdf").write_text("nested", encoding="utf-8")
(photo_dir / "b.png").write_text("png", encoding="utf-8")
summary = delete_managed_files([archive_dir, photo_dir])
self.assertEqual(summary["deleted_file_count"], 3)
self.assertEqual(summary["deleted_dir_count"], 1)
self.assertTrue(archive_dir.exists())
self.assertTrue(photo_dir.exists())
self.assertEqual(list(archive_dir.iterdir()), [])
self.assertEqual(list(photo_dir.iterdir()), [])
def test_reset_with_backup_skips_file_cleanup_by_default(self) -> None:
from app.services import system_initializer
from app.services.system_initializer import SystemInitializeResult, reset_existing_database_with_backup
class Settings:
mysql_database = "erp_test"
reset_result = SystemInitializeResult(
mode="reset",
counts_before={"sys_user": 2},
counts_after={"sys_user": 1},
seeded={"admin_username": "13800000000"},
)
with tempfile.TemporaryDirectory() as tmpdir:
with (
patch.object(system_initializer, "backup_mysql_database", return_value="/tmp/backup.sql"),
patch.object(system_initializer, "reset_existing_database", return_value=reset_result),
patch.object(system_initializer, "delete_managed_files") as delete_mock,
patch.object(system_initializer, "Session") as session_mock,
):
session_mock.return_value.__enter__.return_value = MagicMock()
result = reset_existing_database_with_backup(
engine=object(),
settings=Settings(),
options=_options(confirm_reset=True),
output_dir=tmpdir,
)
delete_mock.assert_not_called()
self.assertNotIn("deleted_file_count", result.seeded)
def test_reset_with_backup_includes_file_cleanup_when_requested(self) -> None:
from app.services import system_initializer
from app.services.system_initializer import SystemInitializeResult, SystemInitializeOptions, reset_existing_database_with_backup
class Settings:
mysql_database = "erp_test"
reset_result = SystemInitializeResult(
mode="reset",
counts_before={"sys_user": 2},
counts_after={"sys_user": 1},
seeded={"admin_username": "13800000000"},
)
options = SystemInitializeOptions(
mode="reset",
company_name="百华",
admin_name="超级管理员",
admin_phone="13800000000",
admin_password="secret123",
confirm_reset=True,
delete_files=True,
)
with tempfile.TemporaryDirectory() as tmpdir:
with (
patch.object(system_initializer, "backup_mysql_database", return_value="/tmp/backup.sql"),
patch.object(system_initializer, "reset_existing_database", return_value=reset_result),
patch.object(
system_initializer,
"delete_managed_files",
return_value={"deleted_file_count": 3, "deleted_dir_count": 2},
) as delete_mock,
patch.object(system_initializer, "Session") as session_mock,
):
session_mock.return_value.__enter__.return_value = MagicMock()
result = reset_existing_database_with_backup(
engine=object(),
settings=Settings(),
options=options,
output_dir=tmpdir,
)
delete_mock.assert_called_once()
self.assertEqual(result.seeded["deleted_file_count"], 3)
self.assertEqual(result.seeded["deleted_dir_count"], 2)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,177 @@
from __future__ import annotations
import unittest
from sqlalchemy import create_engine, select, text
from sqlalchemy.orm import Session, sessionmaker
import app.models # noqa: E402,F401
from app.models.base import Base # noqa: E402
from app.models.master_data import Warehouse # noqa: E402
from app.models.miniapp import MiniAppPersonAttendancePoint, MiniAppPersonnel, MiniAppPersonRole # noqa: E402
from app.models.operations import WarehouseLocation # noqa: E402
from app.models.org import AiAssistantConfig, Department, Employee, Permission, Role, RolePermission, SystemConfig, User, UserRole # noqa: E402
from app.services.auth import verify_password # noqa: E402
from app.services.system_permissions import MENU_PERMISSION_TREE # noqa: E402
class SystemInitializerSeedTest(unittest.TestCase):
def test_table_groups_have_no_duplicates_and_include_expected_tables(self) -> None:
from app.services.system_initializer import (
ACCOUNT_RESET_TABLES,
BUSINESS_RESET_TABLES,
MINIAPP_RESET_TABLES,
RESET_TABLES_IN_DELETE_ORDER,
SKELETON_TABLES,
)
all_tables = BUSINESS_RESET_TABLES + MINIAPP_RESET_TABLES + ACCOUNT_RESET_TABLES + SKELETON_TABLES
reset_tables = BUSINESS_RESET_TABLES + MINIAPP_RESET_TABLES + ACCOUNT_RESET_TABLES
self.assertEqual(len(all_tables), len(set(all_tables)))
self.assertEqual(set(reset_tables).issubset(set(RESET_TABLES_IN_DELETE_ORDER)), True)
self.assertEqual(len(RESET_TABLES_IN_DELETE_ORDER), len(set(RESET_TABLES_IN_DELETE_ORDER)))
self.assertIn("so_sales_order", BUSINESS_RESET_TABLES)
self.assertIn("wh_stock_lot", BUSINESS_RESET_TABLES)
self.assertIn("pp_production_batch_ledger", BUSINESS_RESET_TABLES)
self.assertIn("production_reports", MINIAPP_RESET_TABLES)
self.assertIn("work_sessions", MINIAPP_RESET_TABLES)
self.assertIn("sys_user", ACCOUNT_RESET_TABLES)
self.assertIn("sys_permission", SKELETON_TABLES)
self.assertIn("wh_warehouse", SKELETON_TABLES)
class SystemInitializerSkeletonSeedTest(unittest.TestCase):
def setUp(self) -> None:
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
self.SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
self.db: Session = self.SessionLocal()
def tearDown(self) -> None:
self.db.close()
def test_seed_system_skeleton_creates_required_defaults(self) -> None:
from app.services.system_initializer import SystemInitializeOptions, seed_system_skeleton
options = SystemInitializeOptions(
mode="reset",
company_name="百华",
admin_name="超级管理员",
admin_phone="13800000000",
admin_password="secret123",
smart_operation_report_enabled=False,
confirm_reset=True,
)
result = seed_system_skeleton(self.db, options)
self.db.commit()
admin_user = self.db.scalar(select(User).where(User.username == "13800000000"))
self.assertIsNotNone(admin_user)
assert admin_user is not None
self.assertEqual(admin_user.is_super_admin, 1)
self.assertTrue(verify_password("secret123", admin_user.password_hash))
admin_employee = self.db.scalar(select(Employee).where(Employee.mobile == "13800000000"))
self.assertIsNotNone(admin_employee)
assert admin_employee is not None
self.assertEqual(admin_employee.employee_name, "超级管理员")
root = self.db.scalar(select(Department).where(Department.dept_code == "ORG_ROOT"))
self.assertIsNotNone(root)
assert root is not None
self.assertEqual(root.dept_name, "百华")
expected_permission_codes = set()
def walk(nodes: list[tuple[str, str, list]]) -> None:
for code, _name, children in nodes:
expected_permission_codes.add(code)
walk(children)
walk(MENU_PERMISSION_TREE)
actual_permission_codes = set(self.db.scalars(select(Permission.permission_code)).all())
self.assertEqual(expected_permission_codes, actual_permission_codes)
admin_role = self.db.scalar(select(Role).where(Role.role_code == "ADMIN"))
self.assertIsNotNone(admin_role)
assert admin_role is not None
self.assertEqual(
self.db.query(RolePermission).filter(RolePermission.role_id == admin_role.id).count(),
len(expected_permission_codes),
)
role_names = {role.role_code: role.role_name for role in self.db.scalars(select(Role)).all()}
self.assertEqual(
role_names,
{
"ADMIN": "超级管理员",
"PURCHASER": "采购专员",
"WAREHOUSE": "仓库管理人员",
"SALES": "销售人员",
},
)
self.assertEqual(self.db.query(UserRole).count(), 1)
self.assertEqual(self.db.query(Warehouse).count(), 6)
self.assertEqual(self.db.query(WarehouseLocation).count(), 6)
self.assertEqual({row.warehouse_type for row in self.db.scalars(select(Warehouse)).all()}, {"RAW", "SEMI", "FINISHED", "AUX", "SCRAP", "RETURN"})
self.assertEqual(self.db.query(WarehouseLocation).filter(WarehouseLocation.is_default == 1).count(), 6)
prefix = self.db.scalar(select(SystemConfig).where(SystemConfig.config_code == "RAW_MATERIAL_LOT_PREFIX"))
self.assertIsNotNone(prefix)
assert prefix is not None
self.assertEqual(prefix.config_value, "YL")
smart = self.db.scalar(select(SystemConfig).where(SystemConfig.config_code == "SMART_OPERATION_REPORT_ENABLED"))
self.assertIsNotNone(smart)
assert smart is not None
self.assertEqual(smart.config_value, "关闭")
ai_config = self.db.scalar(select(AiAssistantConfig))
self.assertIsNotNone(ai_config)
assert ai_config is not None
self.assertEqual(ai_config.assistant_name, "百华工艺助手")
units = dict(self.db.execute(text("SELECT unit_code, unit_name FROM md_unit")).all())
self.assertEqual(units, {"KG": "kg", "PCS": "件", "SET": "套"})
categories = dict(self.db.execute(text("SELECT category_code, category_name FROM md_item_category")).all())
self.assertEqual(categories, {"RAW": "原材料", "SEMI": "半成品", "FINISHED": "成品", "AUX": "辅料", "SCRAP": "废料"})
miniapp_admin = self.db.get(MiniAppPersonnel, "13800000000")
self.assertIsNotNone(miniapp_admin)
assert miniapp_admin is not None
self.assertEqual(miniapp_admin.name, "超级管理员")
self.assertIsNotNone(self.db.get(MiniAppPersonRole, {"phone": "13800000000", "role": "admin"}))
self.assertIsNotNone(
self.db.get(
MiniAppPersonAttendancePoint,
{"phone": "13800000000", "attendance_point_name": "百华"},
)
)
self.assertEqual(result.seeded["warehouse_count"], 6)
self.assertEqual(result.seeded["admin_username"], "13800000000")
def test_seed_system_skeleton_defaults_smart_operation_report_to_enabled(self) -> None:
from app.services.system_initializer import SystemInitializeOptions, seed_system_skeleton
options = SystemInitializeOptions(
mode="fresh",
company_name="百华",
admin_name="超级管理员",
admin_phone="13800000000",
admin_password="secret123",
)
result = seed_system_skeleton(self.db, options)
self.db.commit()
smart = self.db.scalar(select(SystemConfig).where(SystemConfig.config_code == "SMART_OPERATION_REPORT_ENABLED"))
self.assertIsNotNone(smart)
assert smart is not None
self.assertEqual(smart.config_value, "开启")
self.assertEqual(result.seeded["smart_operation_report_enabled"], True)
if __name__ == "__main__":
unittest.main()