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:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user