T8.6 - D6: the material list uploads the way the location list does
CR-013 accepted free text because the master workbook never arrived; the
Aug 18 call was the CR-005 call again - build the upload path now.
THE component, extracted: T5.4's paste-or-file machinery (file read in the
browser, ONE parser on the server; dry-run check; a report naming every
rejected row with its source line; an editable list that deactivates rather
than deletes) moved from the location-specific functions into
html/wp-list-import.js. The location list and the new material list are both
instances of it - the done-when's "against the same component, not beside it"
made literally true. The loc* names survive as thin delegates because row
handlers, step entry and the probes call them; locations_check re-pointed its
fetch-count assertion to where the fetches now live and still demands every
read and write reach the server.
The material list itself: description, unit, optional code - one new table
(Alembic a1b8c6d4e2f9, additive), GET/import/POST/PATCH routes on the CR-005
pattern, deactivate-never-delete, reactivation reuses the same row so nothing
referencing it orphans. The sample rows are obviously fake (SAMPLE-EMT-075).
NO inventory, price, stock or warehouse field anywhere - the probe walks the
model's columns by regex. The wizard hosts it on step 11 beside the location
list, optional by design: a project with no list still raises free-text
requests (T8.5 wires that).
Parser bug caught by the probe's first run: strip(',;') ate a LEADING comma,
so ',FT' - an empty description - was accepted as a material named FT.
rstrip only, now; the empty first column is rejected with its line number.
Verification (each probe run alone): NEW tests/materials_check.py 17/17.
Regression: locations_check 58/58 through the shared component.
Items: D6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
"""per-project material list (D6 / T8.6)
|
||||
|
||||
CR-013 was written to accept free text because Nate's spreadsheet and the
|
||||
master material workbook had not been supplied - and they still have not.
|
||||
The Aug 18 call was the same one made for locations at CR-005: build the
|
||||
upload path now. One row per line item a request can pick from: description,
|
||||
unit, an optional code. Deliberately NO inventory level, price or warehouse
|
||||
id - a project-scoped uploaded list is not the deferred parts catalog.
|
||||
|
||||
Additive only: a new table, no change to any existing one.
|
||||
|
||||
Revision ID: a1b8c6d4e2f9
|
||||
Revises: f3a9d2c1e8b7
|
||||
Create Date: 2026-08-19 15:40:00.000000
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a1b8c6d4e2f9'
|
||||
down_revision = 'f3a9d2c1e8b7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'material_items',
|
||||
sa.Column('id', sa.String(length=40), nullable=False),
|
||||
sa.Column('project_id', sa.String(length=40), nullable=False),
|
||||
sa.Column('code', sa.String(length=80), nullable=False, server_default=''),
|
||||
sa.Column('description', sa.String(length=300), nullable=False, server_default=''),
|
||||
sa.Column('unit', sa.String(length=20), nullable=False, server_default=''),
|
||||
sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
|
||||
sa.Column('sort', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index('ix_material_items_project_id', 'material_items', ['project_id'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('ix_material_items_project_id', table_name='material_items')
|
||||
op.drop_table('material_items')
|
||||
Reference in New Issue
Block a user