An asset that carries a model but no vendor was showing a blank the database could already answer: the model records its vendor, and both sides reference the same vendors table. Machines, PCs, printers and network devices now fall back to it. The fallback is FLAGGED, not merged silently. to_dict sets vendorfrommodel and the detail pages render "(from model)" beside the value, because the record itself is still empty: the edit form shows an empty vendor box, and a page implying the vendor is stored would be lying about where it came from. The model's type is exposed under its own name, modeltypename, and shown as a separate "Model type" row. It is deliberately NOT used to fill in the asset's own type. modeltypes is the catalog-wide list covering every kind of asset - it holds "Access Point", "Camera" and "Desktop PC" alongside the machine entries - so it is a different taxonomy from machinetypes. Only about two thirds of the names overlap, and mapping one onto the other would mistype the remainder, with the failure mode being a machine labelled "Desktop PC". scripts/backfill_vendor_from_model.py writes the derived vendor down for real, since the display fallback leaves reports that read vendorid still seeing nothing. It is a dry run unless given --commit, fills only rows where the asset's vendor is NULL and the model names one, and never overwrites a vendor somebody chose. It skips a table lacking either column, so it runs against a server whose network migration has not been applied yet. Verified against the development database by nulling one machine's vendor inside a transaction: it was detected as fillable, restored to exactly its original value, and the rollback left the row untouched. FLASK_ENV is not forced by the script. The app already reads it from .env, and overriding it demanded a SECRET_KEY the environment had no reason to supply.
146 lines
4.8 KiB
Python
146 lines
4.8 KiB
Python
"""Machines plugin models."""
|
|
|
|
from shopdb.api import db, BaseModel
|
|
|
|
|
|
class MachineType(BaseModel):
|
|
"""
|
|
Machine type classification.
|
|
|
|
Examples: CNC, CMM, Lathe, Grinder, EDM, Part Marker, etc.
|
|
"""
|
|
__tablename__ = 'machinetypes'
|
|
|
|
machinetypeid = db.Column(db.Integer, primary_key=True)
|
|
machinetype = db.Column(db.String(100), unique=True, nullable=False)
|
|
description = db.Column(db.Text)
|
|
icon = db.Column(db.String(50), comment='Icon name for UI')
|
|
color = db.Column(db.String(20), comment='CSS color for UI/map markers')
|
|
|
|
def __repr__(self):
|
|
return f"<MachineType {self.machinetype}>"
|
|
|
|
|
|
class Machine(BaseModel):
|
|
"""
|
|
Machine-specific extension data.
|
|
|
|
Links to core Asset table via assetid.
|
|
Stores machine-specific fields like type, model, vendor, etc.
|
|
"""
|
|
__tablename__ = 'machines'
|
|
|
|
machineid = db.Column(db.Integer, primary_key=True)
|
|
|
|
# Link to core asset
|
|
assetid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
|
|
unique=True,
|
|
nullable=False,
|
|
index=True
|
|
)
|
|
|
|
# Machine classification
|
|
machinetypeid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('machinetypes.machinetypeid'),
|
|
nullable=True
|
|
)
|
|
|
|
# Vendor and model
|
|
vendorid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('vendors.vendorid'),
|
|
nullable=True
|
|
)
|
|
modelnumberid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('models.modelnumberid'),
|
|
nullable=True
|
|
)
|
|
|
|
# Machine-specific fields
|
|
requiresmanualconfig = db.Column(
|
|
db.Boolean,
|
|
default=False,
|
|
comment='Multi-PC machine needs manual configuration'
|
|
)
|
|
islocationonly = db.Column(
|
|
db.Boolean,
|
|
default=False,
|
|
comment='Virtual location marker (not actual machine)'
|
|
)
|
|
|
|
# Maintenance tracking
|
|
lastmaintenancedate = db.Column(db.DateTime, nullable=True)
|
|
nextmaintenancedate = db.Column(db.DateTime, nullable=True)
|
|
maintenanceintervaldays = db.Column(db.Integer, nullable=True)
|
|
|
|
# Controller info (for CNC machines)
|
|
controllervendorid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('vendors.vendorid'),
|
|
nullable=True,
|
|
comment='Controller vendor (e.g., FANUC)'
|
|
)
|
|
controllermodelid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('models.modelnumberid'),
|
|
nullable=True,
|
|
comment='Controller model (e.g., 31B)'
|
|
)
|
|
|
|
# Relationships
|
|
asset = db.relationship(
|
|
'Asset',
|
|
backref=db.backref('machine', uselist=False, lazy='joined')
|
|
)
|
|
machinetype = db.relationship('MachineType', backref='machines')
|
|
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='machine_items')
|
|
model = db.relationship('Model', foreign_keys=[modelnumberid], backref='machine_items')
|
|
controllervendor = db.relationship('Vendor', foreign_keys=[controllervendorid], backref='machine_controllers')
|
|
controllermodel = db.relationship('Model', foreign_keys=[controllermodelid], backref='machine_controller_models')
|
|
|
|
__table_args__ = (
|
|
db.Index('idx_machine_type', 'machinetypeid'),
|
|
db.Index('idx_machine_vendor', 'vendorid'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<Machine {self.assetid}>"
|
|
|
|
def to_dict(self):
|
|
"""Convert to dictionary with related names."""
|
|
result = super().to_dict()
|
|
|
|
# Add related object names
|
|
if self.machinetype:
|
|
result['machinetypename'] = self.machinetype.machinetype
|
|
if self.vendor:
|
|
result['vendorname'] = self.vendor.vendor
|
|
if self.model:
|
|
result['modelname'] = self.model.modelnumber
|
|
if self.model.imageurl:
|
|
result['imageurl'] = self.model.imageurl
|
|
# The catalog model already knows its maker, so an asset that has a
|
|
# model but no vendor of its own is showing a blank the database can
|
|
# fill. Flagged rather than merged silently: the edit form still has
|
|
# an empty vendor box, and a page implying otherwise would be lying.
|
|
if not self.vendor and self.model.vendor:
|
|
result['vendorname'] = self.model.vendor.vendor
|
|
result['vendorfrommodel'] = True
|
|
# Exposed under its OWN name. modeltypes is the catalog-wide list
|
|
# covering every kind of asset, so it is not interchangeable with
|
|
# this asset's own type and must never be substituted for it.
|
|
if self.model.modeltype:
|
|
result['modeltypename'] = self.model.modeltype.modeltype
|
|
|
|
# Add controller info
|
|
if self.controllervendor:
|
|
result['controllervendorname'] = self.controllervendor.vendor
|
|
if self.controllermodel:
|
|
result['controllermodelname'] = self.controllermodel.modelnumber
|
|
|
|
return result
|