""" Parts drawer labelling: the category tree and the labels themselves. Two tables: * part_categories - a self-referencing tree, two levels deep in the seed data but the model imposes no depth limit. Seeded from app/data/part_taxonomy.json, then yours to edit. * part_labels - one physical label. A label belongs to a category and describes what lives in one drawer or bag. A label deliberately has no quantity or inventory field. This is a labelling system, not a parts inventory - the drawer tells you how many you have. Created by: Jess Rogerson (yelling commands at Claude.AI) """ from datetime import datetime import re from app import db def slugify(value): """Lowercase, hyphenated, ASCII-ish. Used as the stable category key.""" value = (value or '').strip().lower() value = re.sub(r'[^a-z0-9]+', '-', value) return value.strip('-') or 'category' class PartCategory(db.Model): """A node in the parts taxonomy.""" __tablename__ = 'part_categories' id = db.Column(db.Integer, primary_key=True) # Display code, e.g. "08.02". Free text: renumber however you like. code = db.Column(db.String(16), nullable=True, index=True) name = db.Column(db.String(120), nullable=False) # Stable key used by the seeder so re-seeding never duplicates a row. slug = db.Column(db.String(140), unique=True, nullable=False, index=True) parent_id = db.Column(db.Integer, db.ForeignKey('part_categories.id'), nullable=True, index=True) # Hex colour used as the accent stripe on printed labels. colour = db.Column(db.String(7), nullable=True) sort_order = db.Column(db.Integer, nullable=False, default=0) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) children = db.relationship( 'PartCategory', backref=db.backref('parent', remote_side=[id]), lazy='select', order_by='PartCategory.sort_order, PartCategory.code, PartCategory.name', cascade='all, delete-orphan', ) labels = db.relationship('PartLabel', backref='category', lazy='dynamic') def __repr__(self): return f'' @property def full_name(self): """'Technic > Pins' - one level of parent is enough on a label.""" if self.parent: return f'{self.parent.name} > {self.name}' return self.name @property def display_code(self): return self.code or '' def accent_colour(self): """Own colour, else the parent's, else the LIM red.""" if self.colour: return self.colour if self.parent and self.parent.colour: return self.parent.colour return '#d11013' def label_count(self, recursive=True): total = self.labels.count() if recursive: for child in self.children: total += child.label_count(recursive=True) return total @staticmethod def roots(): return (PartCategory.query .filter(PartCategory.parent_id.is_(None)) .order_by(PartCategory.sort_order, PartCategory.code, PartCategory.name) .all()) @staticmethod def choices(): """Flat (id, indented label) list for