chore: initialize mini program backend
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
from collections import defaultdict
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import SessionLocal # noqa: E402
|
||||
from app.models import Product # noqa: E402
|
||||
|
||||
|
||||
def ratio(name: str, salt: str) -> float:
|
||||
digest = sha256(f"{name}|{salt}".encode("utf-8")).hexdigest()
|
||||
return int(digest[:8], 16) / 0xFFFFFFFF
|
||||
|
||||
|
||||
def generated_values(product_name: str) -> tuple[float, float, float, float]:
|
||||
name = product_name or ""
|
||||
if "纵梁" in name and "加强板" not in name:
|
||||
net = 2.6 + ratio(name, "net") * 1.8
|
||||
elif "吸能盒" in name:
|
||||
net = 0.75 + ratio(name, "net") * 0.65
|
||||
elif "加强板" in name:
|
||||
net = 0.55 + ratio(name, "net") * 0.65
|
||||
elif any(keyword in name for keyword in ("安装板", "连接板", "端板", "盖板", "封板")):
|
||||
net = 0.32 + ratio(name, "net") * 0.62
|
||||
else:
|
||||
net = 0.85 + ratio(name, "net") * 1.05
|
||||
|
||||
gross_factor = 1.08 + ratio(name, "gross") * 0.1
|
||||
scrap_rate = 0.012 + ratio(name, "scrap") * 0.016
|
||||
waste_price = 12.0 + ratio(name, "price") * 2.6
|
||||
return (
|
||||
round(net, 3),
|
||||
round(net * gross_factor, 3),
|
||||
round(scrap_rate, 4),
|
||||
round(waste_price, 2),
|
||||
)
|
||||
|
||||
|
||||
def valid_positive(value) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
number = float(value)
|
||||
return number if number > 0 else None
|
||||
|
||||
|
||||
def valid_waste_price(value) -> float | None:
|
||||
number = valid_positive(value)
|
||||
if number is None or number > 25:
|
||||
return None
|
||||
return number
|
||||
|
||||
|
||||
def group_values(product_name: str, items: list[Product]) -> tuple[float, float, float, float]:
|
||||
generated = generated_values(product_name)
|
||||
net = next((value for item in items if (value := valid_positive(item.product_net_weight_kg)) is not None), generated[0])
|
||||
gross = next((value for item in items if (value := valid_positive(item.product_gross_weight_kg)) is not None), generated[1])
|
||||
scrap_rate = next((value for item in items if (value := valid_positive(item.scrap_loss_rate)) is not None), generated[2])
|
||||
waste_price = next((value for item in items if (value := valid_waste_price(item.waste_price_yuan_per_kg)) is not None), generated[3])
|
||||
return net, gross, scrap_rate, waste_price
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with SessionLocal() as db:
|
||||
products = db.scalars(select(Product).order_by(Product.product_name, Product.process_name)).all()
|
||||
groups: dict[str, list[Product]] = defaultdict(list)
|
||||
for product in products:
|
||||
groups[product.product_name].append(product)
|
||||
|
||||
updated_rows = 0
|
||||
for product_name, items in groups.items():
|
||||
net, gross, scrap_rate, waste_price = group_values(product_name, items)
|
||||
for product in items:
|
||||
product.product_net_weight_kg = net
|
||||
product.product_gross_weight_kg = gross
|
||||
product.scrap_loss_rate = scrap_rate
|
||||
product.waste_price_yuan_per_kg = waste_price
|
||||
updated_rows += 1
|
||||
|
||||
db.commit()
|
||||
print(f"backfilled {updated_rows} product rows, {len(groups)} product names")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import SessionLocal # noqa: E402
|
||||
from app.models import PersonRole, Personnel, Role # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = ArgumentParser(description="Create or update the first manager account.")
|
||||
parser.add_argument("--phone", required=True, help="经理手机号")
|
||||
parser.add_argument("--name", required=True, help="经理姓名")
|
||||
args = parser.parse_args()
|
||||
|
||||
with SessionLocal() as db:
|
||||
person = db.get(Personnel, args.phone)
|
||||
if person is None:
|
||||
person = Personnel(phone=args.phone)
|
||||
db.add(person)
|
||||
person.name = args.name
|
||||
if db.get(PersonRole, {"phone": args.phone, "role": Role.manager}) is None:
|
||||
db.add(PersonRole(phone=args.phone, role=Role.manager))
|
||||
db.commit()
|
||||
print(f"manager ready: {args.name} {args.phone}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("CREATE TABLE IF NOT EXISTS products_legacy_device_backup LIKE products"))
|
||||
conn.execute(text("""
|
||||
INSERT IGNORE INTO products_legacy_device_backup
|
||||
SELECT *
|
||||
FROM products
|
||||
WHERE device_no <> ''
|
||||
"""))
|
||||
converted = conn.execute(text("""
|
||||
UPDATE products p
|
||||
LEFT JOIN products k
|
||||
ON k.project_no = p.project_no
|
||||
AND k.product_name = p.product_name
|
||||
AND k.process_name = p.process_name
|
||||
AND k.device_no = ''
|
||||
SET p.device_no = ''
|
||||
WHERE p.device_no <> ''
|
||||
AND k.project_no IS NULL
|
||||
""")).rowcount
|
||||
deleted = conn.execute(text("""
|
||||
DELETE p
|
||||
FROM products p
|
||||
INNER JOIN products k
|
||||
ON k.project_no = p.project_no
|
||||
AND k.product_name = p.product_name
|
||||
AND k.process_name = p.process_name
|
||||
AND k.device_no = ''
|
||||
WHERE p.device_no <> ''
|
||||
""")).rowcount
|
||||
print(
|
||||
"products legacy device rows deduped; "
|
||||
f"converted={converted}, deleted={deleted}, backup_table=products_legacy_device_backup"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
TARGET_CHARSET = "utf8mb4"
|
||||
TARGET_COLLATION = "utf8mb4_unicode_ci"
|
||||
|
||||
APP_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",
|
||||
"device_qrcodes",
|
||||
"notices",
|
||||
"notice_points",
|
||||
"reconciliation_ledger_entries",
|
||||
]
|
||||
|
||||
|
||||
def quote_identifier(value: str) -> str:
|
||||
return f"`{value.replace('`', '``')}`"
|
||||
|
||||
|
||||
def table_exists(conn, table: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
AND table_type = 'BASE TABLE'
|
||||
"""), {"table": table}).scalar_one())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
database = conn.execute(text("SELECT DATABASE()")).scalar_one()
|
||||
if database:
|
||||
conn.execute(text(
|
||||
f"ALTER DATABASE {quote_identifier(database)} "
|
||||
f"CHARACTER SET {TARGET_CHARSET} COLLATE {TARGET_COLLATION}"
|
||||
))
|
||||
|
||||
changed = []
|
||||
skipped = []
|
||||
for table in APP_TABLES:
|
||||
if not table_exists(conn, table):
|
||||
skipped.append(table)
|
||||
continue
|
||||
conn.execute(text(
|
||||
f"ALTER TABLE {quote_identifier(table)} "
|
||||
f"CONVERT TO CHARACTER SET {TARGET_CHARSET} COLLATE {TARGET_COLLATION}"
|
||||
))
|
||||
changed.append(table)
|
||||
|
||||
print(f"collation fixed: {', '.join(changed)}")
|
||||
if skipped:
|
||||
print(f"skipped missing tables: {', '.join(skipped)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,18 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import Base, engine # noqa: E402
|
||||
from app import models # noqa: F401,E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
Base.metadata.create_all(bind=engine)
|
||||
print("database tables created")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
from app.services.work_schedule import DEFAULT_WORK_SCHEDULE # noqa: E402
|
||||
|
||||
|
||||
SCHEDULE_FIELDS = [
|
||||
"day_start",
|
||||
"day_end",
|
||||
"lunch_start",
|
||||
"lunch_end",
|
||||
"dinner_start",
|
||||
"dinner_end",
|
||||
"overtime_start",
|
||||
"overtime_end",
|
||||
"night_start",
|
||||
"night_end",
|
||||
]
|
||||
|
||||
|
||||
def column_exists(conn, table: str, column: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
AND column_name = :column
|
||||
"""), {"table": table, "column": column}).scalar_one())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
for field in SCHEDULE_FIELDS:
|
||||
if column_exists(conn, "attendance_points", field):
|
||||
continue
|
||||
conn.execute(text(
|
||||
f"ALTER TABLE attendance_points "
|
||||
f"ADD COLUMN {field} VARCHAR(5) NOT NULL DEFAULT :default_value AFTER radius_meters"
|
||||
), {"default_value": DEFAULT_WORK_SCHEDULE[field]})
|
||||
|
||||
has_global_schedule = bool(conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'work_schedules'
|
||||
""")).scalar_one())
|
||||
if has_global_schedule:
|
||||
set_clause = ", ".join(
|
||||
f"ap.{field} = COALESCE(ws.{field}, :{field})"
|
||||
for field in SCHEDULE_FIELDS
|
||||
)
|
||||
conn.execute(text(f"""
|
||||
UPDATE attendance_points ap
|
||||
JOIN work_schedules ws ON ws.id = 1
|
||||
SET {set_clause}
|
||||
"""), DEFAULT_WORK_SCHEDULE)
|
||||
|
||||
for field in SCHEDULE_FIELDS:
|
||||
conn.execute(text(
|
||||
f"UPDATE attendance_points SET {field} = :default_value "
|
||||
f"WHERE {field} IS NULL OR {field} = ''"
|
||||
), {"default_value": DEFAULT_WORK_SCHEDULE[field]})
|
||||
|
||||
print("attendance point schedules migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,207 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
from app.services.attendance_points import DEFAULT_ATTENDANCE_POINT_NAME # noqa: E402
|
||||
|
||||
TARGET_COLLATION = "utf8mb4_unicode_ci"
|
||||
|
||||
|
||||
def column_exists(conn, table: str, column: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
AND column_name = :column
|
||||
"""), {"table": table, "column": column}).scalar_one())
|
||||
|
||||
|
||||
def table_exists(conn, table: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
"""), {"table": table}).scalar_one())
|
||||
|
||||
|
||||
def primary_columns(conn, table: str) -> list[str]:
|
||||
rows = conn.execute(text("""
|
||||
SELECT column_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
AND constraint_name = 'PRIMARY'
|
||||
ORDER BY ordinal_position
|
||||
"""), {"table": table}).all()
|
||||
return [row[0] for row in rows]
|
||||
|
||||
|
||||
def index_exists(conn, table: str, index_name: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table
|
||||
AND index_name = :index_name
|
||||
"""), {"table": table, "index_name": index_name}).scalar_one())
|
||||
|
||||
|
||||
def create_index(conn, table: str, index_name: str, columns: str) -> None:
|
||||
if not index_exists(conn, table, index_name):
|
||||
conn.execute(text(f"CREATE INDEX {index_name} ON {table} ({columns})"))
|
||||
|
||||
|
||||
def add_point_column(conn, table: str, after: str | None = None) -> None:
|
||||
if column_exists(conn, table, "attendance_point_name"):
|
||||
return
|
||||
after_clause = f" AFTER {after}" if after else ""
|
||||
conn.execute(text(
|
||||
f"ALTER TABLE {table} "
|
||||
f"ADD COLUMN attendance_point_name VARCHAR(128) COLLATE {TARGET_COLLATION} NOT NULL DEFAULT :default_name{after_clause}"
|
||||
), {"default_name": DEFAULT_ATTENDANCE_POINT_NAME})
|
||||
|
||||
|
||||
def ensure_primary(conn, table: str, columns: list[str]) -> None:
|
||||
if primary_columns(conn, table) == columns:
|
||||
return
|
||||
if primary_columns(conn, table):
|
||||
conn.execute(text(f"ALTER TABLE {table} DROP PRIMARY KEY"))
|
||||
joined = ", ".join(columns)
|
||||
conn.execute(text(f"ALTER TABLE {table} ADD PRIMARY KEY ({joined})"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS attendance_points (
|
||||
name VARCHAR(128) NOT NULL PRIMARY KEY,
|
||||
latitude DECIMAL(10, 7) NULL,
|
||||
longitude DECIMAL(10, 7) NULL,
|
||||
radius_meters INT NOT NULL DEFAULT 500,
|
||||
day_start VARCHAR(5) NOT NULL DEFAULT '08:00',
|
||||
day_end VARCHAR(5) NOT NULL DEFAULT '17:20',
|
||||
lunch_start VARCHAR(5) NOT NULL DEFAULT '11:40',
|
||||
lunch_end VARCHAR(5) NOT NULL DEFAULT '12:40',
|
||||
dinner_start VARCHAR(5) NOT NULL DEFAULT '17:20',
|
||||
dinner_end VARCHAR(5) NOT NULL DEFAULT '18:00',
|
||||
overtime_start VARCHAR(5) NOT NULL DEFAULT '18:00',
|
||||
overtime_end VARCHAR(5) NOT NULL DEFAULT '20:00',
|
||||
night_start VARCHAR(5) NOT NULL DEFAULT '20:00',
|
||||
night_end VARCHAR(5) NOT NULL DEFAULT '06:00',
|
||||
remark VARCHAR(500) NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
INSERT IGNORE INTO attendance_points (
|
||||
name, latitude, longitude, radius_meters, remark, is_active
|
||||
)
|
||||
SELECT
|
||||
:name,
|
||||
attendance_latitude,
|
||||
attendance_longitude,
|
||||
COALESCE(attendance_radius_meters, 500),
|
||||
'默认考勤点',
|
||||
TRUE
|
||||
FROM work_schedules
|
||||
WHERE id = 1
|
||||
"""), {"name": DEFAULT_ATTENDANCE_POINT_NAME})
|
||||
conn.execute(text("""
|
||||
INSERT IGNORE INTO attendance_points (name, radius_meters, remark, is_active)
|
||||
VALUES (:name, 500, '默认考勤点', TRUE)
|
||||
"""), {"name": DEFAULT_ATTENDANCE_POINT_NAME})
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS person_attendance_points (
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
attendance_point_name VARCHAR(128) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (phone, attendance_point_name),
|
||||
INDEX idx_person_points_point (attendance_point_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
INSERT IGNORE INTO person_attendance_points (phone, attendance_point_name)
|
||||
SELECT phone, :name FROM personnel
|
||||
"""), {"name": DEFAULT_ATTENDANCE_POINT_NAME})
|
||||
|
||||
if table_exists(conn, "products"):
|
||||
add_point_column(conn, "products")
|
||||
ensure_primary(conn, "products", [
|
||||
"attendance_point_name",
|
||||
"project_no",
|
||||
"product_name",
|
||||
"device_no",
|
||||
"process_name",
|
||||
])
|
||||
create_index(conn, "products", "idx_products_point", "attendance_point_name")
|
||||
|
||||
if table_exists(conn, "equipment"):
|
||||
add_point_column(conn, "equipment")
|
||||
ensure_primary(conn, "equipment", ["attendance_point_name", "device_no"])
|
||||
create_index(conn, "equipment", "idx_equipment_point", "attendance_point_name")
|
||||
|
||||
if table_exists(conn, "work_sessions"):
|
||||
add_point_column(conn, "work_sessions", "id")
|
||||
create_index(conn, "work_sessions", "idx_sessions_point_status", "attendance_point_name, status")
|
||||
|
||||
if table_exists(conn, "work_session_devices"):
|
||||
add_point_column(conn, "work_session_devices", "session_id")
|
||||
create_index(conn, "work_session_devices", "idx_session_devices_point", "attendance_point_name")
|
||||
|
||||
if table_exists(conn, "production_reports"):
|
||||
add_point_column(conn, "production_reports", "id")
|
||||
create_index(conn, "production_reports", "idx_reports_point_status", "attendance_point_name, status, report_date")
|
||||
|
||||
if table_exists(conn, "production_report_items"):
|
||||
add_point_column(conn, "production_report_items", "report_id")
|
||||
conn.execute(text("""
|
||||
UPDATE production_report_items item
|
||||
JOIN production_reports report ON report.id = item.report_id
|
||||
SET item.attendance_point_name = report.attendance_point_name
|
||||
WHERE item.attendance_point_name = ''
|
||||
OR item.attendance_point_name IS NULL
|
||||
"""))
|
||||
create_index(conn, "production_report_items", "idx_report_items_point_product", "attendance_point_name, project_no, product_name")
|
||||
|
||||
if table_exists(conn, "device_qrcodes"):
|
||||
add_point_column(conn, "device_qrcodes")
|
||||
ensure_primary(conn, "device_qrcodes", ["attendance_point_name", "device_no", "process_name"])
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS notice_points (
|
||||
notice_id BIGINT NOT NULL,
|
||||
attendance_point_name VARCHAR(128) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (notice_id, attendance_point_name),
|
||||
INDEX idx_notice_points_point (attendance_point_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
INSERT IGNORE INTO notice_points (notice_id, attendance_point_name)
|
||||
SELECT id, :name FROM notices
|
||||
"""), {"name": DEFAULT_ATTENDANCE_POINT_NAME})
|
||||
|
||||
if table_exists(conn, "reconciliation_ledger_entries"):
|
||||
add_point_column(conn, "reconciliation_ledger_entries")
|
||||
ensure_primary(conn, "reconciliation_ledger_entries", [
|
||||
"attendance_point_name",
|
||||
"year",
|
||||
"month",
|
||||
"product_name",
|
||||
])
|
||||
|
||||
print("attendance points migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def _column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"table_name": table_name, "column_name": column_name},
|
||||
).scalar_one())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not _column_exists(conn, "production_reports", "is_system_auto_submitted"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN is_system_auto_submitted TINYINT(1) NOT NULL DEFAULT 0 AFTER submitted_at
|
||||
"""))
|
||||
if not _column_exists(conn, "production_reports", "auto_submit_reason"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN auto_submit_reason VARCHAR(120) NULL AFTER is_system_auto_submitted
|
||||
"""))
|
||||
print("auto submitted report columns migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("ALTER TABLE production_report_items MODIFY device_no VARCHAR(255) NOT NULL"))
|
||||
print("production_report_items.device_no migrated to varchar(255)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,128 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from sqlalchemy import select, text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import SessionLocal, engine # noqa: E402
|
||||
from app.models import DeviceQRCode, Product, ProductionReportItem, WorkSessionDevice # noqa: E402
|
||||
from app.services.cleaning import CLEANING_STAMPING_METHOD, LEGACY_CLEANING_PROCESS_NAME # noqa: E402
|
||||
|
||||
|
||||
def _column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"table_name": table_name, "column_name": column_name},
|
||||
).scalar_one())
|
||||
|
||||
|
||||
def _sequence_number(process_name: str) -> int | None:
|
||||
matched = re.search(r"\d+", str(process_name or ""))
|
||||
return int(matched.group(0)) if matched else None
|
||||
|
||||
|
||||
def _next_process_name(products: list[Product]) -> str:
|
||||
existing = {str(product.process_name or "").strip() for product in products}
|
||||
max_number = max(
|
||||
(number for number in (_sequence_number(value) for value in existing) if number is not None),
|
||||
default=0,
|
||||
)
|
||||
number = max_number + 1
|
||||
while f"{number}序" in existing:
|
||||
number += 1
|
||||
return f"{number}序"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not _column_exists(conn, "production_report_items", "stamping_method"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_report_items
|
||||
ADD COLUMN stamping_method VARCHAR(128) NULL AFTER process_name
|
||||
"""))
|
||||
|
||||
with SessionLocal() as db:
|
||||
cleaning_products = db.scalars(
|
||||
select(Product).where(Product.process_name == LEGACY_CLEANING_PROCESS_NAME)
|
||||
).all()
|
||||
migrated_products = 0
|
||||
for product in cleaning_products:
|
||||
old_process_name = product.process_name
|
||||
siblings = db.scalars(
|
||||
select(Product).where(
|
||||
Product.project_no == product.project_no,
|
||||
Product.product_name == product.product_name,
|
||||
Product.device_no == product.device_no,
|
||||
)
|
||||
).all()
|
||||
next_process_name = _next_process_name(siblings)
|
||||
|
||||
old_qr = db.get(DeviceQRCode, {
|
||||
"device_no": product.product_name,
|
||||
"process_name": old_process_name,
|
||||
})
|
||||
if old_qr is not None:
|
||||
old_qr.process_name = next_process_name
|
||||
|
||||
for session_device in db.scalars(
|
||||
select(WorkSessionDevice).where(
|
||||
WorkSessionDevice.device_no == product.product_name,
|
||||
WorkSessionDevice.process_name == old_process_name,
|
||||
)
|
||||
).all():
|
||||
session_device.process_name = next_process_name
|
||||
|
||||
for report_item in db.scalars(
|
||||
select(ProductionReportItem).where(
|
||||
ProductionReportItem.project_no == product.project_no,
|
||||
ProductionReportItem.product_name == product.product_name,
|
||||
ProductionReportItem.process_name == old_process_name,
|
||||
)
|
||||
).all():
|
||||
report_item.process_name = next_process_name
|
||||
report_item.stamping_method = CLEANING_STAMPING_METHOD
|
||||
report_item.standard_beat = 0
|
||||
report_item.standard_workload = 0
|
||||
report_item.allocated_minutes = 0
|
||||
|
||||
product.process_name = next_process_name
|
||||
product.stamping_method = CLEANING_STAMPING_METHOD
|
||||
product.standard_beat = 0
|
||||
product.standard_workload = 0
|
||||
migrated_products += 1
|
||||
|
||||
for product in db.scalars(
|
||||
select(Product).where(Product.stamping_method == CLEANING_STAMPING_METHOD)
|
||||
).all():
|
||||
product.standard_beat = 0
|
||||
product.standard_workload = 0
|
||||
|
||||
for report_item in db.scalars(select(ProductionReportItem)).all():
|
||||
if report_item.stamping_method:
|
||||
continue
|
||||
product = db.scalar(
|
||||
select(Product).where(
|
||||
Product.project_no == report_item.project_no,
|
||||
Product.product_name == report_item.product_name,
|
||||
Product.process_name == (report_item.process_name or ""),
|
||||
Product.device_no == "",
|
||||
)
|
||||
)
|
||||
if product is not None:
|
||||
report_item.stamping_method = product.stamping_method
|
||||
|
||||
db.commit()
|
||||
print(f"cleaning migrated to stamping_method, products={migrated_products}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.database import engine
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
exists = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'production_report_items'
|
||||
AND column_name = 'changeover_count'
|
||||
"""
|
||||
)
|
||||
).scalar()
|
||||
if not exists:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
ALTER TABLE production_report_items
|
||||
ADD COLUMN changeover_count DECIMAL(12, 2) NOT NULL DEFAULT 0
|
||||
AFTER process_unit_price_yuan
|
||||
"""
|
||||
)
|
||||
)
|
||||
print("migrate_continuous_die_changeover_count done")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS equipment (
|
||||
device_no VARCHAR(64) NOT NULL PRIMARY KEY,
|
||||
remark VARCHAR(500) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
INSERT IGNORE INTO equipment (device_no, remark)
|
||||
SELECT DISTINCT device_no, '由历史产品清单带入'
|
||||
FROM products
|
||||
WHERE device_no IS NOT NULL AND device_no <> ''
|
||||
"""))
|
||||
print("equipment migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
exists = conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'equipment'
|
||||
AND column_name = 'device_type'
|
||||
""")).scalar()
|
||||
if not exists:
|
||||
conn.execute(text("""
|
||||
ALTER TABLE equipment
|
||||
ADD COLUMN device_type VARCHAR(32) NOT NULL DEFAULT '冲压设备' AFTER device_no
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
UPDATE equipment
|
||||
SET device_type = '冲压设备'
|
||||
WHERE device_type IS NULL OR device_type = ''
|
||||
"""))
|
||||
print("equipment device type migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,51 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import select, text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import SessionLocal, engine # noqa: E402
|
||||
from app.models import AttendancePoint # noqa: E402
|
||||
from app.services.misc_work import ensure_misc_work_product # noqa: E402
|
||||
|
||||
|
||||
def _column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
dialect = conn.dialect.name
|
||||
if dialect == "mysql":
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"table_name": table_name, "column_name": column_name},
|
||||
).scalar_one())
|
||||
rows = conn.execute(text(f"PRAGMA table_info({table_name})")).all()
|
||||
return any(row[1] == column_name for row in rows)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not _column_exists(conn, "production_report_items", "remark"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_report_items
|
||||
ADD COLUMN remark VARCHAR(500) NULL
|
||||
"""))
|
||||
|
||||
with SessionLocal() as db:
|
||||
points = db.scalars(
|
||||
select(AttendancePoint).where(AttendancePoint.is_active.is_(True))
|
||||
).all()
|
||||
for point in points:
|
||||
ensure_misc_work_product(db, point.name)
|
||||
db.commit()
|
||||
|
||||
print(f"misc work migrated, attendance_points={len(points)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
result = conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE work_session_devices d
|
||||
JOIN work_sessions s ON s.id = d.session_id
|
||||
SET d.released_at = NULL,
|
||||
d.released_by = NULL,
|
||||
d.release_reason = NULL
|
||||
WHERE s.status = 'reporting'
|
||||
AND d.release_reason IN ('下班释放占用', '历史非活动记录迁移释放占用')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM production_reports r
|
||||
WHERE r.session_id = s.id
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
print(f"restored reporting mold locks: {result.rowcount}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,124 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
return bool(
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""
|
||||
),
|
||||
{"table_name": table_name, "column_name": column_name},
|
||||
).scalar()
|
||||
)
|
||||
|
||||
|
||||
def index_exists(conn, table_name: str, index_name: str) -> bool:
|
||||
return bool(
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND index_name = :index_name
|
||||
"""
|
||||
),
|
||||
{"table_name": table_name, "index_name": index_name},
|
||||
).scalar()
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not column_exists(conn, "work_session_devices", "released_at"):
|
||||
conn.execute(text("ALTER TABLE work_session_devices ADD COLUMN released_at DATETIME NULL AFTER sort_order"))
|
||||
if not column_exists(conn, "work_session_devices", "released_by"):
|
||||
conn.execute(text("ALTER TABLE work_session_devices ADD COLUMN released_by VARCHAR(20) NULL AFTER released_at"))
|
||||
if not column_exists(conn, "work_session_devices", "release_reason"):
|
||||
conn.execute(text("ALTER TABLE work_session_devices ADD COLUMN release_reason VARCHAR(120) NULL AFTER released_by"))
|
||||
if not index_exists(conn, "work_session_devices", "idx_session_devices_release"):
|
||||
conn.execute(text("CREATE INDEX idx_session_devices_release ON work_session_devices (released_at)"))
|
||||
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mold_lock_feedbacks (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
attendance_point_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
mold_name VARCHAR(255) NOT NULL,
|
||||
process_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
session_device_id BIGINT NOT NULL,
|
||||
reporter_phone VARCHAR(20) NOT NULL,
|
||||
occupied_phone VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
read_at DATETIME NULL,
|
||||
handled_at DATETIME NULL,
|
||||
handled_by VARCHAR(20) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_mold_lock_feedback_point_read (attendance_point_name, read_at, created_at),
|
||||
KEY idx_mold_lock_feedback_status (status, created_at),
|
||||
KEY idx_mold_lock_feedback_device (session_device_id),
|
||||
CONSTRAINT fk_mold_lock_feedback_device
|
||||
FOREIGN KEY (session_device_id) REFERENCES work_session_devices (id),
|
||||
CONSTRAINT fk_mold_lock_feedback_reporter
|
||||
FOREIGN KEY (reporter_phone) REFERENCES personnel (phone),
|
||||
CONSTRAINT fk_mold_lock_feedback_occupied
|
||||
FOREIGN KEY (occupied_phone) REFERENCES personnel (phone),
|
||||
CONSTRAINT fk_mold_lock_feedback_handler
|
||||
FOREIGN KEY (handled_by) REFERENCES personnel (phone)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE work_session_devices d
|
||||
JOIN work_sessions s ON s.id = d.session_id
|
||||
SET d.released_at = COALESCE(s.end_at, s.updated_at, NOW()),
|
||||
d.release_reason = '历史非活动记录迁移释放占用'
|
||||
WHERE d.released_at IS NULL
|
||||
AND s.status NOT IN ('active', 'reporting')
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE work_session_devices d
|
||||
JOIN work_sessions s ON s.id = d.session_id
|
||||
JOIN (
|
||||
SELECT session_id, MAX(sort_order) AS max_sort_order
|
||||
FROM work_session_devices
|
||||
GROUP BY session_id
|
||||
) latest ON latest.session_id = d.session_id
|
||||
SET d.released_at = NOW(),
|
||||
d.release_reason = '历史切换记录迁移释放占用'
|
||||
WHERE d.released_at IS NULL
|
||||
AND s.status = 'active'
|
||||
AND d.sort_order < latest.max_sort_order
|
||||
"""
|
||||
)
|
||||
)
|
||||
print("mold lock columns and feedback table migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def _column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""), {"table_name": table_name, "column_name": column_name}).scalar())
|
||||
|
||||
|
||||
def _primary_columns(conn, table_name: str) -> list[str]:
|
||||
rows = conn.execute(text("""
|
||||
SELECT column_name
|
||||
FROM information_schema.key_column_usage
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND constraint_name = 'PRIMARY'
|
||||
ORDER BY ordinal_position
|
||||
"""), {"table_name": table_name}).all()
|
||||
return [row[0] for row in rows]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not _column_exists(conn, "work_session_devices", "process_name"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE work_session_devices
|
||||
ADD COLUMN process_name VARCHAR(128) NOT NULL DEFAULT '' AFTER device_no
|
||||
"""))
|
||||
|
||||
if not _column_exists(conn, "device_qrcodes", "process_name"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE device_qrcodes
|
||||
ADD COLUMN process_name VARCHAR(128) NOT NULL DEFAULT '' AFTER device_no
|
||||
"""))
|
||||
|
||||
primary_columns = _primary_columns(conn, "device_qrcodes")
|
||||
if primary_columns != ["device_no", "process_name"]:
|
||||
conn.execute(text("ALTER TABLE device_qrcodes DROP PRIMARY KEY"))
|
||||
conn.execute(text("ALTER TABLE device_qrcodes ADD PRIMARY KEY (device_no, process_name)"))
|
||||
|
||||
print("mold process qrcode migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("ALTER TABLE work_session_devices MODIFY device_no VARCHAR(255) NOT NULL"))
|
||||
conn.execute(text("ALTER TABLE device_qrcodes MODIFY device_no VARCHAR(255) NOT NULL"))
|
||||
print("mold qrcode fields migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def _column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"table_name": table_name, "column_name": column_name},
|
||||
).scalar_one())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not _column_exists(conn, "production_report_items", "operator_count"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_report_items
|
||||
ADD COLUMN operator_count DECIMAL(10, 2) NOT NULL DEFAULT 1 AFTER stamping_method
|
||||
"""))
|
||||
if not _column_exists(conn, "production_reports", "is_multi_person_assistant"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN is_multi_person_assistant TINYINT(1) NOT NULL DEFAULT 0 AFTER auto_submit_reason
|
||||
"""))
|
||||
if not _column_exists(conn, "production_reports", "multi_person_source_report_id"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN multi_person_source_report_id BIGINT NULL AFTER is_multi_person_assistant
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
UPDATE production_report_items item
|
||||
JOIN products product
|
||||
ON product.attendance_point_name = item.attendance_point_name
|
||||
AND product.project_no = item.project_no
|
||||
AND product.product_name = item.product_name
|
||||
AND product.process_name = IFNULL(item.process_name, '')
|
||||
AND product.device_no = ''
|
||||
SET item.operator_count = product.operator_count
|
||||
WHERE item.operator_count IS NULL OR item.operator_count <= 1
|
||||
"""))
|
||||
print("multi person report columns migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,165 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import SessionLocal # noqa: E402
|
||||
from app.models import DeviceQRCode, MoldLockFeedback, Product, ProductionReportItem, WorkSessionDevice # noqa: E402
|
||||
|
||||
|
||||
LEGACY_PROCESS_PATTERN = re.compile(r"0*(\d+)序")
|
||||
|
||||
|
||||
def normalized(value: str | None) -> str | None:
|
||||
text = str(value or "").strip()
|
||||
matched = LEGACY_PROCESS_PATTERN.fullmatch(text)
|
||||
if matched:
|
||||
return str(int(matched.group(1)))
|
||||
return None
|
||||
|
||||
|
||||
def normalize_products(db, skipped: list[str]) -> int:
|
||||
updated = 0
|
||||
products = db.scalars(
|
||||
select(Product)
|
||||
.where(Product.device_no == "")
|
||||
.order_by(Product.attendance_point_name, Product.project_no, Product.product_name, Product.process_name)
|
||||
).all()
|
||||
for product in products:
|
||||
next_process_name = normalized(product.process_name)
|
||||
if next_process_name is None or next_process_name == product.process_name:
|
||||
continue
|
||||
next_key = {
|
||||
"attendance_point_name": product.attendance_point_name,
|
||||
"project_no": product.project_no,
|
||||
"product_name": product.product_name,
|
||||
"device_no": product.device_no,
|
||||
"process_name": next_process_name,
|
||||
}
|
||||
if db.get(Product, next_key) is not None:
|
||||
skipped.append(
|
||||
f"products: {product.attendance_point_name} / {product.project_no} / "
|
||||
f"{product.product_name} / {product.process_name}"
|
||||
)
|
||||
continue
|
||||
qr_conflict = db.get(DeviceQRCode, {
|
||||
"attendance_point_name": product.attendance_point_name,
|
||||
"device_no": product.product_name,
|
||||
"process_name": next_process_name,
|
||||
})
|
||||
if qr_conflict is not None:
|
||||
skipped.append(
|
||||
f"products: {product.attendance_point_name} / {product.project_no} / "
|
||||
f"{product.product_name} / {product.process_name} (二维码冲突)"
|
||||
)
|
||||
continue
|
||||
|
||||
old_process_name = product.process_name
|
||||
point_name = product.attendance_point_name
|
||||
product_name = product.product_name
|
||||
|
||||
for qr in db.scalars(select(DeviceQRCode).where(
|
||||
DeviceQRCode.attendance_point_name == point_name,
|
||||
DeviceQRCode.device_no == product_name,
|
||||
DeviceQRCode.process_name == old_process_name,
|
||||
)).all():
|
||||
qr.process_name = next_process_name
|
||||
|
||||
for device in db.scalars(select(WorkSessionDevice).where(
|
||||
WorkSessionDevice.attendance_point_name == point_name,
|
||||
WorkSessionDevice.device_no == product_name,
|
||||
WorkSessionDevice.process_name == old_process_name,
|
||||
)).all():
|
||||
device.process_name = next_process_name
|
||||
|
||||
for item in db.scalars(select(ProductionReportItem).where(
|
||||
ProductionReportItem.attendance_point_name == point_name,
|
||||
ProductionReportItem.product_name == product_name,
|
||||
ProductionReportItem.process_name == old_process_name,
|
||||
)).all():
|
||||
item.process_name = next_process_name
|
||||
|
||||
for feedback in db.scalars(select(MoldLockFeedback).where(
|
||||
MoldLockFeedback.attendance_point_name == point_name,
|
||||
MoldLockFeedback.mold_name == product_name,
|
||||
MoldLockFeedback.process_name == old_process_name,
|
||||
)).all():
|
||||
feedback.process_name = next_process_name
|
||||
|
||||
product.process_name = next_process_name
|
||||
updated += 1
|
||||
return updated
|
||||
|
||||
|
||||
def normalize_remaining_references(db, skipped: list[str]) -> dict[str, int]:
|
||||
counts = {
|
||||
"device_qrcodes": 0,
|
||||
"work_session_devices": 0,
|
||||
"production_report_items": 0,
|
||||
"mold_lock_feedbacks": 0,
|
||||
}
|
||||
for qr in db.scalars(select(DeviceQRCode)).all():
|
||||
next_process_name = normalized(qr.process_name)
|
||||
if next_process_name is None or next_process_name == qr.process_name:
|
||||
continue
|
||||
next_key = {
|
||||
"attendance_point_name": qr.attendance_point_name,
|
||||
"device_no": qr.device_no,
|
||||
"process_name": next_process_name,
|
||||
}
|
||||
if db.get(DeviceQRCode, next_key) is not None:
|
||||
skipped.append(f"device_qrcodes: {qr.attendance_point_name} / {qr.device_no} / {qr.process_name}")
|
||||
continue
|
||||
qr.process_name = next_process_name
|
||||
counts["device_qrcodes"] += 1
|
||||
|
||||
for device in db.scalars(select(WorkSessionDevice)).all():
|
||||
next_process_name = normalized(device.process_name)
|
||||
if next_process_name is None or next_process_name == device.process_name:
|
||||
continue
|
||||
device.process_name = next_process_name
|
||||
counts["work_session_devices"] += 1
|
||||
|
||||
for item in db.scalars(select(ProductionReportItem)).all():
|
||||
next_process_name = normalized(item.process_name)
|
||||
if next_process_name is None or next_process_name == item.process_name:
|
||||
continue
|
||||
item.process_name = next_process_name
|
||||
counts["production_report_items"] += 1
|
||||
|
||||
for feedback in db.scalars(select(MoldLockFeedback)).all():
|
||||
next_process_name = normalized(feedback.process_name)
|
||||
if next_process_name is None or next_process_name == feedback.process_name:
|
||||
continue
|
||||
feedback.process_name = next_process_name
|
||||
counts["mold_lock_feedbacks"] += 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def main() -> None:
|
||||
skipped: list[str] = []
|
||||
with SessionLocal() as db:
|
||||
product_updates = normalize_products(db, skipped)
|
||||
reference_updates = normalize_remaining_references(db, skipped)
|
||||
db.commit()
|
||||
|
||||
reference_total = sum(reference_updates.values())
|
||||
print(
|
||||
"numeric process names migrated, "
|
||||
f"products={product_updates}, references={reference_total}, skipped={len(skipped)}"
|
||||
)
|
||||
for name, count in reference_updates.items():
|
||||
print(f"{name}={count}")
|
||||
for item in skipped[:20]:
|
||||
print(f"skipped conflict: {item}")
|
||||
if len(skipped) > 20:
|
||||
print(f"... and {len(skipped) - 20} more")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from sqlalchemy import text # noqa: E402
|
||||
|
||||
from app.database import SessionLocal # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with SessionLocal() as db:
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS person_roles (
|
||||
phone VARCHAR(20) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
role ENUM('worker','admin','manager') COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (phone, role),
|
||||
CONSTRAINT fk_person_roles_person
|
||||
FOREIGN KEY (phone) REFERENCES personnel(phone)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
)
|
||||
)
|
||||
columns = db.execute(text("SHOW COLUMNS FROM personnel LIKE 'role'")).all()
|
||||
if columns:
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT IGNORE INTO person_roles (phone, role, created_at)
|
||||
SELECT phone, role, COALESCE(created_at, CURRENT_TIMESTAMP)
|
||||
FROM personnel
|
||||
"""
|
||||
)
|
||||
)
|
||||
db.execute(text("ALTER TABLE personnel DROP COLUMN role"))
|
||||
db.commit()
|
||||
print("personnel migrated to one phone/name with multiple roles")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def _column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"table_name": table_name, "column_name": column_name},
|
||||
).scalar())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not _column_exists(conn, "products", "process_unit_price_yuan"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE products
|
||||
ADD COLUMN process_unit_price_yuan DECIMAL(12,4) NOT NULL DEFAULT 0 AFTER operator_count
|
||||
"""))
|
||||
if not _column_exists(conn, "production_report_items", "process_unit_price_yuan"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_report_items
|
||||
ADD COLUMN process_unit_price_yuan DECIMAL(12,4) NOT NULL DEFAULT 0 AFTER operator_count
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
UPDATE production_report_items item
|
||||
JOIN products product
|
||||
ON product.attendance_point_name = item.attendance_point_name
|
||||
AND product.project_no = item.project_no
|
||||
AND product.product_name = item.product_name
|
||||
AND product.process_name = IFNULL(item.process_name, '')
|
||||
AND product.device_no = ''
|
||||
SET item.process_unit_price_yuan = product.process_unit_price_yuan
|
||||
WHERE item.process_unit_price_yuan IS NULL OR item.process_unit_price_yuan = 0
|
||||
"""))
|
||||
print("process unit price columns migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
FIELDS = [
|
||||
("product_net_weight_kg", "DECIMAL(12,4) NULL", "material_name"),
|
||||
("product_gross_weight_kg", "DECIMAL(12,4) NULL", "product_net_weight_kg"),
|
||||
("scrap_loss_rate", "DECIMAL(10,6) NULL", "product_gross_weight_kg"),
|
||||
("waste_price_yuan_per_kg", "DECIMAL(12,4) NULL", "scrap_loss_rate"),
|
||||
]
|
||||
|
||||
|
||||
def column_exists(conn, column_name: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'products'
|
||||
AND column_name = :column_name
|
||||
"""), {"column_name": column_name}).scalar())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
for column_name, column_type, after_column in FIELDS:
|
||||
if not column_exists(conn, column_name):
|
||||
conn.execute(text(
|
||||
f"ALTER TABLE products ADD COLUMN {column_name} {column_type} AFTER {after_column}"
|
||||
))
|
||||
print("product ERP auxiliary fields migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from sqlalchemy import text # noqa: E402
|
||||
|
||||
from app.database import SessionLocal # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with SessionLocal() as db:
|
||||
db.execute(text("UPDATE products SET process_name = '' WHERE process_name IS NULL"))
|
||||
db.execute(text("ALTER TABLE products DROP PRIMARY KEY"))
|
||||
db.execute(text("ALTER TABLE products MODIFY device_no VARCHAR(64) NOT NULL"))
|
||||
db.execute(text("ALTER TABLE products MODIFY process_name VARCHAR(128) NOT NULL"))
|
||||
db.execute(
|
||||
text(
|
||||
"ALTER TABLE products "
|
||||
"ADD PRIMARY KEY (project_no, product_name, device_no, process_name)"
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
print("products primary key migrated to project_no + product_name + device_no + process_name")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def column_exists(conn, column_name: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'products'
|
||||
AND column_name = :column_name
|
||||
"""), {"column_name": column_name}).scalar())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not column_exists(conn, "supplier"):
|
||||
conn.execute(text(
|
||||
"ALTER TABLE products ADD COLUMN supplier VARCHAR(255) NULL AFTER material_name"
|
||||
))
|
||||
print("product supplier field migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS device_qrcode_batch_tasks (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
item_count INT NOT NULL DEFAULT 0,
|
||||
completed_count INT NOT NULL DEFAULT 0,
|
||||
failed_count INT NOT NULL DEFAULT 0,
|
||||
items_json JSON NOT NULL,
|
||||
zip_url VARCHAR(500) NULL,
|
||||
error_message VARCHAR(1000) NULL,
|
||||
created_by VARCHAR(20) NOT NULL,
|
||||
started_at DATETIME NULL,
|
||||
finished_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_device_qrcode_batch_tasks_creator (created_by, created_at),
|
||||
KEY idx_device_qrcode_batch_tasks_status (status, created_at),
|
||||
CONSTRAINT fk_device_qrcode_batch_tasks_creator
|
||||
FOREIGN KEY (created_by) REFERENCES personnel (phone)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
)
|
||||
)
|
||||
print("qrcode batch tasks migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS reconciliation_ledger_entries (
|
||||
year INT NOT NULL,
|
||||
month INT NOT NULL,
|
||||
product_name VARCHAR(255) NOT NULL,
|
||||
reconciled_good_qty DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
return_qty DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
updated_by VARCHAR(20) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (year, month, product_name),
|
||||
INDEX idx_reconciliation_product (product_name),
|
||||
INDEX idx_reconciliation_updated (updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
print("reconciliation ledger migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def column_exists(conn, column_name: str) -> bool:
|
||||
return bool(conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'reconciliation_ledger_entries'
|
||||
AND column_name = :column_name
|
||||
"""), {"column_name": column_name}).scalar())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not column_exists(conn, "return_qty"):
|
||||
conn.execute(text(
|
||||
"ALTER TABLE reconciliation_ledger_entries "
|
||||
"ADD COLUMN return_qty DECIMAL(14, 2) NOT NULL DEFAULT 0 "
|
||||
"AFTER reconciled_good_qty"
|
||||
))
|
||||
print("reconciliation return quantity migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
exists = conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'production_report_items'
|
||||
AND column_name = 'raw_material_batch_no'
|
||||
""")).scalar()
|
||||
if not exists:
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_report_items
|
||||
ADD COLUMN raw_material_batch_no VARCHAR(128) NULL AFTER material_name
|
||||
"""))
|
||||
print("report item raw material batch migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
columns = conn.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM information_schema.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() "
|
||||
"AND TABLE_NAME = 'production_report_items' "
|
||||
"AND COLUMN_NAME = 'started_at'"
|
||||
)
|
||||
).scalar()
|
||||
if columns:
|
||||
print("production_report_items.started_at already exists")
|
||||
return
|
||||
conn.execute(
|
||||
text(
|
||||
"ALTER TABLE production_report_items "
|
||||
"ADD COLUMN started_at DATETIME NULL AFTER allocated_minutes"
|
||||
)
|
||||
)
|
||||
print("added production_report_items.started_at")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def _column_exists(conn, table_name: str, column_name: str) -> bool:
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = :table_name
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"table_name": table_name, "column_name": column_name},
|
||||
).scalar())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not _column_exists(conn, "production_reports", "review_remark"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN review_remark VARCHAR(500) NULL AFTER reject_reason
|
||||
"""))
|
||||
print("report review remark column migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.database import SessionLocal
|
||||
|
||||
|
||||
def column_exists(db, table_name: str, column_name: str) -> bool:
|
||||
return bool(db.execute(text(f"SHOW COLUMNS FROM {table_name} LIKE :column"), {"column": column_name}).first())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with SessionLocal() as db:
|
||||
if not column_exists(db, "personnel", "is_temporary"):
|
||||
db.execute(
|
||||
text(
|
||||
"ALTER TABLE personnel "
|
||||
"ADD COLUMN is_temporary TINYINT(1) NOT NULL DEFAULT 0 AFTER name"
|
||||
)
|
||||
)
|
||||
if not column_exists(db, "personnel", "temporary_expires_at"):
|
||||
db.execute(
|
||||
text(
|
||||
"ALTER TABLE personnel "
|
||||
"ADD COLUMN temporary_expires_at DATETIME NULL AFTER is_temporary"
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
print("temporary worker columns migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
from app.database import engine
|
||||
|
||||
|
||||
def column_exists(inspector, table_name: str, column_name: str) -> bool:
|
||||
return any(column["name"] == column_name for column in inspector.get_columns(table_name))
|
||||
|
||||
|
||||
def index_exists(inspector, table_name: str, index_name: str) -> bool:
|
||||
return any(index["name"] == index_name for index in inspector.get_indexes(table_name))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
inspector = inspect(conn)
|
||||
if not column_exists(inspector, "production_reports", "is_voided"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN is_voided TINYINT(1) NOT NULL DEFAULT 0 AFTER multi_person_source_report_id
|
||||
"""))
|
||||
if not column_exists(inspector, "production_reports", "voided_at"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN voided_at DATETIME NULL AFTER is_voided
|
||||
"""))
|
||||
if not column_exists(inspector, "production_reports", "voided_by"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN voided_by VARCHAR(20) NULL AFTER voided_at
|
||||
"""))
|
||||
if not column_exists(inspector, "production_reports", "unvoid_deadline_at"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE production_reports
|
||||
ADD COLUMN unvoid_deadline_at DATETIME NULL AFTER voided_by
|
||||
"""))
|
||||
inspector = inspect(conn)
|
||||
if not index_exists(inspector, "production_reports", "idx_reports_voided"):
|
||||
conn.execute(text("""
|
||||
CREATE INDEX idx_reports_voided ON production_reports (is_voided, voided_at)
|
||||
"""))
|
||||
print("voided report columns migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
from app.services.work_schedule import DEFAULT_WORK_SCHEDULE # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS work_schedules (
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
day_start VARCHAR(5) NOT NULL,
|
||||
day_end VARCHAR(5) NOT NULL,
|
||||
lunch_start VARCHAR(5) NOT NULL,
|
||||
lunch_end VARCHAR(5) NOT NULL,
|
||||
dinner_start VARCHAR(5) NOT NULL,
|
||||
dinner_end VARCHAR(5) NOT NULL,
|
||||
overtime_start VARCHAR(5) NOT NULL,
|
||||
overtime_end VARCHAR(5) NOT NULL,
|
||||
night_start VARCHAR(5) NOT NULL,
|
||||
night_end VARCHAR(5) NOT NULL,
|
||||
attendance_latitude DECIMAL(10, 7) NULL,
|
||||
attendance_longitude DECIMAL(10, 7) NULL,
|
||||
attendance_radius_meters INT NOT NULL DEFAULT 500,
|
||||
auto_submit_hours DECIMAL(10, 2) NOT NULL DEFAULT 15,
|
||||
updated_by VARCHAR(20) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
exists = conn.execute(
|
||||
text("SELECT COUNT(*) FROM work_schedules WHERE id = 1")
|
||||
).scalar_one()
|
||||
if not exists:
|
||||
conn.execute(
|
||||
text("""
|
||||
INSERT INTO work_schedules (
|
||||
id,
|
||||
day_start,
|
||||
day_end,
|
||||
lunch_start,
|
||||
lunch_end,
|
||||
dinner_start,
|
||||
dinner_end,
|
||||
overtime_start,
|
||||
overtime_end,
|
||||
night_start,
|
||||
night_end
|
||||
) VALUES (
|
||||
1,
|
||||
:day_start,
|
||||
:day_end,
|
||||
:lunch_start,
|
||||
:lunch_end,
|
||||
:dinner_start,
|
||||
:dinner_end,
|
||||
:overtime_start,
|
||||
:overtime_end,
|
||||
:night_start,
|
||||
:night_end
|
||||
)
|
||||
"""),
|
||||
DEFAULT_WORK_SCHEDULE,
|
||||
)
|
||||
print("work_schedules migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def column_exists(conn, column_name: str) -> bool:
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'work_schedules'
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"column_name": column_name},
|
||||
).scalar_one())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not column_exists(conn, "attendance_latitude"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE work_schedules
|
||||
ADD COLUMN attendance_latitude DECIMAL(10, 7) NULL AFTER night_end
|
||||
"""))
|
||||
if not column_exists(conn, "attendance_longitude"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE work_schedules
|
||||
ADD COLUMN attendance_longitude DECIMAL(10, 7) NULL AFTER attendance_latitude
|
||||
"""))
|
||||
if not column_exists(conn, "attendance_radius_meters"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE work_schedules
|
||||
ADD COLUMN attendance_radius_meters INT NOT NULL DEFAULT 500 AFTER attendance_longitude
|
||||
"""))
|
||||
print("work_schedules attendance location migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def column_exists(conn, column_name: str) -> bool:
|
||||
return bool(conn.execute(
|
||||
text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'work_schedules'
|
||||
AND column_name = :column_name
|
||||
"""),
|
||||
{"column_name": column_name},
|
||||
).scalar_one())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
if not column_exists(conn, "auto_submit_hours"):
|
||||
conn.execute(text("""
|
||||
ALTER TABLE work_schedules
|
||||
ADD COLUMN auto_submit_hours DECIMAL(10, 2) NOT NULL DEFAULT 15 AFTER attendance_radius_meters
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
UPDATE work_schedules
|
||||
SET auto_submit_hours = 15
|
||||
WHERE auto_submit_hours IS NULL OR auto_submit_hours <= 0
|
||||
"""))
|
||||
print("work_schedules auto_submit_hours migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import engine # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with engine.begin() as conn:
|
||||
session_index_exists = conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'work_session_devices'
|
||||
AND index_name = 'idx_session_devices_session'
|
||||
""")).scalar()
|
||||
if not session_index_exists:
|
||||
conn.execute(text("CREATE INDEX idx_session_devices_session ON work_session_devices (session_id)"))
|
||||
|
||||
index_exists = conn.execute(text("""
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'work_session_devices'
|
||||
AND index_name = 'uk_session_device'
|
||||
""")).scalar()
|
||||
if index_exists:
|
||||
conn.execute(text("ALTER TABLE work_session_devices DROP INDEX uk_session_device"))
|
||||
print("work session repeated molds migrated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import SessionLocal # noqa: E402
|
||||
from app.models import ProductionReport, WorkSession # noqa: E402
|
||||
from app.services.metrics import calculate_report_metrics # noqa: E402
|
||||
from app.services.work_schedule import get_work_schedule_config # noqa: E402
|
||||
|
||||
|
||||
def apply_metrics(report: ProductionReport, schedule) -> None:
|
||||
metrics = calculate_report_metrics(
|
||||
report.start_at,
|
||||
report.end_at,
|
||||
list(report.items),
|
||||
devices=report.session.devices if report.session else None,
|
||||
schedule=schedule,
|
||||
)
|
||||
report.duration_minutes = metrics["duration_minutes"]
|
||||
report.effective_minutes = metrics["effective_minutes"]
|
||||
report.total_good_qty = metrics["total_good_qty"]
|
||||
report.total_output_qty = metrics["total_output_qty"]
|
||||
report.actual_beat = metrics["actual_beat"]
|
||||
report.standard_beat = metrics["standard_beat"]
|
||||
report.expected_workload = metrics["expected_workload"]
|
||||
report.pace_rate = metrics["pace_rate"]
|
||||
report.workload_rate = metrics["workload_rate"]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = ArgumentParser(description="Recalculate production report metrics.")
|
||||
parser.add_argument("--report-id", type=int, help="只重算指定报工 ID")
|
||||
args = parser.parse_args()
|
||||
|
||||
query = select(ProductionReport).options(
|
||||
selectinload(ProductionReport.items),
|
||||
selectinload(ProductionReport.session).selectinload(WorkSession.devices),
|
||||
)
|
||||
if args.report_id:
|
||||
query = query.where(ProductionReport.id == args.report_id)
|
||||
|
||||
with SessionLocal() as db:
|
||||
reports = db.scalars(query).all()
|
||||
for report in reports:
|
||||
schedule = get_work_schedule_config(db, report.attendance_point_name)
|
||||
apply_metrics(report, schedule)
|
||||
db.commit()
|
||||
|
||||
print(f"recalculated reports: {len(reports)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.database import SessionLocal # noqa: E402
|
||||
from app.models import PersonRole, Personnel, Product, Role # noqa: E402
|
||||
|
||||
|
||||
PEOPLE = [
|
||||
{"phone": "13800000001", "name": "张三", "role": Role.worker},
|
||||
{"phone": "13800000002", "name": "李四", "role": Role.admin},
|
||||
{"phone": "13800000003", "name": "王经理", "role": Role.manager},
|
||||
]
|
||||
|
||||
PRODUCTS = [
|
||||
{
|
||||
"project_no": "W02",
|
||||
"profile_no": "XUS-157-6001-003",
|
||||
"product_name": "LAC-56011041后防撞梁左吸能盒端板",
|
||||
"material_code": "110101002022",
|
||||
"material_name": "LAC-56011041-后防撞梁左吸能盒端板-半成品",
|
||||
"device_no": "20#",
|
||||
"process_name": "1序",
|
||||
"stamping_method": "切边冲孔",
|
||||
"operator_count": 1,
|
||||
"standard_beat": 10.984,
|
||||
"standard_workload": 2622,
|
||||
},
|
||||
{
|
||||
"project_no": "W02",
|
||||
"profile_no": "XUS-157-6009-001",
|
||||
"product_name": "LAC-56010050/0014后防撞梁左连接板",
|
||||
"material_code": "110501000186",
|
||||
"material_name": "LAC-56010050-后防撞梁左连接板-半成品",
|
||||
"device_no": "18#",
|
||||
"process_name": "1序",
|
||||
"stamping_method": "切边冲孔",
|
||||
"operator_count": 1,
|
||||
"standard_beat": 5.112,
|
||||
"standard_workload": 5634,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with SessionLocal() as db:
|
||||
for item in PEOPLE:
|
||||
person = db.get(Personnel, item["phone"]) or Personnel(phone=item["phone"])
|
||||
person.name = item["name"]
|
||||
db.add(person)
|
||||
if db.get(PersonRole, {"phone": item["phone"], "role": item["role"]}) is None:
|
||||
db.add(PersonRole(phone=item["phone"], role=item["role"]))
|
||||
|
||||
for item in PRODUCTS:
|
||||
product = db.get(
|
||||
Product,
|
||||
{
|
||||
"project_no": item["project_no"],
|
||||
"product_name": item["product_name"],
|
||||
"device_no": item["device_no"],
|
||||
"process_name": item["process_name"],
|
||||
},
|
||||
) or Product(
|
||||
project_no=item["project_no"],
|
||||
product_name=item["product_name"],
|
||||
device_no=item["device_no"],
|
||||
process_name=item["process_name"],
|
||||
)
|
||||
for key, value in item.items():
|
||||
setattr(product, key, value)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
print("demo data seeded")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user