computers: declare subordinate devices instead of coding each one
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 6s

A PC that drives a device which is its own asset had been implemented twice.
METROLOGY_TOOL_MAP covered CMM, Keyence, Genspect and wax-trace, minting a
measuring_tool. A separate path keyed on one hardcoded pc-type minted a Part
Marker machine and filed it under its operation. Both create a device, link the
PC with controls, and archive that link when the PC is re-imaged: one mechanism
with different nouns, written out twice because the second case arrived later.

That is the same trap as the site literals in ADR-015 - a pattern implemented
per instance rather than declared - and it has a known next occurrence. Part
markers already share operation numbers, and any site with two marking lasers
or two wax-trace units on one number needs identical treatment.

One SUBORDINATE_DEVICE_MAP now declares asset type, type name, naming suffix,
whether the device files partof the operation, and the relationship label. The
labels are unchanged per case on purpose: those values are in the production
database and only rows carrying them are archived by a collector push. A site
overrides or adds an entry through subordinatedevice_<pctype> settings, per
ADR-015, so the next case needs no code. A malformed override falls back to the
default rather than failing the push, because a bad setting must not stop a bay
reporting its inventory.

metrology_tool_for stays as a shim over the same map: filters.py and the older
tests read it, and unifying must not change what it returns. A test pins that.

Also adds flask relationships check-shared-machines, which finds the next 0615
rather than waiting for someone to notice duplicate backups. Several devices
legitimately sharing a number and two PCs mis-numbered at imaging look the same
from outside; the difference is whether child assets exist, so that is what it
reports. Read-only.
This commit is contained in:
cproudlock
2026-08-11 11:13:12 -04:00
parent 91143d94fb
commit c90ebcbc7c
5 changed files with 280 additions and 53 deletions

View File

@@ -1217,3 +1217,88 @@ def csv_import(path, directory, tablename, commit):
'Looks good: %d would be created, %d updated.' % (total_new, total_upd),
fg='green', bold=True))
click.echo('Run again with --commit to apply.')
@relationships_cli.command('check-shared-machines')
@with_appcontext
def check_shared_machines():
"""Find machine numbers that more than one PC reports against.
Two very different situations look identical from the outside, and both
were found the hard way rather than by asking:
LEGITIMATE - several devices genuinely share one number. Part markers do:
0613, 0615 and WJPRT each carry more than one, and their configurations
differ by COM port. Modelled correctly, each device is its own asset filed
`partof` the operation, so the operation has CHILD ASSETS.
A FAULT - two PCs carrying the same machine number, usually a mistake at
imaging. Nothing is filed under the operation, the PCs contest one link,
and whichever reported last appears to own the machine.
The difference is whether child assets exist, which is exactly what this
reports. Read-only.
"""
from shopdb.extensions import db
from shopdb.core.models import Asset, AssetRelationship, RelationshipType
from sqlalchemy.orm import aliased
controls = RelationshipType.query.filter_by(relationshiptype='controls').first()
partof = RelationshipType.query.filter_by(relationshiptype='partof').first()
if not controls:
click.echo(click.style("No 'controls' relationship type; "
'run flask seed reference-data.', fg='yellow'))
return
pcasset = aliased(Asset)
machineasset = aliased(Asset)
# Every active collector-made PC -> machine link, grouped by machine.
rows = (db.session.query(machineasset.assetid, machineasset.assetnumber,
pcasset.assetnumber)
.select_from(AssetRelationship)
.join(pcasset, AssetRelationship.sourceassetid == pcasset.assetid)
.join(machineasset, AssetRelationship.targetassetid == machineasset.assetid)
.filter(AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
AssetRelationship.label == 'collector:machine',
AssetRelationship.isactive.is_(True))
.all())
bymachine = {}
for assetid, machinenumber, pcnumber in rows:
bymachine.setdefault((assetid, machinenumber), []).append(pcnumber)
shared = {k: v for k, v in bymachine.items() if len(v) > 1}
if not shared:
click.echo(click.style('No machine number is claimed by more than one PC.',
fg='green'))
return
faults = 0
for (assetid, machinenumber), pcs in sorted(shared.items(), key=lambda kv: kv[0][1] or ''):
children = 0
if partof:
children = (AssetRelationship.query
.filter_by(targetassetid=assetid,
relationshiptypeid=partof.relationshiptypeid,
isactive=True)
.count())
if children:
click.echo(' {:<10} {} PCs, {} child asset(s) - modelled'.format(
machinenumber, len(pcs), children))
else:
faults += 1
click.echo(click.style(
' {:<10} {} PCs, NO child assets - {}'.format(
machinenumber, len(pcs), ', '.join(sorted(pcs))), fg='yellow'))
click.echo()
if faults:
click.echo(click.style(
'{} machine number(s) claimed by several PCs with nothing filed '
'under them.'.format(faults), fg='yellow', bold=True))
click.echo('Either the PCs are mis-numbered - fix that on the PC - or the '
'device type needs an entry in SUBORDINATE_DEVICE_MAP so each '
'device becomes its own asset.')
else:
click.echo(click.style('Every shared number has child assets.', fg='green'))