You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make the Design BOM list read-only with edit links, and sort entries naturally by reference
Inline per-row editing required rendering a full Parts dropdown
for every BOM row, which was the main cause of the Design detail
page slowing down as designs and the Parts library grew; rows now
link to a dedicated edit page instead. Reference designators
(e.g. R2 vs R11) now sort numerically rather than as plain strings.
Copy file name to clipboardExpand all lines: CLAUDE.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -150,7 +150,7 @@ ERP/stock-and-ordering features. Provides the **Settings** hub with **Production
150
150
-`Location` — a physical location in a hierarchy (e.g. building › room › shelf). Fields: `parent` (self-referential FK, nullable, `CASCADE` — deleting a parent deletes all descendants), `name`, `description`, `order` (`Meta.ordering = ['order', 'name']`). The `_build_location_tree(all_locations, parent_id, depth)` helper in `views.py` performs a depth-first traversal of a pre-fetched list and returns `[(location, depth), ...]` for template rendering with indentation.
151
151
-`PartCategory` — a category in a hierarchy for classifying parts (e.g. Passives › Resistors › SMD). Fields: `parent` (self-referential FK, nullable, `CASCADE`), `name`, `description`, `order` (`Meta.ordering = ['order', 'name']`, `verbose_name_plural = 'part categories'`). The `_build_part_category_tree(all_categories, parent_id, depth)` helper in `views.py` performs a depth-first traversal, returning `[(category, depth), ...]` for indented rendering — same pattern as `_build_location_tree`.
152
152
- `Part` — a component part. Fields: `name`, `description`, `category` (FK → `PartCategory`, nullable, `SET_NULL` — deleting a category leaves parts uncategorised), `device`, `package`, `value`, `fusion_library`, `stock` (`IntegerField`, nullable — a manually-tracked on-hand count, independent of the supplier-listing stock below), `image` (`ImageField`, stored under `part_images/`), `created_dt` (`Meta.ordering = ['name']`). `__str__` appends the value in parentheses if set. `total_stock` property sums `stock` across all `PartSource` listings (`None` if none have a known level) — shown as "Available" in the Parts list, distinct from the manually-tracked `stock` field above shown as "Stock". `has_stale_source_data` property is `True` if any of the part's `PartSource` listings has `has_stale_variant_data` (see below) — drives a warning icon next to "Available" in the Parts list and Part edit page, and next to "Available" in the Batch detail page's Parts Required section (see `Batch` below), when supplier pricing/stock hasn't been refreshed within `STALE_REFRESH_THRESHOLD` (48 hours) or has never been refreshed at all.
153
-
- `DesignBomEntry` — a single placed component on a `Design`'s BOM (e.g. RefDes `R3` = a 10k resistor `Part`); one row per physical placement rather than a collapsed line item with a quantity, so placement data can support pick-and-place/AOI use cases later. Fields: `design` (FK → `device.Design`, `CASCADE`, `related_name='bom_entries'`), `part` (FK → `Part`, `PROTECT`, `related_name='design_bom_entries'`), `reference` (the reference designator, e.g. `"R3"`), `pos_x`/`pos_y` (`DecimalField`, 9/4, nullable), `rotation` (`DecimalField`, 6/2, nullable), `side` (`TOP`/`BOTTOM` via `SIDE_CHOICES`, blank). `Meta.ordering = ['reference']`; `unique_together` on `(design, reference)`. `pos_x`/`pos_y`/`rotation`/`side` are never user-editable (`DesignBomEntryForm` only exposes `reference`/`part`) — they're only ever populated from the design's PCB Design File asset by `_apply_brd_placements()` (see `design_bom_populate` below). Shown on the Design detail page's Bill of Materials table (Reference/Part/Device/Package/Value/Position/Rotation/Side columns; Position shown as "x, y" in mm, blank/`—` fields shown as `—`).
153
+
- `DesignBomEntry` — a single placed component on a `Design`'s BOM (e.g. RefDes `R3` = a 10k resistor `Part`); one row per physical placement rather than a collapsed line item with a quantity, so placement data can support pick-and-place/AOI use cases later. Fields: `design` (FK → `device.Design`, `CASCADE`, `related_name='bom_entries'`), `part` (FK → `Part`, `PROTECT`, `related_name='design_bom_entries'`), `reference` (the reference designator, e.g. `"R3"`), `pos_x`/`pos_y` (`DecimalField`, 9/4, nullable), `rotation` (`DecimalField`, 6/2, nullable), `side` (`TOP`/`BOTTOM` via `SIDE_CHOICES`, blank). `Meta.ordering = ['reference']`; `unique_together` on `(design, reference)`. `pos_x`/`pos_y`/`rotation`/`side` are never user-editable (`DesignBomEntryForm` only exposes `reference`/`part`) — they're only ever populated from the design's PCB Design File asset by `_apply_brd_placements()` (see `design_bom_populate` below). `reference_sort_key` property splits `reference` into a `(letter prefix, numeric value, remainder)` tuple for natural sorting (so `"R2"` sorts before `"R11"`) — `Meta.ordering`/the DB-level `ORDER BY` is plain string ordering and is left as-is for other consumers (CSV export, admin), so this key is applied explicitly wherever the BOM list needs natural order (currently only `device.views.design_detail`, which `sorted()`s the queryset in Python rather than trying to express natural sort portably at the DB level). Shown on the Design detail page's Bill of Materials table (Reference/Part/Device/Package/Value/Position/Rotation/Side columns; Position shown as "x, y" in mm, blank/`—` fields shown as `—`) as a read-only list — see `design_bom_entry_edit` below for how rows are edited.
154
154
-`PartSource` — a supplier's **listing** for a `Part`: one manufacturer SKU as stocked by one supplier. Fields: `part` (FK, `CASCADE`, `related_name='sources'`), `supplier_name`, `manufacturer_sku`, `stock` (`PositiveIntegerField`, nullable — `None` means unknown). `Meta.ordering = ['supplier_name']`. Stock is held here rather than on `PartSourceVariant` because suppliers such as DigiKey sell the same physical inventory pool under several packaging-specific SKUs (cut tape, tape & reel, Digi-Reel, etc.) that all report the same stock level but different pricing/MOQ — those become separate `PartSourceVariant` rows under one `PartSource`. `has_stale_variant_data` property is `True` if any of this listing's `PartSourceVariant`s has `last_refreshed = None` (never fetched) or older than `STALE_REFRESH_THRESHOLD` (module-level constant, 48 hours) — used both by `Part.has_stale_source_data` above and directly next to each listing's stock figure in the Part edit page's Sources table.
155
155
-`PartSourceVariant` — a specific orderable SKU/packaging option under a `PartSource` listing. Fields: `source` (FK → `PartSource`, `CASCADE`, `related_name='variants'`), `supplier_sku`, `packaging` (e.g. "Tape & Reel (TR)", "Cut Tape (CT)"), `url` (`URLField`), `moq` (`PositiveIntegerField`, nullable — minimum order quantity; `None` means unknown), `last_refreshed` (`DateTimeField`, nullable — when pricing/stock was last fetched from a supplier API via `_refresh_variant()`; `None` means it was only ever added manually via `part_source_add` and has never been fetched). `Meta.ordering = ['supplier_sku']`.
156
156
-`PartPriceBreak` — a quantity-based price break for a `PartSourceVariant` (e.g. qty 1 @ $0.50, qty 10 @ $0.45). Fields: `variant` (FK → `PartSourceVariant`, `CASCADE`, `related_name='price_breaks'`), `quantity`, `price` (`DecimalField`, 12/6), `currency` (ISO code, default `'USD'` — most supplier APIs don't report a currency code at all, so it's stored as an assumption rather than something read from the response; LCSC's API returns a `"$"` symbol rather than an ISO code, so that's normalised to `'USD'` too rather than stored as-is). `Meta.ordering = ['quantity']`, `unique_together = [('variant', 'quantity')]`. `CURRENCY_SYMBOLS` class dict + `symbol` property map the stored ISO code to a display prefix (e.g. `'USD'` → `'$'`); the Part edit page renders breaks as `{{ symbol }}{{ price }} {{ currency }}` (e.g. `$0.5904 USD`).
@@ -203,7 +203,9 @@ ERP/stock-and-ordering features. Provides the **Settings** hub with **Production
203
203
-`part_import_bom` (`/parts/import-bom/`) — POST-only; reads a CSV file (`utf-8-sig` encoding) with columns `reference`, `device`, `package`, `value`, `library`. Each row is resolved via the shared `_resolve_bom_csv_row()` helper (see below) and reports added/skipped/excluded counts via messages.
204
204
- `_resolve_bom_csv_row(row, exclusion_rules, equivalence_rules, library_settings_by_name)` — shared per-row BOM CSV resolution, used by both `part_import_bom` above and `design_bom_populate` below. In order: skips the row if it matches a `BomExclusionRule` (`_bom_row_is_excluded()`); applies the first matching `BomEquivalenceRule` to remap `library`/`device`/`package`/`value` (`_bom_apply_equivalence()`); applies the row's `BomLibrarySetting` (if any, looked up by the post-transformation library) to blank out `device`/`package`/`value` per its `ignore_*` flags; then checks for an existing `Part` matching `device`, `package`, and `value` (case-insensitive `__iexact`; `fusion_library` is intentionally excluded so the same physical part from different Fusion libraries is not duplicated), otherwise creates a new part with `name = " ".join([value, package, device.capitalize()])` and `fusion_library` set to the (possibly remapped) library. Returns `(reference, part, created)`, or `None` if excluded. `_bom_field_matches(rule_value, row_value)` is the shared "blank rule field matches anything" comparison helper used by both rule types.
205
205
-`design_bom_populate` (`/design-bom/<design_id>/populate/`, POST-only) — seeds a `Design`'s `DesignBomEntry` rows from its uploaded BOM CSV `DesignAsset`, via `_resolve_bom_csv_row()`. Rows whose `reference` is already present on the design are skipped, so re-running after manual edits never duplicates or overwrites them. Also calls `_apply_brd_placements()` (see below) to backfill `pos_x`/`pos_y`/`rotation`/`side` on every entry — new and pre-existing — from the design's PCB Design File asset. The success message reports added/skipped/excluded counts plus, when applicable, how many new `Part`s were created in the Parts library and how many entries' positions were updated from the PCB design file. Exposed as a "Populate from BOM" button on the Design detail page's Bill of Materials section (staff only).
206
-
-`design_bom_entry_add` / `_edit` / `_delete` (`/design-bom/<design_id>/add/`, `/design-bom/entry/<entry_id>/edit/`, `/design-bom/entry/<entry_id>/delete/`) — manual CRUD for individual `DesignBomEntry` rows on the Design detail page, via `DesignBomEntryForm` (`reference`/`part` only).
206
+
-`design_bom_entry_add` (`/design-bom/<design_id>/add/`, POST-only) — adds a new `DesignBomEntry` via the inline add row at the bottom of the Design detail page's Bill of Materials table (staff only), using `DesignBomEntryForm` (`reference`/`part` only).
207
+
-`design_bom_entry_edit` (`/design-bom/entry/<entry_id>/edit/`) — GET renders a dedicated edit page ([erp/design_bom_entry_edit.html](pyproj/erp/templates/erp/design_bom_entry_edit.html), staff only) with `DesignBomEntryForm` pre-filled for the entry; POST validates and saves, then redirects back to the Design detail page at its `#bom` anchor. The Bill of Materials table itself is read-only — each row is the whole `<tr>` (`clickable-row` class, same pattern as the Design Files/Attachments tables) linking to this edit page rather than holding inline form fields, since editing every row's `part` field inline meant rendering a full `Part` dropdown (`Part.objects.all()`, default queryset) once per row — an N+1 query and an O(rows × parts) HTML cost that was the main cause of the Design detail page being slow once the BOM list and Parts library both grew.
208
+
-`design_bom_entry_delete` (`/design-bom/entry/<entry_id>/delete/`, POST-only) — deletes a `DesignBomEntry`. Its row's delete button (`cil-trash`) sits in a cell with `onclick="event.stopPropagation()"` so clicking it doesn't also trigger the row's navigation to the edit page.
207
209
-`_parse_brd_placements(brd_path)` — parses a Fusion-exported EAGLE `.brd` XML file's `drawing/board/elements/element` entries into `{reference designator: {pos_x, pos_y, rotation, side}}`. Joins on the reference designator (matches the BOM CSV's `reference` column exactly, since both are generated from the same board) rather than library/device/package/value, which don't line up reliably between the two exports. An element's `rot` attribute (e.g. `R90`, `MR180`) is parsed into a plain degree value plus a `TOP`/`BOTTOM` side — an `M` prefix means mirrored, i.e. a bottom-side placement.
208
210
-`_apply_brd_placements(design)` — looks up the design's `PCB_DESIGN` (PCB Design File) `DesignAsset`, parses it via `_parse_brd_placements()`, and `bulk_update()`s `pos_x`/`pos_y`/`rotation`/`side` on every matching `DesignBomEntry`. Returns `None` if the design has no PCB Design File asset, otherwise the number of entries updated. Called by `design_bom_populate` above.
209
211
-`part_import_filter_list` (`/settings/part-import-filters/`) — the merged "Part Import Filters" settings page: renders `BomExclusionRule`, `BomEquivalenceRule`, and `BomLibrarySetting` as three sections on one page, in the order they're applied in `part_import_bom`. `_part_import_filter_context()` builds the shared context (querysets + a form per section) reused by this view and by each rule type's `_add` view below.
0 commit comments