"""add inventory management tables and permissions Revision ID: c3d4e5f6a7b8 Revises: b2c3d4e5f6a7 Create Date: 2026-04-05 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql revision: str = 'c3d4e5f6a7b8' down_revision: Union[str, None] = 'b2c3d4e5f6a7' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # 物料分类表 op.create_table( 'material_categories', sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), sa.Column('name', sa.String(length=100), nullable=False), sa.Column('code', sa.String(length=50), nullable=False), sa.Column('description', sa.Text(), nullable=True), sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('code'), ) # 物料主数据表 op.create_table( 'materials', sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), sa.Column('category_id', sa.BigInteger(), nullable=True), sa.Column('name', sa.String(length=200), nullable=False), sa.Column('model', sa.String(length=100), nullable=True), sa.Column('specification', sa.Text(), nullable=True), sa.Column('brand', sa.String(length=100), nullable=True), sa.Column('unit', sa.String(length=20), server_default='个', nullable=True), sa.Column('safe_quantity', sa.Integer(), server_default='0', nullable=True), sa.Column('notes', sa.Text(), nullable=True), sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.Column('updated_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.ForeignKeyConstraint(['category_id'], ['material_categories.id']), sa.PrimaryKeyConstraint('id'), ) op.create_index('ix_materials_id', 'materials', ['id'], unique=False) # 库存批次表 op.create_table( 'inventory_batches', sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), sa.Column('material_id', sa.BigInteger(), nullable=True), sa.Column('batch_no', sa.String(length=50), nullable=False), sa.Column('quantity', sa.Integer(), nullable=False), sa.Column('available_quantity', sa.Integer(), nullable=False, server_default='0'), sa.Column('supplier', sa.String(length=200), nullable=True), sa.Column('purchase_date', sa.Date(), nullable=True), sa.Column('purchase_price', sa.Numeric(10, 2), nullable=True), sa.Column('expiry_date', sa.Date(), nullable=True), sa.Column('location', sa.String(length=100), nullable=True), sa.Column('status', sa.String(length=20), server_default='in_stock', nullable=True), sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.ForeignKeyConstraint(['material_id'], ['materials.id']), sa.PrimaryKeyConstraint('id'), ) # 序列号设备表 op.create_table( 'serial_devices', sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), sa.Column('material_id', sa.BigInteger(), nullable=True), sa.Column('batch_id', sa.BigInteger(), nullable=True), sa.Column('serial_no', sa.String(length=100), nullable=False), sa.Column('mac_address', sa.String(length=17), nullable=True), sa.Column('asset_no', sa.String(length=50), nullable=True), sa.Column('status', sa.String(length=20), server_default='in_stock', nullable=True), sa.Column('current_location', sa.String(length=200), nullable=True), sa.Column('installed_info', postgresql.JSONB(), nullable=True), sa.Column('onu_device_id', sa.BigInteger(), nullable=True), sa.Column('notes', sa.Text(), nullable=True), sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.Column('updated_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.ForeignKeyConstraint(['batch_id'], ['inventory_batches.id']), sa.ForeignKeyConstraint(['material_id'], ['materials.id']), sa.ForeignKeyConstraint(['onu_device_id'], ['onu_devices.id']), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('serial_no'), ) op.create_index('ix_serial_devices_mac_address', 'serial_devices', ['mac_address'], unique=False) op.create_index('ix_serial_devices_status', 'serial_devices', ['status'], unique=False) # 出入库记录表 op.create_table( 'inventory_transactions', sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), sa.Column('transaction_no', sa.String(length=50), nullable=False), sa.Column('transaction_type', sa.String(length=20), nullable=False), sa.Column('material_id', sa.BigInteger(), nullable=True), sa.Column('batch_id', sa.BigInteger(), nullable=True), sa.Column('serial_device_id', sa.BigInteger(), nullable=True), sa.Column('quantity', sa.Integer(), nullable=False), sa.Column('from_status', sa.String(length=20), nullable=True), sa.Column('to_status', sa.String(length=20), nullable=True), sa.Column('operator_id', sa.BigInteger(), nullable=True), sa.Column('project_name', sa.String(length=200), nullable=True), sa.Column('installation_info', postgresql.JSONB(), nullable=True), sa.Column('notes', sa.Text(), nullable=True), sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.ForeignKeyConstraint(['batch_id'], ['inventory_batches.id']), sa.ForeignKeyConstraint(['material_id'], ['materials.id']), sa.ForeignKeyConstraint(['operator_id'], ['users.id']), sa.ForeignKeyConstraint(['serial_device_id'], ['serial_devices.id']), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('transaction_no'), ) # 盘点记录表 op.create_table( 'inventory_checks', sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), sa.Column('check_no', sa.String(length=50), nullable=False), sa.Column('check_date', sa.Date(), nullable=False), sa.Column('checker_id', sa.BigInteger(), nullable=True), sa.Column('material_id', sa.BigInteger(), nullable=True), sa.Column('batch_id', sa.BigInteger(), nullable=True), sa.Column('book_quantity', sa.Integer(), nullable=True), sa.Column('actual_quantity', sa.Integer(), nullable=True), sa.Column('difference', sa.Integer(), nullable=True), sa.Column('reason', sa.Text(), nullable=True), sa.Column('adjusted', sa.Boolean(), server_default='false', nullable=True), sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), nullable=True), sa.ForeignKeyConstraint(['batch_id'], ['inventory_batches.id']), sa.ForeignKeyConstraint(['checker_id'], ['users.id']), sa.ForeignKeyConstraint(['material_id'], ['materials.id']), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('check_no'), ) # 新增库存管理权限 permissions_table = sa.table( 'permissions', sa.column('name', sa.String), sa.column('code', sa.String), sa.column('module', sa.String), sa.column('description', sa.Text), ) op.bulk_insert(permissions_table, [ {'name': '查看库存', 'code': 'inventory.view', 'module': 'inventory', 'description': '查看库存列表和统计'}, {'name': '管理物料', 'code': 'inventory.manage', 'module': 'inventory', 'description': '创建、编辑、删除物料'}, {'name': '出入库操作', 'code': 'inventory.transaction', 'module': 'inventory', 'description': '执行采购入库、领用出库、退库操作'}, {'name': '库存盘点', 'code': 'inventory.check', 'module': 'inventory', 'description': '创建盘点单并执行库存调整'}, {'name': '库存报表', 'code': 'inventory.report', 'module': 'inventory', 'description': '查看库存统计报表'}, ]) # 默认给 area_admin 查看权限 op.execute(""" INSERT INTO role_permissions (role, permission_id) SELECT 'area_admin', id FROM permissions WHERE code = 'inventory.view' """) # 插入默认物料分类 op.execute(""" INSERT INTO material_categories (name, code, description) VALUES ('ONU设备', 'ONU', '光网络单元设备'), ('OLT设备', 'OLT', '光线路终端设备'), ('交换机', 'SWITCH', '网络交换机'), ('防火墙', 'FIREWALL', '网络防火墙设备'), ('上网行为管理', 'BEHAVIOR', '上网行为管理设备'), ('光模块及配件', 'ACCESSORY', '光模块、跳线等配件') """) def downgrade() -> None: op.drop_table('inventory_checks') op.drop_table('inventory_transactions') op.drop_index('ix_serial_devices_status', table_name='serial_devices') op.drop_index('ix_serial_devices_mac_address', table_name='serial_devices') op.drop_table('serial_devices') op.drop_table('inventory_batches') op.drop_index('ix_materials_id', table_name='materials') op.drop_table('materials') op.drop_table('material_categories') op.execute(""" DELETE FROM role_permissions WHERE permission_id IN ( SELECT id FROM permissions WHERE module = 'inventory' ) """) op.execute("DELETE FROM permissions WHERE module = 'inventory'")