Add bulk install-in-device from dashboard and unretire action

- Dashboard bulk toolbar: select batteries, pick a device, click 'Install
  in device'; confirms before moving already-installed batteries; enforces
  slot capacity and warns on brand mix
- Battery detail: 'Unretire Battery' button replaces 'Retire Battery' when
  battery is retired, restoring it to available status
- Tests: 3 new bulk-install-device tests (capacity block, move, success);
  42 total passing
This commit is contained in:
2026-04-12 21:08:48 -05:00
parent 81e87d2fe2
commit 0869ef3d5e
4 changed files with 137 additions and 0 deletions
+66
View File
@@ -225,6 +225,23 @@ def create_app(config_object="config"):
flash(f"{battery.label} has been retired.", "success")
return redirect(url_for("dashboard"))
# ------------------------------------------------------------------ #
# Battery — unretire
# ------------------------------------------------------------------ #
@app.route("/battery/<int:battery_id>/unretire", methods=["POST"])
def battery_unretire(battery_id):
battery = db.get(Battery, battery_id)
if battery is None:
abort(404)
if not battery.is_retired():
flash(f"{battery.label} is not retired.", "error")
return redirect(url_for("battery_detail", battery_id=battery_id))
battery.status = "available"
db.commit()
flash(f"{battery.label} is now available again.", "success")
return redirect(url_for("battery_detail", battery_id=battery_id))
# ------------------------------------------------------------------ #
# Battery — delete
# ------------------------------------------------------------------ #
@@ -285,6 +302,55 @@ def create_app(config_object="config"):
b.brand = new_brand
db.commit()
flash(f"Updated brand to '{new_brand}' for {n} batter{'y' if n == 1 else 'ies'}.", "success")
elif action == "install_device":
device_id = request.form.get("device_id", type=int)
if not device_id:
flash("Please select a device.", "error")
return redirect(url_for("dashboard"))
device = db.get(Device, device_id)
if device is None:
flash("Device not found.", "error")
return redirect(url_for("dashboard"))
already_here = [b for b in batteries if b.device_id == device.id]
retired_sel = [b for b in batteries if b.is_retired()]
to_process = [b for b in batteries
if b.device_id != device.id and not b.is_retired()]
if not to_process:
flash("No eligible batteries to install.", "error")
return redirect(url_for("dashboard"))
free_slots = device.battery_slots - device.installed_count()
if len(to_process) > free_slots:
flash(
f"{device.name} only has {free_slots} free slot(s), "
f"but {len(to_process)} need installing.",
"error",
)
return redirect(url_for("dashboard"))
existing_brands = device.installed_brands()
new_brands = set(b.brand for b in to_process)
if existing_brands and (new_brands - existing_brands):
flash(f"Warning: mixing brands in {device.name}.", "warning")
for b in to_process:
b.status = "installed"
b.device_id = device.id
db.commit()
msg = (f"Installed {len(to_process)} "
f"batter{'y' if len(to_process) == 1 else 'ies'} in {device.name}.")
notes = []
if already_here:
notes.append(f"{len(already_here)} already there")
if retired_sel:
notes.append(f"{len(retired_sel)} retired skipped")
if notes:
msg += f" ({', '.join(notes)}.)"
flash(msg, "success")
elif action == "set_field":
field_name = request.form.get("field_name", "").strip()
field_value = request.form.get("field_value", "").strip() or None