Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session: Settings IA - Replace the flat 27-card settings hub with a persistent two-pane shell (SettingsLayout.vue): grouped, searchable left rail + content pane. - Nest all settings/* routes under the shell via router post-processing; shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers, Equipment, Network) so per-type settings stop scattering. Custom fields (core) - customfields + customfieldvalues tables (migration 7d14), CRUD API at /api/customfields, per-asset value get/save. - Settings management page + reusable CustomFieldsSection (detail) and CustomFieldsInputs (form) wired into all four asset types. Warranty (new plugin) - plugins/warranty: warranties + warrantyassets (migration 7d15), derived coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs). - API CRUD + per-asset panel + report buckets; WarrantyPanel on all four detail pages; Warranties management page; Warranty report + Reports card. - Seed warranty.* permissions. Printer drivers - printerdrivers table (migration 7d13) linked to printer models; drivers now surface on the matching printer's detail page. Other - PCDetail rebalanced (Network + Status + Warranty + custom fields on the right). - Rename PCs list "Features" column to "Remote Access"; fix badge hover underline. - Drop equipment islocationonly field. - Centralize asset-type label/route maps into utils/assetTypes.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,27 @@ from logging.config import fileConfig
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.schema import CreateTable
|
||||
|
||||
|
||||
# Force every table the migrations create on MySQL to utf8mb4 + DYNAMIC row
|
||||
# format. Without this a fresh `flask db upgrade` inherits the server default
|
||||
# charset, so a box whose default is latin1 (common on older MySQL) silently
|
||||
# builds a latin1 schema that drifts from the utf8mb4 production target. The
|
||||
# DYNAMIC row format also keeps utf8mb4 indexes under the 767-byte prefix limit
|
||||
# on pre-5.7 InnoDB. Scoped to the mysql dialect so the SQLite test DB is
|
||||
# untouched.
|
||||
@compiles(CreateTable, "mysql")
|
||||
def _mysql_create_table_utf8mb4(element, compiler, **kw):
|
||||
sql = compiler.visit_create_table(element, **kw)
|
||||
if "CHARSET" not in sql.upper():
|
||||
sql = sql.rstrip().rstrip(";")
|
||||
sql += (
|
||||
" ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
" COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC"
|
||||
)
|
||||
return sql
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
|
||||
37
migrations/versions/7d02_widen_notification_employee_cols.py
Normal file
37
migrations/versions/7d02_widen_notification_employee_cols.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Widen notifications.employeesso / employeename to TEXT
|
||||
|
||||
Recognition and recertification notifications comma-join every listed
|
||||
employee's SSO and name into a single column. VARCHAR(100) truncated the list
|
||||
at ~11 people, dropping names off the shopfloor grid. TEXT removes the cap.
|
||||
|
||||
Revision ID: 7d02_widen_notification_employee_cols
|
||||
Revises: 7d01_dashboarddefaults
|
||||
Create Date: 2026-07-07
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d02_widen_notification_employee_cols'
|
||||
down_revision = '7d01_dashboarddefaults'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('notifications', 'employeesso',
|
||||
existing_type=sa.String(length=100), type_=sa.Text(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('notifications', 'employeename',
|
||||
existing_type=sa.String(length=100), type_=sa.Text(),
|
||||
existing_nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('notifications', 'employeesso',
|
||||
existing_type=sa.Text(), type_=sa.String(length=100),
|
||||
existing_nullable=True)
|
||||
op.alter_column('notifications', 'employeename',
|
||||
existing_type=sa.Text(), type_=sa.String(length=100),
|
||||
existing_nullable=True)
|
||||
53
migrations/versions/7d03_notificationtype_expiry.py
Normal file
53
migrations/versions/7d03_notificationtype_expiry.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Per-type auto-expiry rule on notificationtypes
|
||||
|
||||
Makes the shopfloor auto-expiry window configurable per notification type
|
||||
instead of hardcoding recognition (8 AM Eastern) and recertification (14 days)
|
||||
in the API. Adds:
|
||||
expirymode 'none' | 'duration' | 'dailytime'
|
||||
expirydays days for 'duration'
|
||||
expiryhour hour (Eastern) for 'dailytime'
|
||||
expiryminute minute (Eastern) for 'dailytime'
|
||||
|
||||
Seeds the two existing rule-bearing types so behavior is unchanged:
|
||||
Recognition (typecolor 'recognition') -> dailytime 08:00 Eastern
|
||||
Recertification (typecolor 'recertification') -> duration 14 days
|
||||
|
||||
Revision ID: 7d03_notificationtype_expiry
|
||||
Revises: 7d02_widen_notification_employee_cols
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d03_notificationtype_expiry'
|
||||
down_revision = '7d02_widen_notification_employee_cols'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('notificationtypes', sa.Column('expirymode', sa.String(length=20), nullable=True, server_default='none'))
|
||||
op.add_column('notificationtypes', sa.Column('expirydays', sa.Integer(), nullable=True))
|
||||
op.add_column('notificationtypes', sa.Column('expiryhour', sa.SmallInteger(), nullable=True))
|
||||
op.add_column('notificationtypes', sa.Column('expiryminute', sa.SmallInteger(), nullable=True, server_default='0'))
|
||||
|
||||
# preserve current behavior for the two rule-bearing types
|
||||
op.execute(
|
||||
"UPDATE notificationtypes SET expirymode='dailytime', expiryhour=8, expiryminute=0 "
|
||||
"WHERE typecolor='recognition'"
|
||||
)
|
||||
op.execute(
|
||||
"UPDATE notificationtypes SET expirymode='duration', expirydays=14 "
|
||||
"WHERE typecolor='recertification'"
|
||||
)
|
||||
# everything else: explicit 'none' (indefinite unless the creator sets an end time)
|
||||
op.execute("UPDATE notificationtypes SET expirymode='none' WHERE expirymode IS NULL")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('notificationtypes', 'expiryminute')
|
||||
op.drop_column('notificationtypes', 'expiryhour')
|
||||
op.drop_column('notificationtypes', 'expirydays')
|
||||
op.drop_column('notificationtypes', 'expirymode')
|
||||
43
migrations/versions/7d04_notificationtype_display.py
Normal file
43
migrations/versions/7d04_notificationtype_display.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Add data-driven shopfloor display behavior to notification types
|
||||
|
||||
Replaces the hardcoded recognition/training/recertification logic with per-type
|
||||
columns: split one card per employee, show employee photo, and the display
|
||||
style (standard rows / carousel / grid / banner). Seeds the built-in special
|
||||
types by their typecolor keyword.
|
||||
|
||||
Revision ID: 7d04_notificationtype_display
|
||||
Revises: 7d03_notificationtype_expiry
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d04_notificationtype_display'
|
||||
down_revision = '7d03_notificationtype_expiry'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('notificationtypes', sa.Column('splitperemployee', sa.Boolean(), nullable=True, server_default='0'))
|
||||
op.add_column('notificationtypes', sa.Column('showemployeephoto', sa.Boolean(), nullable=True, server_default='0'))
|
||||
op.add_column('notificationtypes', sa.Column('displaystyle', sa.String(length=20), nullable=True, server_default='standard'))
|
||||
|
||||
conn = op.get_bind()
|
||||
conn.execute(sa.text(
|
||||
"UPDATE notificationtypes SET splitperemployee=1, showemployeephoto=1, displaystyle='carousel' "
|
||||
"WHERE typecolor='recognition'"))
|
||||
conn.execute(sa.text(
|
||||
"UPDATE notificationtypes SET splitperemployee=1, showemployeephoto=1, displaystyle='grid' "
|
||||
"WHERE typecolor='recertification'"))
|
||||
conn.execute(sa.text(
|
||||
"UPDATE notificationtypes SET splitperemployee=1, showemployeephoto=1, displaystyle='carousel' "
|
||||
"WHERE typecolor='training'"))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('notificationtypes', 'displaystyle')
|
||||
op.drop_column('notificationtypes', 'showemployeephoto')
|
||||
op.drop_column('notificationtypes', 'splitperemployee')
|
||||
76
migrations/versions/7d05_pc_access_protocols.py
Normal file
76
migrations/versions/7d05_pc_access_protocols.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""PC access protocols: catalog + per-PC links, retire isvnc/iswinrm
|
||||
|
||||
Adds an admin-managed protocol catalog (accessprotocols) and a per-PC link
|
||||
table (computeraccess). Seeds VNC/WinRM/RDP, migrates each PC's isvnc/iswinrm
|
||||
into computeraccess rows, then drops the two boolean columns.
|
||||
|
||||
Revision ID: 7d05_pc_access_protocols
|
||||
Revises: 7d04_notificationtype_display
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d05_pc_access_protocols'
|
||||
down_revision = '7d04_notificationtype_display'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'accessprotocols',
|
||||
sa.Column('protocolid', sa.Integer(), primary_key=True),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.Column('scheme', sa.String(length=20), nullable=False),
|
||||
sa.Column('defaultport', sa.Integer(), nullable=True),
|
||||
sa.Column('linktemplate', sa.String(length=255), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.UniqueConstraint('name', name='uq_accessprotocol_name'),
|
||||
)
|
||||
op.create_table(
|
||||
'computeraccess',
|
||||
sa.Column('id', sa.Integer(), primary_key=True),
|
||||
sa.Column('computerid', sa.Integer(), nullable=False),
|
||||
sa.Column('protocolid', sa.Integer(), nullable=False),
|
||||
sa.Column('portoverride', sa.Integer(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.ForeignKeyConstraint(['computerid'], ['computers.computerid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['protocolid'], ['accessprotocols.protocolid']),
|
||||
sa.UniqueConstraint('computerid', 'protocolid', name='uq_computer_protocol'),
|
||||
)
|
||||
op.create_index('idx_compaccess_computer', 'computeraccess', ['computerid'])
|
||||
|
||||
conn = op.get_bind()
|
||||
# Seed the protocol catalog. exec_driver_sql so the ':' / '{}' in the
|
||||
# templates are not parsed as bind params.
|
||||
conn.exec_driver_sql(
|
||||
"INSERT INTO accessprotocols (name, scheme, defaultport, linktemplate, isactive) VALUES "
|
||||
"('VNC','vnc',5900,'vnc://{host}:{port}',1),"
|
||||
"('WinRM','https',5986,'https://{host}:{port}/wsman',1),"
|
||||
"('RDP','rdp',3389,'rdp://{host}:{port}',1)"
|
||||
)
|
||||
# Migrate the old booleans into per-PC access rows.
|
||||
conn.exec_driver_sql(
|
||||
"INSERT INTO computeraccess (computerid, protocolid, isactive) "
|
||||
"SELECT c.computerid, p.protocolid, 1 FROM computers c "
|
||||
"JOIN accessprotocols p ON p.name='VNC' WHERE c.isvnc = 1"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"INSERT INTO computeraccess (computerid, protocolid, isactive) "
|
||||
"SELECT c.computerid, p.protocolid, 1 FROM computers c "
|
||||
"JOIN accessprotocols p ON p.name='WinRM' WHERE c.iswinrm = 1"
|
||||
)
|
||||
|
||||
op.drop_column('computers', 'isvnc')
|
||||
op.drop_column('computers', 'iswinrm')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('computers', sa.Column('iswinrm', sa.Boolean(), nullable=True))
|
||||
op.add_column('computers', sa.Column('isvnc', sa.Boolean(), nullable=True))
|
||||
op.drop_index('idx_compaccess_computer', table_name='computeraccess')
|
||||
op.drop_table('computeraccess')
|
||||
op.drop_table('accessprotocols')
|
||||
46
migrations/versions/7d06_typecolor_to_hex.py
Normal file
46
migrations/versions/7d06_typecolor_to_hex.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Give the built-in notification types real hex colors
|
||||
|
||||
Recognition/Recertification/Training stored keyword typecolors ('recognition'
|
||||
etc.) that static color maps had to translate. Every other type already stores
|
||||
a hex. This converts the three to hex so color is fully data-driven and the
|
||||
static maps can go away. Their special behavior now rides on displaystyle /
|
||||
splitperemployee / showemployeephoto, not the color keyword.
|
||||
|
||||
Revision ID: 7d06_typecolor_to_hex
|
||||
Revises: 7d05_pc_access_protocols
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d06_typecolor_to_hex'
|
||||
down_revision = '7d05_pc_access_protocols'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
KEYWORD_HEX = {
|
||||
'recognition': '#ffc107',
|
||||
'recertification': '#0d6efd',
|
||||
'training': '#17a2b8',
|
||||
}
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
for keyword, hexcolor in KEYWORD_HEX.items():
|
||||
conn.execute(
|
||||
sa.text("UPDATE notificationtypes SET typecolor = :hex WHERE typecolor = :kw"),
|
||||
{'hex': hexcolor, 'kw': keyword}
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
for keyword, hexcolor in KEYWORD_HEX.items():
|
||||
conn.execute(
|
||||
sa.text("UPDATE notificationtypes SET typecolor = :kw WHERE typecolor = :hex"),
|
||||
{'kw': keyword, 'hex': hexcolor}
|
||||
)
|
||||
42
migrations/versions/7d07_assettype_color.py
Normal file
42
migrations/versions/7d07_assettype_color.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Add color to asset types (data-driven map/type colors)
|
||||
|
||||
Gives AssetType a color column (like AssetStatus) so the map's top-level type
|
||||
colors come from data instead of the hardcoded map in mapColors.js. Seeds the
|
||||
four base types with the exact colors that were hardcoded, so nothing changes
|
||||
visually until someone edits them.
|
||||
|
||||
Revision ID: 7d07_assettype_color
|
||||
Revises: 7d06_typecolor_to_hex
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d07_assettype_color'
|
||||
down_revision = '7d06_typecolor_to_hex'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
SEED = {
|
||||
'equipment': '#F44336',
|
||||
'computer': '#2196F3',
|
||||
'printer': '#4CAF50',
|
||||
'network_device': '#FF9800',
|
||||
}
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('assettypes', sa.Column('color', sa.String(length=20), nullable=True))
|
||||
conn = op.get_bind()
|
||||
for assettype, hexcolor in SEED.items():
|
||||
conn.execute(
|
||||
sa.text("UPDATE assettypes SET color = :c WHERE assettype = :t AND (color IS NULL OR color = '')"),
|
||||
{'c': hexcolor, 't': assettype}
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('assettypes', 'color')
|
||||
36
migrations/versions/7d08_tvslides.py
Normal file
36
migrations/versions/7d08_tvslides.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Slide manager: tvslides table
|
||||
|
||||
Backing table for the slides plugin (lobby display + shopfloor screensaver
|
||||
playlists). Image files live on disk; this holds order + per-slide duration.
|
||||
|
||||
Revision ID: 7d08_tvslides
|
||||
Revises: 7d07_assettype_color
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d08_tvslides'
|
||||
down_revision = '7d07_assettype_color'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'tvslides',
|
||||
sa.Column('slideid', sa.Integer(), primary_key=True),
|
||||
sa.Column('surface', sa.String(length=20), nullable=False),
|
||||
sa.Column('filename', sa.String(length=255), nullable=False),
|
||||
sa.Column('sortorder', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('seconds', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('uploadeddate', sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.create_index('idx_tvslide_surface', 'tvslides', ['surface'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('idx_tvslide_surface', table_name='tvslides')
|
||||
op.drop_table('tvslides')
|
||||
31
migrations/versions/7d09_subtype_colors.py
Normal file
31
migrations/versions/7d09_subtype_colors.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Add color to asset subtype tables (data-driven map colors)
|
||||
|
||||
The map colors equipment/computer/network/printer subtypes round-robin from a
|
||||
palette. Give each subtype table a color column so a site can set stable, chosen
|
||||
colors that the map reads instead of the auto-assigned palette.
|
||||
|
||||
Revision ID: 7d09_subtype_colors
|
||||
Revises: 7d08_tvslides
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d09_subtype_colors'
|
||||
down_revision = '7d08_tvslides'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
TABLES = ['equipmenttypes', 'computertypes', 'networkdevicetypes', 'printertypes']
|
||||
|
||||
|
||||
def upgrade():
|
||||
for table in TABLES:
|
||||
op.add_column(table, sa.Column('color', sa.String(length=20), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
for table in TABLES:
|
||||
op.drop_column(table, 'color')
|
||||
26
migrations/versions/7d10_relationshiptype_color.py
Normal file
26
migrations/versions/7d10_relationshiptype_color.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Add color to relationship types
|
||||
|
||||
Lets sites color relationship types (Controls, Contains, Stored At...) so the
|
||||
asset relationship graph/badges are visually distinct, like statuses.
|
||||
|
||||
Revision ID: 7d10_relationshiptype_color
|
||||
Revises: 7d09_subtype_colors
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d10_relationshiptype_color'
|
||||
down_revision = '7d09_subtype_colors'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('relationshiptypes', sa.Column('color', sa.String(length=20), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('relationshiptypes', 'color')
|
||||
23
migrations/versions/7d11_locationtype_color.py
Normal file
23
migrations/versions/7d11_locationtype_color.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Add color to location types
|
||||
|
||||
Revision ID: 7d11_locationtype_color
|
||||
Revises: 7d10_relationshiptype_color
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d11_locationtype_color'
|
||||
down_revision = '7d10_relationshiptype_color'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('locationtypes', sa.Column('color', sa.String(length=20), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('locationtypes', 'color')
|
||||
26
migrations/versions/7d12_drop_computer_isshopfloor.py
Normal file
26
migrations/versions/7d12_drop_computer_isshopfloor.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Drop computers.isshopfloor (redundant with the Shopfloor PC type)
|
||||
|
||||
The flag was never populated (0 rows) and duplicated the 'Shopfloor' computer
|
||||
type. Shopfloor classification now lives entirely in computertype.
|
||||
|
||||
Revision ID: 7d12_drop_isshopfloor
|
||||
Revises: 7d11_locationtype_color
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d12_drop_isshopfloor'
|
||||
down_revision = '7d11_locationtype_color'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.drop_column('computers', 'isshopfloor')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('computers', sa.Column('isshopfloor', sa.Boolean(), nullable=True, server_default='0'))
|
||||
32
migrations/versions/7d13_printerdrivers.py
Normal file
32
migrations/versions/7d13_printerdrivers.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Printer drivers: named SMB/HTTP links to driver packages
|
||||
|
||||
Revision ID: 7d13_printerdrivers
|
||||
Revises: 7d12_drop_isshopfloor
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d13_printerdrivers'
|
||||
down_revision = '7d12_drop_isshopfloor'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'printerdrivers',
|
||||
sa.Column('driverid', sa.Integer(), primary_key=True),
|
||||
sa.Column('name', sa.String(length=150), nullable=False),
|
||||
sa.Column('location', sa.String(length=500), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid']),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('printerdrivers')
|
||||
48
migrations/versions/7d14_customfields.py
Normal file
48
migrations/versions/7d14_customfields.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Custom fields: site-defined extra attributes per asset type
|
||||
|
||||
Revision ID: 7d14_customfields
|
||||
Revises: 7d13_printerdrivers
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d14_customfields'
|
||||
down_revision = '7d13_printerdrivers'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'customfields',
|
||||
sa.Column('fieldid', sa.Integer(), primary_key=True),
|
||||
sa.Column('assettypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('fieldkey', sa.String(length=50), nullable=False),
|
||||
sa.Column('label', sa.String(length=150), nullable=False),
|
||||
sa.Column('datatype', sa.String(length=20), nullable=False, server_default='text'),
|
||||
sa.Column('options', sa.Text(), nullable=True),
|
||||
sa.Column('showondetail', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('showonform', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('sortorder', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.ForeignKeyConstraint(['assettypeid'], ['assettypes.assettypeid']),
|
||||
sa.UniqueConstraint('assettypeid', 'fieldkey', name='uq_customfield_type_key'),
|
||||
)
|
||||
op.create_table(
|
||||
'customfieldvalues',
|
||||
sa.Column('valueid', sa.Integer(), primary_key=True),
|
||||
sa.Column('fieldid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.Column('value', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['fieldid'], ['customfields.fieldid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.UniqueConstraint('fieldid', 'assetid', name='uq_customfieldvalue_field_asset'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('customfieldvalues')
|
||||
op.drop_table('customfields')
|
||||
45
migrations/versions/7d15_warranties.py
Normal file
45
migrations/versions/7d15_warranties.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Warranty plugin: warranties + warrantyassets
|
||||
|
||||
Revision ID: 7d15_warranties
|
||||
Revises: 7d14_customfields
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d15_warranties'
|
||||
down_revision = '7d14_customfields'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'warranties',
|
||||
sa.Column('warrantyid', sa.Integer(), primary_key=True),
|
||||
sa.Column('vendor', sa.String(length=100), nullable=False),
|
||||
sa.Column('servicetag', sa.String(length=100), nullable=True),
|
||||
sa.Column('provider', sa.String(length=20), nullable=False, server_default='manual'),
|
||||
sa.Column('servicelevel', sa.String(length=150), nullable=True),
|
||||
sa.Column('startdate', sa.Date(), nullable=True),
|
||||
sa.Column('enddate', sa.Date(), nullable=True),
|
||||
sa.Column('lastcheckeddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
)
|
||||
op.create_table(
|
||||
'warrantyassets',
|
||||
sa.Column('id', sa.Integer(), primary_key=True),
|
||||
sa.Column('warrantyid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['warrantyid'], ['warranties.warrantyid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.UniqueConstraint('warrantyid', 'assetid', name='uq_warrantyasset_warranty_asset'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('warrantyassets')
|
||||
op.drop_table('warranties')
|
||||
Reference in New Issue
Block a user