feat: add inventory CSV import with warehouse/bin locations
- Add warehouse and bin_location columns to Inventory model - Create inventory_import_service for bulk TSV/CSV import - Add POST /api/v1/admin/inventory/import endpoint - Add Import button and modal to inventory admin page - Support both single-unit rows and explicit QUANTITY column File format: BIN, EAN, PRODUCT (optional), QUANTITY (optional) Products matched by GTIN/EAN, unmatched items reported. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""add_warehouse_and_bin_location_to_inventory
|
||||
|
||||
Revision ID: e1bfb453fbe9
|
||||
Revises: j8e9f0a1b2c3
|
||||
Create Date: 2025-12-25 12:21:24.006548
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e1bfb453fbe9'
|
||||
down_revision: Union[str, None] = 'j8e9f0a1b2c3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Check if columns already exist (idempotent)
|
||||
result = conn.execute(text("PRAGMA table_info(inventory)"))
|
||||
columns = {row[1] for row in result.fetchall()}
|
||||
|
||||
if 'warehouse' not in columns:
|
||||
op.add_column('inventory', sa.Column('warehouse', sa.String(), nullable=False, server_default='strassen'))
|
||||
|
||||
if 'bin_location' not in columns:
|
||||
op.add_column('inventory', sa.Column('bin_location', sa.String(), nullable=False, server_default=''))
|
||||
|
||||
# Migrate existing data: copy location to bin_location, set default warehouse
|
||||
conn.execute(text("""
|
||||
UPDATE inventory
|
||||
SET bin_location = COALESCE(location, 'UNKNOWN'),
|
||||
warehouse = 'strassen'
|
||||
WHERE bin_location IS NULL OR bin_location = ''
|
||||
"""))
|
||||
|
||||
# Create indexes if they don't exist
|
||||
indexes = conn.execute(text("PRAGMA index_list(inventory)"))
|
||||
existing_indexes = {row[1] for row in indexes.fetchall()}
|
||||
|
||||
if 'idx_inventory_warehouse_bin' not in existing_indexes:
|
||||
op.create_index('idx_inventory_warehouse_bin', 'inventory', ['warehouse', 'bin_location'], unique=False)
|
||||
if 'ix_inventory_bin_location' not in existing_indexes:
|
||||
op.create_index(op.f('ix_inventory_bin_location'), 'inventory', ['bin_location'], unique=False)
|
||||
if 'ix_inventory_warehouse' not in existing_indexes:
|
||||
op.create_index(op.f('ix_inventory_warehouse'), 'inventory', ['warehouse'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Check which indexes exist before dropping
|
||||
indexes = conn.execute(text("PRAGMA index_list(inventory)"))
|
||||
existing_indexes = {row[1] for row in indexes.fetchall()}
|
||||
|
||||
if 'ix_inventory_warehouse' in existing_indexes:
|
||||
op.drop_index(op.f('ix_inventory_warehouse'), table_name='inventory')
|
||||
if 'ix_inventory_bin_location' in existing_indexes:
|
||||
op.drop_index(op.f('ix_inventory_bin_location'), table_name='inventory')
|
||||
if 'idx_inventory_warehouse_bin' in existing_indexes:
|
||||
op.drop_index('idx_inventory_warehouse_bin', table_name='inventory')
|
||||
|
||||
# Check if columns exist before dropping
|
||||
result = conn.execute(text("PRAGMA table_info(inventory)"))
|
||||
columns = {row[1] for row in result.fetchall()}
|
||||
|
||||
if 'bin_location' in columns:
|
||||
op.drop_column('inventory', 'bin_location')
|
||||
if 'warehouse' in columns:
|
||||
op.drop_column('inventory', 'warehouse')
|
||||
Reference in New Issue
Block a user