Adding post-import adjustments

This commit is contained in:
Ian Roddis committed 2026-07-02 13:12:09 -03:00
1 parent c320595423
commit 87019ff2af
6 files changed
+170 -3

No files matched your search

+16
View File
@@ -1,7 +1,9 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.associations import augmented_part_imported_parts
from app.models.augmented_part import AugmentedPart
from app.models.imported_part import ImportedPart
_RELATIONSHIPS = ["colour", "manufacturer", "scope_model", "part_type", "sub_part_type"]
@@ -58,6 +60,20 @@ async def update(
return ap
async def get_unlinked_imported_parts(db: AsyncSession) -> list[ImportedPart]:
"""ImportedParts that have no corresponding AugmentedPart link."""
result = await db.execute(
select(ImportedPart)
.where(
ImportedPart.InventoryID.not_in(
select(augmented_part_imported_parts.c.imported_part_id)
)
)
.order_by(ImportedPart.InventoryID)
)
return list(result.scalars().all())
async def delete(db: AsyncSession, ap: AugmentedPart) -> None:
await db.delete(ap)
await db.commit()
+45
View File
@@ -32,6 +32,51 @@ from app.models.part_type import PartType
from app.schemas.augmented_part import AugmentedPartSyncResult
async def sync_one(
db: AsyncSession,
*,
inventory_id: str,
part_type_id: int,
sub_part_type_id: int | None,
) -> AugmentedPart:
"""Create an AugmentedPart for a single ImportedPart with manually supplied type."""
ip = await db.get(ImportedPart, inventory_id)
if ip is None:
raise ValueError(f"ImportedPart {inventory_id!r} not found")
description = (ip.Description or "").strip() or ip.InventoryID
colour_name = (ip.TBXColour or "").strip() or "Unknown"
colour = await part_colour_crud.get_by_name(db, colour_name)
if colour is None:
colour = await part_colour_crud.create(db, name=colour_name)
manufacturer = await manufacturer_crud.get_by_name(db, "Armament")
if manufacturer is None:
manufacturer = await manufacturer_crud.create(db, name="Armament", url="")
ap = AugmentedPart(
sku=None,
description=description,
colour_id=colour.id,
manufacturer_id=manufacturer.id,
scope_model_id=None,
part_type_id=part_type_id,
sub_part_type_id=sub_part_type_id,
)
db.add(ap)
await db.flush()
await db.execute(
augmented_part_imported_parts.insert().values(
augmented_part_id=ap.id,
imported_part_id=ip.InventoryID,
)
)
await db.commit()
return ap
def _infer_part_type(description: str, part_types: list[PartType]) -> PartType | None:
"""Return the best-matching PartType or None.
@@ -0,0 +1,4 @@
<div id="augmented-parts-sections">
{% include "admin/_augmented_parts_table.html" %}
{% include "admin/_unlinked_parts_table.html" %}
</div>
@@ -0,0 +1,58 @@
<div id="unlinked-parts-list">
{% if unlinked_parts %}
<div class="card">
<h2>Skipped Imported Parts</h2>
<p class="muted">These parts were skipped during sync because no Part Type could be inferred from their description. Assign a type and click Sync to create the Augmented Part entry.</p>
{% if unlinked_sync_result %}<p class="alert alert-success">{{ unlinked_sync_result }}</p>{% endif %}
{% if unlinked_sync_error %}<p class="alert alert-error">{{ unlinked_sync_error }}</p>{% endif %}
<div style="overflow-x:auto">
<table class="table">
<thead>
<tr>
<th>Inventory ID</th>
<th>Description</th>
<th>Colour</th>
<th>Type</th>
<th>Sub Type</th>
<th></th>
</tr>
</thead>
<tbody>
{% for ip in unlinked_parts %}
<tr>
<td><code>{{ ip.InventoryID }}</code></td>
<td>{{ ip.Description | truncate(60) if ip.Description else '—' }}</td>
<td>{{ ip.TBXColour or '—' }}</td>
<td>
<select name="part_type_id" form="unlinked-sync-{{ loop.index }}" required>
<option value="">— select —</option>
{% for pt in part_types %}
<option value="{{ pt.id }}">{{ pt.name }}</option>
{% endfor %}
</select>
</td>
<td>
<select name="sub_part_type_id" form="unlinked-sync-{{ loop.index }}">
<option value="">— none —</option>
{% for spt in sub_part_types %}
<option value="{{ spt.id }}">{{ spt.name }} ({{ spt.part_type.name }})</option>
{% endfor %}
</select>
</td>
<td>
<form id="unlinked-sync-{{ loop.index }}"
hx-post="/admin/augmented-parts/unlinked"
hx-target="#augmented-parts-sections"
hx-swap="outerHTML">
<input type="hidden" name="inventory_id" value="{{ ip.InventoryID }}">
<button type="submit" class="btn btn-primary">Sync</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</div>
+2 -2
View File
@@ -57,11 +57,11 @@
<div class="card">
<h2>Sync with Imported Parts</h2>
<p class="muted">Creates Augmented Part entries for new Imported Parts, and removes entries for deleted Imported Parts.</p>
<form hx-post="/admin/augmented-parts/sync" hx-target="#augmented-part-list" hx-swap="outerHTML">
<form hx-post="/admin/augmented-parts/sync" hx-target="#augmented-parts-sections" hx-swap="outerHTML">
<button type="submit" class="btn btn-primary">Sync now</button>
</form>
</div>
{% include "admin/_augmented_parts_table.html" %}
{% include "admin/_augmented_parts_sections.html" %}
</main>
{% endblock %}
+45 -1
View File
@@ -1143,6 +1143,7 @@ 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)
@@ -1208,17 +1209,60 @@ 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_table.html",
"admin/_augmented_parts_sections.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(
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")),
):
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",
{
**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,
},
)
@router.get("/augmented-parts/{augmented_part_id}/edit", response_class=HTMLResponse)
async def edit_augmented_part_row(
request: Request,