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:
|
Wave 8 adds these:
|
||||||
|
|
||||||
```bash
|
```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
|
python tests/kitting_notify_check.py # CR-011 - kitting mail, coalesced, gated 17 checks
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1605,7 +1605,7 @@ function collectPackage(){
|
|||||||
gateOverride:pkgGateOverride || undefined,
|
gateOverride:pkgGateOverride || undefined,
|
||||||
assets:pkgAssets.filter(a=>a.tag||a.link||a.desc),
|
assets:pkgAssets.filter(a=>a.tag||a.link||a.desc),
|
||||||
materials:pkgMaterials.filter(m=>m.qty||m.desc), attachments:pkgAttach.filter(a=>a.doc),
|
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'),
|
mimoTime:gv('wp_mimo_time'), mimoLoc:gv('wp_mimo_loc'),
|
||||||
constraints:pkgConstraints.map(c=>({name:c.name,status:c.status,comment:c.comment})),
|
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'),
|
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>Warehouse Owner</th><td>${cell(pkg.kitOwner)}</td></tr>
|
||||||
<tr><th>Kitting Need Date</th><td>${cell(pkg.kitDate)}</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>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>`);
|
</tbody></table>`);
|
||||||
|
|
||||||
let ct=`<table><thead><tr><th>Constraint</th><th style="width:90px">Status</th><th>Comment</th></tr></thead><tbody>`;
|
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(){
|
async function loadLocations(){
|
||||||
if(!activeProjectId){ wpLocations = []; wpLocationsLoaded = true; buildLocationPickers(); return; }
|
if(!activeProjectId){ wpLocations = []; wpLocationsLoaded = true; buildLocationPickers(); buildDeliveryPickers(); return; }
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/projects/' + encodeURIComponent(activeProjectId)
|
const r = await fetch('/api/projects/' + encodeURIComponent(activeProjectId)
|
||||||
+ '/locations?include_inactive=true',
|
+ '/locations?include_inactive=true',
|
||||||
@@ -2209,6 +2210,7 @@ async function loadLocations(){
|
|||||||
}
|
}
|
||||||
wpLocationsLoaded = true;
|
wpLocationsLoaded = true;
|
||||||
buildLocationPickers();
|
buildLocationPickers();
|
||||||
|
buildDeliveryPickers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fill one level's <select>, filtered to the children of `parent`.
|
/* 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
|
`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
|
inactive — otherwise opening an old package would silently blank its location
|
||||||
and the next save would write that blank back. */
|
and the next save would write that blank back. */
|
||||||
function fillLocSelect(level, parent, keep){
|
function fillLocSelect(level, parent, keep, fieldMap){
|
||||||
const sel = document.getElementById(LOC_FIELD[level]);
|
const sel = document.getElementById((fieldMap || LOC_FIELD)[level]);
|
||||||
if(!sel) return;
|
if(!sel) return;
|
||||||
const wanted = LOC_LEVELS.indexOf(level);
|
const wanted = LOC_LEVELS.indexOf(level);
|
||||||
const options = wpLocations.filter(n =>
|
const options = wpLocations.filter(n =>
|
||||||
@@ -2240,6 +2242,36 @@ function fillLocSelect(level, parent, keep){
|
|||||||
if(keep) sel.value = 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){
|
function buildLocationPickers(keepValues){
|
||||||
const cur = keepValues || {
|
const cur = keepValues || {
|
||||||
building: (document.getElementById('wp_building') || {}).value || '',
|
building: (document.getElementById('wp_building') || {}).value || '',
|
||||||
@@ -2738,6 +2770,8 @@ function loadPackageIntoForm(p){
|
|||||||
pkgKitOwnerId=p.kitOwnerId||'';
|
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);
|
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();
|
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_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_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);
|
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='';
|
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
|
const prio=document.getElementById('wp_priority'); if(prio) prio.value=WP_PRIORITY_DEFAULT; // CR-003
|
||||||
buildLocationPickers({building:'', floor:'', sector:''}); // CR-004
|
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='';});
|
['wp_iff','wp_clash'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';});
|
||||||
onClashChange();
|
onClashChange();
|
||||||
pkgKind='iwp'; applyKind();
|
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>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 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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 /
|
The statuses become an explicit five (Not Started / Picking / Staged /
|
||||||
In Transit / Delivered) - fulfillment states, not free text. A value stored
|
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 browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
|
||||||
from sections_check import set_sop # noqa: E402
|
from sections_check import set_sop # noqa: E402
|
||||||
from stepper_check import dismiss_dialogs # 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"]
|
KIT = ["Not Started", "Picking", "Staged", "In Transit", "Delivered"]
|
||||||
|
|
||||||
@@ -186,6 +187,50 @@ def main():
|
|||||||
page.eval("!!document.querySelector('[aria-label=%s]')"
|
page.eval("!!document.querySelector('[aria-label=%s]')"
|
||||||
% json.dumps("Filter by warehouse owner")))
|
% 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]
|
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,
|
chk("no JavaScript errors anywhere in this run", not js_errors,
|
||||||
ascii_(js_errors[:2]))
|
ascii_(js_errors[:2]))
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ def ascii_(v, n=300):
|
|||||||
def save_wp(base, tok, kit_status, extra=None):
|
def save_wp(base, tok, kit_status, extra=None):
|
||||||
data = {"constraints": [{"name": "Materials", "status": "cleared", "comment": ""}],
|
data = {"constraints": [{"name": "Materials", "status": "cleared", "comment": ""}],
|
||||||
"kitStatus": kit_status, "kitOwner": "Sue", "kitOwnerId": "user_sue",
|
"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 {})
|
data.update(extra or {})
|
||||||
return api(base, "/api/wps", tok, "POST", {
|
return api(base, "/api/wps", tok, "POST", {
|
||||||
"id": "wpKN1", "project_id": "projA", "number": "KN-1",
|
"id": "wpKN1", "project_id": "projA", "number": "KN-1",
|
||||||
@@ -108,7 +110,8 @@ def main():
|
|||||||
chk("the mail says old status, new status and who",
|
chk("the mail says old status, new status and who",
|
||||||
"In Transit" in body and "Delivered" in body and "Root" in body,
|
"In Transit" in body and "Delivered" in body and "Root" in body,
|
||||||
ascii_(body, 260))
|
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",
|
chk("...and a deep link to THAT package, not the app root",
|
||||||
"/wp-creation-index.html?project=projA&wp=wpKN1" in body)
|
"/wp-creation-index.html?project=projA&wp=wpKN1" in body)
|
||||||
chk("...in the house convention (greeting + automated-message footer)",
|
chk("...in the house convention (greeting + automated-message footer)",
|
||||||
|
|||||||
Reference in New Issue
Block a user