T8.4 - CR-012: the delivery location is the shared vocabulary plus fifty feet

Staging is not the pain; the last fifty feet are - the correct floor lay-down,
shark cage or conduit tree instead of material picked at will by whoever is
closest. The Kitting & MIMO section gains:

- Delivery Building / Floor / Sector: the SAME dependent pickers CR-004 built,
  through the same fillLocSelect (which learned an optional field-map instead
  of being copied), reading the same project location lists, storing PATHS.
  A parallel free-text location vocabulary is exactly what CR-004 removed;
  none was added.
- A free-text detail field for the specifics ("Shark cage 7, conduit tree C"),
  persisted as delivDetail.
- deliveryLoc, the composed display string (labels off the shared lists, then
  the detail after a dash) - which is what the CR-011 email already reads
  (kitting_body preferred deliveryLoc from day one, with mimoLoc as the
  pre-CR-012 fallback) and what the package printout now carries as its own
  Delivery Location row.

Verification (each probe run alone): kitting_check.py extended to 26/26 (the
delivery selects are asserted to offer the SAME option list as the CR-004
trio, values persist as paths, the printout carries the composed value);
kitting_notify_check 17/17 now asserting the mail carries CR-012's composed
value, not the fallback. Regression: locations_check 58/58.

Items: CR-012

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:11:05 -07:00
parent 829fa22236
commit b9d5f8ef92
5 changed files with 99 additions and 8 deletions

View File

@@ -290,7 +290,7 @@ python tests/creator_dialogs_check.py # S1 creator - 0 natives, errors at fields
Wave 8 adds these:
```bash
python tests/kitting_check.py # CR-009/CR-010 - statuses, owner, filter 21 checks
python tests/kitting_check.py # CR-009/010/012 - statuses, owner, delivery 26 checks
python tests/kitting_notify_check.py # CR-011 - kitting mail, coalesced, gated 17 checks
```

View File

@@ -1605,7 +1605,7 @@ function collectPackage(){
gateOverride:pkgGateOverride || undefined,
assets:pkgAssets.filter(a=>a.tag||a.link||a.desc),
materials:pkgMaterials.filter(m=>m.qty||m.desc), attachments:pkgAttach.filter(a=>a.doc),
kitStatus:gv('wp_kit_status'), kitOwner:gv('wp_kit_owner'), kitOwnerId:pkgKitOwnerId||'', kitDate:gv('wp_kit_date'),
kitStatus:gv('wp_kit_status'), kitOwner:gv('wp_kit_owner'), kitOwnerId:pkgKitOwnerId||'', delivBuilding:gv('wp_deliv_building'), delivFloor:gv('wp_deliv_floor'), delivSector:gv('wp_deliv_sector'), delivDetail:gv('wp_deliv_detail'), deliveryLoc:deliveryLocOf({delivBuilding:gv('wp_deliv_building'), delivFloor:gv('wp_deliv_floor'), delivSector:gv('wp_deliv_sector'), delivDetail:gv('wp_deliv_detail')}), kitDate:gv('wp_kit_date'),
mimoTime:gv('wp_mimo_time'), mimoLoc:gv('wp_mimo_loc'),
constraints:pkgConstraints.map(c=>({name:c.name,status:c.status,comment:c.comment})),
qc:gv('wp_qc'), photo:gv('wp_photo'), hold:gv('wp_hold'),
@@ -1759,6 +1759,7 @@ function renderPackage(pkg){
<tr><th>Warehouse Owner</th><td>${cell(pkg.kitOwner)}</td></tr>
<tr><th>Kitting Need Date</th><td>${cell(pkg.kitDate)}</td></tr>
<tr><th>MIMO Sch. Time / Location</th><td>${cell(pkg.mimoTime)} ${pkg.mimoLoc?'· '+esc(pkg.mimoLoc):''}</td></tr>
<tr><th>Delivery Location</th><td>${cell(pkg.deliveryLoc || deliveryLocOf(pkg))}</td></tr>
</tbody></table>`);
let ct=`<table><thead><tr><th>Constraint</th><th style="width:90px">Status</th><th>Comment</th></tr></thead><tbody>`;
@@ -2193,7 +2194,7 @@ function locLabelFull(path){
}
async function loadLocations(){
if(!activeProjectId){ wpLocations = []; wpLocationsLoaded = true; buildLocationPickers(); return; }
if(!activeProjectId){ wpLocations = []; wpLocationsLoaded = true; buildLocationPickers(); buildDeliveryPickers(); return; }
try {
const r = await fetch('/api/projects/' + encodeURIComponent(activeProjectId)
+ '/locations?include_inactive=true',
@@ -2209,6 +2210,7 @@ async function loadLocations(){
}
wpLocationsLoaded = true;
buildLocationPickers();
buildDeliveryPickers();
}
/* Fill one level's <select>, filtered to the children of `parent`.
@@ -2216,8 +2218,8 @@ async function loadLocations(){
`keep` is the value already on the package. It is offered even when it is
inactive — otherwise opening an old package would silently blank its location
and the next save would write that blank back. */
function fillLocSelect(level, parent, keep){
const sel = document.getElementById(LOC_FIELD[level]);
function fillLocSelect(level, parent, keep, fieldMap){
const sel = document.getElementById((fieldMap || LOC_FIELD)[level]);
if(!sel) return;
const wanted = LOC_LEVELS.indexOf(level);
const options = wpLocations.filter(n =>
@@ -2240,6 +2242,36 @@ function fillLocSelect(level, parent, keep){
if(keep) sel.value = keep;
}
// CR-012: the delivery trio reads the SAME lists through the same builder -
// a parallel copy of the location vocabulary is exactly what CR-004 removed.
const DELIV_FIELD = {building: 'wp_deliv_building', floor: 'wp_deliv_floor', sector: 'wp_deliv_sector'};
function buildDeliveryPickers(keepValues){
const cur = keepValues || {
building: (document.getElementById('wp_deliv_building') || {}).value || '',
floor: (document.getElementById('wp_deliv_floor') || {}).value || '',
sector: (document.getElementById('wp_deliv_sector') || {}).value || '',
};
fillLocSelect('building', '', cur.building, DELIV_FIELD);
fillLocSelect('floor', cur.building, cur.building ? cur.floor : '', DELIV_FIELD);
fillLocSelect('sector', cur.floor, cur.floor ? cur.sector : '', DELIV_FIELD);
}
function onDeliveryLocChange(level){
const b = (document.getElementById('wp_deliv_building') || {}).value || '';
const f = (document.getElementById('wp_deliv_floor') || {}).value || '';
const s = (document.getElementById('wp_deliv_sector') || {}).value || '';
if(level === 'building') buildDeliveryPickers({building: b, floor: '', sector: ''});
else if(level === 'floor') buildDeliveryPickers({building: b, floor: f, sector: ''});
else buildDeliveryPickers({building: b, floor: f, sector: s});
}
// The display string the kitting email (CR-011) and the printout carry:
// labels off the shared lists, then the free-text detail after a dash.
function deliveryLocOf(p){
const parts = ['delivBuilding','delivFloor','delivSector']
.map(k => p[k]).filter(Boolean).map(locLabelFull);
const detail = (p.delivDetail || '').trim();
return parts.join(' / ') + (detail ? (parts.length ? ' — ' : '') + detail : '');
}
function buildLocationPickers(keepValues){
const cur = keepValues || {
building: (document.getElementById('wp_building') || {}).value || '',
@@ -2738,6 +2770,8 @@ function loadPackageIntoForm(p){
pkgKitOwnerId=p.kitOwnerId||'';
set('wp_kit_owner',p.kitOwner); set('wp_kit_date',p.kitDate); set('wp_mimo_time',p.mimoTime); set('wp_mimo_loc',p.mimoLoc);
buildKitOwnerPicker();
set('wp_deliv_detail', p.delivDetail);
buildDeliveryPickers({building:p.delivBuilding||'', floor:p.delivFloor||'', sector:p.delivSector||''});
set('wp_actual_hrs',p.actualHrs); set('wp_installed_qty',p.installedQty); set('wp_redlines',p.redlines); set('wp_lessons',p.lessons);
set('wp_bimlink',p.bimlink); set('wp_iff',p.iff); set('wp_model_area',p.modelArea);
set('wp_clash',p.clash); set('wp_scan_link',p.scanLink);
@@ -2826,6 +2860,8 @@ function newPackage(){
document.getElementById('wp_type').value=''; buildKitStatusOptions(''); pkgKitOwnerId=''; buildKitOwnerPicker(); document.getElementById('wp_cost').value='';
const prio=document.getElementById('wp_priority'); if(prio) prio.value=WP_PRIORITY_DEFAULT; // CR-003
buildLocationPickers({building:'', floor:'', sector:''}); // CR-004
buildDeliveryPickers({building:'', floor:'', sector:''}); // CR-012
{ const dd=document.getElementById('wp_deliv_detail'); if(dd) dd.value=''; }
['wp_iff','wp_clash'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';});
onClashChange();
pkgKind='iwp'; applyKind();

View File

@@ -393,6 +393,13 @@
<div class="field"><label>Kitting need date</label><input type="date" id="wp_kit_date"></div>
<div class="field"><label>MIMO sch. time</label><input type="datetime-local" id="wp_mimo_time"><div class="field-hint">scheduled material-move date &amp; time</div></div>
<div class="field"><label>MIMO location</label><input type="text" id="wp_mimo_loc" placeholder="staging / move location"></div>
<!-- CR-012 / T8.4: delivery uses the SHARED Building/Floor/Sector lists
(CR-004), never a parallel free-text copy; the detail field carries
the last fifty feet - lay-down area, shark cage, conduit tree. -->
<div class="field"><label>Delivery building</label><select id="wp_deliv_building" onchange="onDeliveryLocChange('building')"><option value="">Building…</option></select></div>
<div class="field"><label>Delivery floor</label><select id="wp_deliv_floor" onchange="onDeliveryLocChange('floor')"><option value="">Floor…</option></select></div>
<div class="field"><label>Delivery sector</label><select id="wp_deliv_sector" onchange="onDeliveryLocChange('sector')"><option value="">Sector…</option></select></div>
<div class="field"><label>Delivery detail</label><input type="text" id="wp_deliv_detail" placeholder="lay-down area, shark cage, conduit tree…"></div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Kitting: statuses, the owner, the filter — CR-009 (T8.1) + CR-010 (T8.2).
"""Kitting: statuses, owner, filter, delivery — CR-009/CR-010/CR-012 (T8.1/2/4).
The statuses become an explicit five (Not Started / Picking / Staged /
In Transit / Delivered) - fulfillment states, not free text. A value stored
@@ -25,6 +25,7 @@ import cdp # noqa: E40
from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
from sections_check import set_sop # noqa: E402
from stepper_check import dismiss_dialogs # noqa: E402
from qa_gate_check import api # noqa: E402
KIT = ["Not Started", "Picking", "Staged", "In Transit", "Delivered"]
@@ -186,6 +187,50 @@ def main():
page.eval("!!document.querySelector('[aria-label=%s]')"
% json.dumps("Filter by warehouse owner")))
# ── 5. CR-012: the delivery location (T8.4) ──────────────────────────
print(chr(10) + "5. CR-012: delivery on the shared lists")
code, b1 = api(base, "/api/projects/projA/locations", tok["root"], "POST",
{"level": "building", "name": "B-100"})
code, f1 = api(base, "/api/projects/projA/locations", tok["root"], "POST",
{"level": "floor", "parent_id": b1["id"], "name": "Level 3"})
code, s1 = api(base, "/api/projects/projA/locations", tok["root"], "POST",
{"level": "sector", "parent_id": f1["id"], "name": "Sector East"})
page.goto(base + "/wp-creation-index.html?project=projA")
dismiss_dialogs(page)
wait_creator(page)
settle(1.8)
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
opts = json.loads(page.eval(
"JSON.stringify([...document.getElementById('wp_deliv_building').options].map(o=>o.value))"))
wp_opts = json.loads(page.eval(
"JSON.stringify([...document.getElementById('wp_building').options].map(o=>o.value))"))
chk("the delivery building select is fed by the SAME shared list as CR-004's",
opts == wp_opts and len(opts) > 1, ascii_((opts, wp_opts)))
page.eval("document.getElementById('wp_deliv_building').value=%s; onDeliveryLocChange('building')"
% json.dumps(b1["path"]))
settle(0.3)
page.eval("document.getElementById('wp_deliv_floor').value=%s; onDeliveryLocChange('floor')"
% json.dumps(f1["path"]))
settle(0.3)
page.eval("document.getElementById('wp_deliv_sector').value=%s; onDeliveryLocChange('sector')"
% json.dumps(s1["path"]))
page.eval("document.getElementById('wp_deliv_detail').value='Shark cage 7, conduit tree C'")
got = json.loads(page.eval("""JSON.stringify((() => { const p = collectPackage();
return {b: p.delivBuilding, f: p.delivFloor, s: p.delivSector,
d: p.delivDetail, disp: p.deliveryLoc}; })())"""))
chk("the picked values persist as PATHS off the shared lists",
got["b"] == b1["path"] and got["f"] == f1["path"] and got["s"] == s1["path"],
ascii_(got))
chk("...the detail field persists", got["d"] == "Shark cage 7, conduit tree C")
chk("...and the composed display carries labels AND the detail",
"B-100" in got["disp"] and "Shark cage 7" in got["disp"], ascii_(got["disp"]))
page.eval("document.getElementById('wp_subject').value='deliv print'")
page.eval("document.getElementById('wp_type').value='Conduit Install'")
doc = page.eval("(() => { renderPackage(collectPackage()); "
"return document.getElementById('pkg-doc').innerHTML; })()")
chk("the delivery location prints on the package output",
"Delivery Location" in doc and "Shark cage 7" in doc, ascii_(doc, 120))
js_errors = [e for e in page.js_errors() if "beforeunload" not in e]
chk("no JavaScript errors anywhere in this run", not js_errors,
ascii_(js_errors[:2]))

View File

@@ -38,7 +38,9 @@ def ascii_(v, n=300):
def save_wp(base, tok, kit_status, extra=None):
data = {"constraints": [{"name": "Materials", "status": "cleared", "comment": ""}],
"kitStatus": kit_status, "kitOwner": "Sue", "kitOwnerId": "user_sue",
"distributionIds": ["user_pat"], "mimoLoc": "Staging 04, dock B"}
"distributionIds": ["user_pat"], "mimoLoc": "Staging 04, dock B",
# CR-012 (T8.4): the composed delivery location wins over mimoLoc
"deliveryLoc": "B-100 / Level 3 / Sector East — Shark cage 7"}
data.update(extra or {})
return api(base, "/api/wps", tok, "POST", {
"id": "wpKN1", "project_id": "projA", "number": "KN-1",
@@ -108,7 +110,8 @@ def main():
chk("the mail says old status, new status and who",
"In Transit" in body and "Delivered" in body and "Root" in body,
ascii_(body, 260))
chk("...and the delivery location", "Staging 04, dock B" in body)
chk("...and the delivery location - CR-012's composed value, not the "
"mimoLoc fallback", "Shark cage 7" in body and "Staging 04" not in body)
chk("...and a deep link to THAT package, not the app root",
"/wp-creation-index.html?project=projA&wp=wpKN1" in body)
chk("...in the house convention (greeting + automated-message footer)",