Add the dualpath-as-single-machine site toggle
Most facilities consider a Dualpath pair one physical dual-bay machine. New site setting dualpath_single_machine (default on): the machines list, dashboard counts, machines-by-type report, and floor map collapse each pair to its primary bay (lower assetnumber), with combined 2007 / 2008 labels; pagination totals stay honest. Detail pages remain per-bay and always show a dual-bay sibling banner linking the partner. Pair resolution lives in core services and joins the plugin contract surface (0.8.0 -> 0.9.0). On the WJ dataset: 31 pairs collapse, machine counts 262 -> 231, map 470 assets. Toggle verified live in both states, left on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.api import db, Asset, AssetType, Vendor, Model, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||
from shopdb.api import db, Asset, AssetType, Vendor, Model, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query, resolve_dualpath_pairs, dualpath_single_machine_enabled
|
||||
|
||||
from ..models import Machine, MachineType
|
||||
|
||||
@@ -213,18 +213,61 @@ def list_machines():
|
||||
|
||||
query = query.order_by(col.desc() if sort_dir == 'desc' else col)
|
||||
|
||||
# Dualpath single-machine collapse: hide the secondary bay so a dual-bay
|
||||
# pair lists (and paginates) as one machine. Excluding before pagination
|
||||
# keeps totals honest. Gated on the site setting (default on).
|
||||
collapse = None
|
||||
if dualpath_single_machine_enabled():
|
||||
collapse = resolve_dualpath_pairs()
|
||||
if collapse.secondaryassetids:
|
||||
query = query.filter(Asset.assetid.notin_(collapse.secondaryassetids))
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
|
||||
# Resolve partner machineids for this page's primaries in one query.
|
||||
machineidbyasset = {}
|
||||
if collapse:
|
||||
partnerassetids = [
|
||||
collapse.partnerbyasset[m.assetid]['assetid']
|
||||
for m in items if m.assetid in collapse.partnerbyasset
|
||||
]
|
||||
if partnerassetids:
|
||||
for partner in Machine.query.filter(
|
||||
Machine.assetid.in_(partnerassetids)).all():
|
||||
machineidbyasset[partner.assetid] = partner.machineid
|
||||
|
||||
# Build response with both asset and machine data
|
||||
data = []
|
||||
for mach in items:
|
||||
item = mach.asset.to_dict() if mach.asset else {}
|
||||
item['machine'] = mach.to_dict()
|
||||
# annotate the visible bay with its hidden partner (for '2007 / 2008')
|
||||
partner = collapse.partnerbyasset.get(mach.assetid) if collapse else None
|
||||
item['dualpathpartner'] = {
|
||||
'assetid': partner['assetid'],
|
||||
'machineid': machineidbyasset.get(partner['assetid']),
|
||||
'assetnumber': partner['assetnumber'],
|
||||
} if partner else None
|
||||
data.append(item)
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
def _dualpath_partner_for(assetid):
|
||||
"""Resolve this machine's Dualpath sibling, or None. Always evaluated
|
||||
(independent of the collapse toggle) so the detail-page banner shows even
|
||||
when a site lists both bays. Returns {assetid, machineid, assetnumber}."""
|
||||
partner = resolve_dualpath_pairs().partnerbyasset.get(assetid)
|
||||
if not partner:
|
||||
return None
|
||||
partnermach = Machine.query.filter_by(assetid=partner['assetid']).first()
|
||||
return {
|
||||
'assetid': partner['assetid'],
|
||||
'machineid': partnermach.machineid if partnermach else None,
|
||||
'assetnumber': partner['assetnumber'],
|
||||
}
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def get_machine(machine_id: int):
|
||||
@@ -240,6 +283,7 @@ def get_machine(machine_id: int):
|
||||
|
||||
result = mach.asset.to_dict() if mach.asset else {}
|
||||
result['machine'] = mach.to_dict()
|
||||
result['dualpathpartner'] = _dualpath_partner_for(mach.assetid)
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -259,6 +303,7 @@ def get_machine_by_asset(asset_id: int):
|
||||
|
||||
result = mach.asset.to_dict() if mach.asset else {}
|
||||
result['machine'] = mach.to_dict()
|
||||
result['dualpathpartner'] = _dualpath_partner_for(mach.assetid)
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -462,20 +507,30 @@ def delete_machine(machine_id: int):
|
||||
@jwt_required(optional=True)
|
||||
def dashboard_summary():
|
||||
"""Get machine dashboard summary data."""
|
||||
# Dualpath single-machine collapse: hide the secondary bay from counts so a
|
||||
# dual-bay pair counts once. Gated on the site setting (default on).
|
||||
secondaryassetids = set()
|
||||
if dualpath_single_machine_enabled():
|
||||
secondaryassetids = resolve_dualpath_pairs().secondaryassetids
|
||||
|
||||
# Total active machine count
|
||||
total = db.session.query(Machine).join(Asset).filter(
|
||||
total_query = db.session.query(Machine).join(Asset).filter(
|
||||
Asset.isactive == True
|
||||
).count()
|
||||
)
|
||||
if secondaryassetids:
|
||||
total_query = total_query.filter(Asset.assetid.notin_(secondaryassetids))
|
||||
total = total_query.count()
|
||||
|
||||
# Count by machine type
|
||||
by_type = db.session.query(
|
||||
by_type_query = db.session.query(
|
||||
MachineType.machinetype,
|
||||
db.func.count(Machine.machineid)
|
||||
).join(Machine, Machine.machinetypeid == MachineType.machinetypeid
|
||||
).join(Asset, Asset.assetid == Machine.assetid
|
||||
).filter(Asset.isactive == True
|
||||
).group_by(MachineType.machinetype
|
||||
).all()
|
||||
).filter(Asset.isactive == True)
|
||||
if secondaryassetids:
|
||||
by_type_query = by_type_query.filter(Asset.assetid.notin_(secondaryassetids))
|
||||
by_type = by_type_query.group_by(MachineType.machinetype).all()
|
||||
|
||||
# Count by status
|
||||
from shopdb.api import AssetStatus
|
||||
|
||||
Reference in New Issue
Block a user