Declarative subordinate devices + shared-machine check

This commit is contained in:
2026-08-11 11:14:51 -04:00
parent 321d0a3799
commit 5c33b06688
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'))