Add locationtypes + location tree (ADR-001)

- New locationtypes lookup (LocationType model) seeded with section, cell,
  subcell, operation, meetingroom, lab, office, storage, hallway,
  networkcloset, building.
- locations gains locationtypeid + parentlocationid (self-FK) for the site
  location tree. Migration 7c03; cli reference-data seeds the types.
- GET /api/locations/types; locations CRUD accepts type + parent; list/detail
  return locationtypename + parentlocationname.
- Locations settings page: Type column + Type/Parent selectors in the modal.

Realizes the operation-as-Location model (operations are locations with
locationtypeid='operation').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 12:09:05 -04:00
parent a58feebe7a
commit 6fb8adc256
7 changed files with 192 additions and 10 deletions

View File

@@ -4,7 +4,7 @@ from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from shopdb.extensions import db
from shopdb.core.models import Location
from shopdb.core.models import Location, LocationType
from shopdb.utils.responses import (
success_response,
error_response,
@@ -16,6 +16,19 @@ from shopdb.utils.pagination import get_pagination_params, paginate_query
locations_bp = Blueprint('locations', __name__)
@locations_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
def list_location_types():
"""List all location types."""
types = LocationType.query.filter_by(isactive=True).order_by(
LocationType.locationtype).all()
return success_response([{
'locationtypeid': t.locationtypeid,
'locationtype': t.locationtype,
'description': t.description,
} for t in types])
@locations_bp.route('', methods=['GET'])
@jwt_required(optional=True)
def list_locations():
@@ -81,6 +94,8 @@ def create_location():
floor=data.get('floor'),
room=data.get('room'),
description=data.get('description'),
locationtypeid=data.get('locationtypeid'),
parentlocationid=data.get('parentlocationid'),
mapimage=data.get('mapimage'),
mapwidth=data.get('mapwidth'),
mapheight=data.get('mapheight')
@@ -117,7 +132,9 @@ def update_location(location_id: int):
http_code=409
)
for key in ['locationname', 'building', 'floor', 'room', 'description', 'mapimage', 'mapwidth', 'mapheight', 'isactive']:
for key in ['locationname', 'building', 'floor', 'room', 'description',
'locationtypeid', 'parentlocationid', 'mapimage', 'mapwidth',
'mapheight', 'isactive']:
if key in data:
setattr(loc, key, data[key])

View File

@@ -6,7 +6,7 @@ from .machine import MachineType
from .vendor import Vendor
from .model import Model
from .businessunit import BusinessUnit
from .location import Location
from .location import Location, LocationType
from .operatingsystem import OperatingSystem
from .relationship import AssetRelationship, RelationshipType
from .communication import Communication, CommunicationType
@@ -32,6 +32,7 @@ __all__ = [
'Model',
'BusinessUnit',
'Location',
'LocationType',
'OperatingSystem',
# Relationships
'AssetRelationship',

View File

@@ -1,9 +1,25 @@
"""Location model."""
"""Location + LocationType models."""
from shopdb.extensions import db
from .base import BaseModel
class LocationType(BaseModel):
"""Location classification (ADR-001 shared reference data).
Seeded values: section, cell, subcell, operation, meetingroom, lab,
office, storage, hallway, networkcloset, building. Sites can extend.
"""
__tablename__ = 'locationtypes'
locationtypeid = db.Column(db.Integer, primary_key=True)
locationtype = db.Column(db.String(50), unique=True, nullable=False)
description = db.Column(db.Text)
def __repr__(self):
return f"<LocationType {self.locationtype}>"
class Location(BaseModel):
"""Physical location model."""
__tablename__ = 'locations'
@@ -15,6 +31,19 @@ class Location(BaseModel):
room = db.Column(db.String(50))
description = db.Column(db.Text)
# Classification + tree (ADR-001)
locationtypeid = db.Column(
db.Integer,
db.ForeignKey('locationtypes.locationtypeid'),
nullable=True
)
parentlocationid = db.Column(
db.Integer,
db.ForeignKey('locations.locationid'),
nullable=True,
comment='Parent location for the site location tree'
)
# Map configuration
mapimage = db.Column(db.String(500), comment='Path to floor map image')
mapwidth = db.Column(db.Integer)
@@ -26,5 +55,15 @@ class Location(BaseModel):
mapx = db.Column(db.Integer, comment='Default X coordinate for assets at this location')
mapy = db.Column(db.Integer, comment='Default Y coordinate for assets at this location')
# Relationships
locationtype = db.relationship('LocationType')
parent = db.relationship('Location', remote_side=[locationid])
def to_dict(self):
data = super().to_dict()
data['locationtypename'] = self.locationtype.locationtype if self.locationtype else None
data['parentlocationname'] = self.parent.locationname if self.parent else None
return data
def __repr__(self):
return f"<Location {self.locationname}>"