From 0168b023af64f84d837e24790253e21e20359a8f Mon Sep 17 00:00:00 2001
From: Ian Roddis <31021769+iroddis@users.noreply.github.com>
Date: Thu, 2 Jul 2026 13:22:28 -0300
Subject: [PATCH] Moving post-import reconciliation to a separate page
---
app/templates/_nav.html | 3 +
app/templates/admin/_skipped_parts_table.html | 61 ++++++++++++
app/templates/admin/augmented_parts.html | 11 ++-
.../admin/augmented_parts_skipped.html | 46 +++++++++
app/web/admin.py | 98 +++++++++++++------
5 files changed, 183 insertions(+), 36 deletions(-)
create mode 100644 app/templates/admin/_skipped_parts_table.html
create mode 100644 app/templates/admin/augmented_parts_skipped.html
diff --git a/app/templates/_nav.html b/app/templates/_nav.html
index fb82ea3..97225e0 100644
--- a/app/templates/_nav.html
+++ b/app/templates/_nav.html
@@ -32,6 +32,9 @@
{% if user.is_superuser or "augmented_parts:read" in user.permissions %}
Augmented Parts
{% endif %}
+ {% if user.is_superuser or "augmented_parts:write" in user.permissions %}
+ Skipped Parts
+ {% endif %}
{% if user.is_superuser or "part_colours:read" in user.permissions %}
Part Colours
{% endif %}
diff --git a/app/templates/admin/_skipped_parts_table.html b/app/templates/admin/_skipped_parts_table.html
new file mode 100644
index 0000000..7a80033
--- /dev/null
+++ b/app/templates/admin/_skipped_parts_table.html
@@ -0,0 +1,61 @@
+
+ {% if sync_result %}
{{ sync_result }}
{% endif %}
+ {% if sync_error %}
{{ sync_error }}
{% endif %}
+
+ {% if unlinked_parts %}
+
+ {% else %}
+
+
No skipped parts — all imported parts have been synchronised.
+
+ {% endif %}
+
diff --git a/app/templates/admin/augmented_parts.html b/app/templates/admin/augmented_parts.html
index 1fcc735..a6a6140 100644
--- a/app/templates/admin/augmented_parts.html
+++ b/app/templates/admin/augmented_parts.html
@@ -57,11 +57,14 @@
Sync with Imported Parts
Creates Augmented Part entries for new Imported Parts, and removes entries for deleted Imported Parts.
-
+
- {% include "admin/_augmented_parts_sections.html" %}
+ {% include "admin/_augmented_parts_table.html" %}
{% endblock %}
diff --git a/app/templates/admin/augmented_parts_skipped.html b/app/templates/admin/augmented_parts_skipped.html
new file mode 100644
index 0000000..138b9c6
--- /dev/null
+++ b/app/templates/admin/augmented_parts_skipped.html
@@ -0,0 +1,46 @@
+{% extends "base.html" %}
+{% block title %}Skipped Parts · {{ app_name }}{% endblock %}
+{% block body %}
+{% include "_nav.html" %}
+
+ Skipped Imported Parts
+
+ These parts were skipped during sync because no Part Type could be inferred
+ from their description. Select one or more rows, choose a Type (and optionally
+ a Sub Type), then click Sync selected to create their
+ Augmented Part entries.
+
+ ← Augmented Parts
+
+ {% include "admin/_skipped_parts_table.html" %}
+
+
+
+{% endblock %}
diff --git a/app/web/admin.py b/app/web/admin.py
index 034ba19..97ecaf6 100644
--- a/app/web/admin.py
+++ b/app/web/admin.py
@@ -1143,7 +1143,6 @@ async def augmented_parts_page(
ctx = await _augmented_part_form_ctx(db)
ctx["user"] = user
ctx["augmented_parts"] = await augmented_part_crud.list_all(db)
- ctx["unlinked_parts"] = await augmented_part_crud.get_unlinked_imported_parts(db)
return templates.TemplateResponse(request, "admin/augmented_parts.html", ctx)
@@ -1209,56 +1208,91 @@ async def sync_augmented_parts(
except Exception as exc:
sync_result = None
sync_error = f"Sync failed: {exc}"
- form_ctx = await _augmented_part_form_ctx(db)
return templates.TemplateResponse(
request,
- "admin/_augmented_parts_sections.html",
+ "admin/_augmented_parts_table.html",
{
- **form_ctx,
"augmented_parts": await augmented_part_crud.list_all(db),
- "unlinked_parts": await augmented_part_crud.get_unlinked_imported_parts(db),
"sync_result": sync_result,
"sync_error": sync_error,
},
)
-@router.post("/augmented-parts/unlinked", response_class=HTMLResponse)
-async def sync_unlinked_part(
+@router.get("/augmented-parts/skipped", response_class=HTMLResponse)
+async def skipped_parts_page(
request: Request,
- inventory_id: str = Form(...),
- part_type_id: int = Form(...),
- sub_part_type_id: str = Form(""),
db: AsyncSession = Depends(get_db),
- actor: User = Depends(web_require("augmented_parts:write")),
+ user: User = Depends(web_require("augmented_parts:read")),
):
- try:
- ap = await augmented_part_sync_service.sync_one(
- db,
- inventory_id=inventory_id,
- part_type_id=part_type_id,
- sub_part_type_id=int(sub_part_type_id) if sub_part_type_id else None,
- )
- await audit_crud.log(
- db, actor=actor, action="create", resource_type="AugmentedPart",
- resource_id=ap.id,
- description=audit_crud.describe_create("AugmentedPart", ap.id, ap.description),
- )
- unlinked_sync_result = f"Created Augmented Part for '{ap.description}'."
- unlinked_sync_error = None
- except Exception as exc:
- unlinked_sync_result = None
- unlinked_sync_error = f"Sync failed: {exc}"
form_ctx = await _augmented_part_form_ctx(db)
return templates.TemplateResponse(
request,
- "admin/_augmented_parts_sections.html",
+ "admin/augmented_parts_skipped.html",
+ {
+ "user": user,
+ **form_ctx,
+ "unlinked_parts": await augmented_part_crud.get_unlinked_imported_parts(db),
+ },
+ )
+
+
+@router.post("/augmented-parts/skipped", response_class=HTMLResponse)
+async def sync_skipped_parts(
+ request: Request,
+ db: AsyncSession = Depends(get_db),
+ actor: User = Depends(web_require("augmented_parts:write")),
+):
+ form_data = await request.form()
+ inventory_ids = [str(v) for v in form_data.getlist("inventory_ids")]
+ part_type_id_str = str(form_data.get("part_type_id", ""))
+ sub_part_type_id_str = str(form_data.get("sub_part_type_id", ""))
+
+ part_type_id = int(part_type_id_str) if part_type_id_str else None
+ sub_part_type_id = int(sub_part_type_id_str) if sub_part_type_id_str else None
+
+ sync_result = None
+ sync_error = None
+
+ if not inventory_ids:
+ sync_error = "No parts selected."
+ elif part_type_id is None:
+ sync_error = "No type selected."
+ else:
+ synced = 0
+ errors: list[str] = []
+ for inv_id in inventory_ids:
+ try:
+ ap = await augmented_part_sync_service.sync_one(
+ db,
+ inventory_id=inv_id,
+ part_type_id=part_type_id,
+ sub_part_type_id=sub_part_type_id,
+ )
+ await audit_crud.log(
+ db, actor=actor, action="create", resource_type="AugmentedPart",
+ resource_id=ap.id,
+ description=audit_crud.describe_create(
+ "AugmentedPart", ap.id, ap.description
+ ),
+ )
+ synced += 1
+ except Exception as exc:
+ errors.append(f"{inv_id}: {exc}")
+ if errors:
+ sync_error = "Some parts failed: " + "; ".join(errors)
+ if synced:
+ sync_result = f"{synced} part(s) synced successfully."
+
+ form_ctx = await _augmented_part_form_ctx(db)
+ return templates.TemplateResponse(
+ request,
+ "admin/_skipped_parts_table.html",
{
**form_ctx,
- "augmented_parts": await augmented_part_crud.list_all(db),
"unlinked_parts": await augmented_part_crud.get_unlinked_imported_parts(db),
- "unlinked_sync_result": unlinked_sync_result,
- "unlinked_sync_error": unlinked_sync_error,
+ "sync_result": sync_result,
+ "sync_error": sync_error,
},
)