|
|
|
@@ -25,6 +25,7 @@ from ..domain.children import NEVER_VERIFY, active_depts, co_touched, current_wh
|
|
|
|
|
from ..domain.pipeline import (
|
|
|
|
|
SignBody,
|
|
|
|
|
Write,
|
|
|
|
|
apply_field_cas,
|
|
|
|
|
check_row_version,
|
|
|
|
|
jsonable,
|
|
|
|
|
next_counter,
|
|
|
|
@@ -43,10 +44,36 @@ router = APIRouter(prefix="/api/packets", tags=["packet-children"])
|
|
|
|
|
|
|
|
|
|
A = policy.Action
|
|
|
|
|
|
|
|
|
|
TEXT_SHORT = 40 # weld type, size, process, wo_base
|
|
|
|
|
FILLER_MAX = 120
|
|
|
|
|
TEXT_SHORT = LIMITS["weld_text"] # weld type, size, process, wo_base
|
|
|
|
|
QC_FIELD_MAX = 120
|
|
|
|
|
|
|
|
|
|
# The weld vocabulary (the old WELD_TYPES and PROCESSES).
|
|
|
|
|
WELD_TYPES = ("Fillet", "Groove", "Fill", "Seal", "Tack")
|
|
|
|
|
PROCESSES = ("GMAW", "FCAW", "SMAW", "GTAW", "SAW")
|
|
|
|
|
DEFAULT_SIZE = '5/16"'
|
|
|
|
|
POSITION = frozenset({"fx", "fy", "bfx", "bfy"})
|
|
|
|
|
# The weld fields in the order the change note lists them, with their note names.
|
|
|
|
|
WELD_NOTE_NAMES = {
|
|
|
|
|
"type": "type",
|
|
|
|
|
"size": "size",
|
|
|
|
|
"process": "process",
|
|
|
|
|
"items": "items",
|
|
|
|
|
"wo_base": "wo",
|
|
|
|
|
"piece": "piece",
|
|
|
|
|
"stamp": "stamp",
|
|
|
|
|
"page": "sheet",
|
|
|
|
|
}
|
|
|
|
|
# The heat table bounds (the old HT_MIN, HT_MAX and the drag clamp).
|
|
|
|
|
HT_XY_MAX = 0.97
|
|
|
|
|
HT_S_MIN, HT_S_MAX = 0.6, 2.1
|
|
|
|
|
STAGE_FIELDS = {
|
|
|
|
|
"filler": LIMITS["filler"],
|
|
|
|
|
"filler_lot": LIMITS["filler"],
|
|
|
|
|
"preheat": LIMITS["preheat"],
|
|
|
|
|
"notes": LIMITS["notes"],
|
|
|
|
|
}
|
|
|
|
|
STAGE_NOTE_NAMES = {"filler": "filler", "filler_lot": "filler lot", "preheat": "preheat", "notes": "notes"}
|
|
|
|
|
|
|
|
|
|
# The warehouse hold reasons, a closed set (the old build took any text). "Other" needs a note.
|
|
|
|
|
WH_HOLD_REASONS = (
|
|
|
|
|
"Waiting on material (not in stock)",
|
|
|
|
@@ -86,6 +113,7 @@ WH_PULLING_FROM = {"hold": "Pulling resumed", "pulled": "Back to pulling"}
|
|
|
|
|
MSG_NO_STAGE = "This packet has no open stage."
|
|
|
|
|
MSG_NO_INSPECTION = "This packet has no open Q.C. inspection."
|
|
|
|
|
MSG_SHORT_OPEN = "This line has an open short. Use the short to change heat, issued or location."
|
|
|
|
|
MSG_VOID = "This sheet is VOID. Mark the current sheet."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- bodies
|
|
|
|
@@ -145,6 +173,8 @@ class WeldBody(It):
|
|
|
|
|
page: int | None = Field(default=None, ge=1, le=100_000)
|
|
|
|
|
fx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
fy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
wo_base: str | None = Field(default=None, max_length=200)
|
|
|
|
|
piece: int | None = Field(default=None, ge=1, le=100_000)
|
|
|
|
|
items: list[int] = Field(min_length=1, max_length=200)
|
|
|
|
@@ -158,6 +188,8 @@ class WeldPatchBody(It):
|
|
|
|
|
page: int | None = Field(default=None, ge=1, le=100_000)
|
|
|
|
|
fx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
fy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
wo_base: str | None = Field(default=None, max_length=200)
|
|
|
|
|
piece: int | None = Field(default=None, ge=1, le=100_000)
|
|
|
|
|
items: list[int] | None = Field(default=None, min_length=1, max_length=200)
|
|
|
|
@@ -176,6 +208,8 @@ class TagBody(It):
|
|
|
|
|
item: int = Field(ge=1, le=100_000)
|
|
|
|
|
fx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
fy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TagPatchBody(It):
|
|
|
|
@@ -183,19 +217,26 @@ class TagPatchBody(It):
|
|
|
|
|
item: int | None = Field(default=None, ge=1, le=100_000)
|
|
|
|
|
fx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
fy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfx: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
bfy: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HeatTableBody(It):
|
|
|
|
|
x: float = Field(allow_inf_nan=False)
|
|
|
|
|
y: float = Field(allow_inf_nan=False)
|
|
|
|
|
s: float = Field(gt=0, le=100, allow_inf_nan=False)
|
|
|
|
|
s: float = Field(allow_inf_nan=False)
|
|
|
|
|
carried_from: int | None = Field(default=None, ge=1, le=100_000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StageBody(It):
|
|
|
|
|
"""The flat body (with If-Match on the stage row), or `{set, base}` for field-level compare-and-set."""
|
|
|
|
|
|
|
|
|
|
filler: str | None = Field(default=None, max_length=500)
|
|
|
|
|
filler_lot: str | None = Field(default=None, max_length=500)
|
|
|
|
|
preheat: str | None = Field(default=None, max_length=500)
|
|
|
|
|
notes: str | None = Field(default=None, max_length=5000)
|
|
|
|
|
set: dict[str, str | None] | None = Field(default=None, max_length=10)
|
|
|
|
|
base: dict[str, str | None] | None = Field(default=None, max_length=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChecklistBody(It):
|
|
|
|
@@ -420,6 +461,51 @@ def _tag(conn: Any, packet_id: int, no: int) -> dict[str, Any]:
|
|
|
|
|
return row
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _page_row(conn: Any, packet_id: int, page: int) -> dict[str, Any] | None:
|
|
|
|
|
return packets.one(
|
|
|
|
|
conn, "SELECT n, type, dwg, rev, void_at FROM packet_pages WHERE packet_id = %s AND n = %s", (packet_id, page)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _drawing_page(conn: Any, packet_id: int, page: int) -> dict[str, Any]:
|
|
|
|
|
"""A drawing page of the packet, live or VOID. 422 invalid_page otherwise."""
|
|
|
|
|
pg = _page_row(conn, packet_id, page)
|
|
|
|
|
if pg is None or pg["type"] != "drawing":
|
|
|
|
|
raise ApiError(422, "invalid_page", f"There is no drawing sheet {page} on this packet.")
|
|
|
|
|
return pg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _live_page(conn: Any, packet_id: int, page: int | None) -> None:
|
|
|
|
|
"""A mark goes on a live drawing page. 422 void_page on a VOID sheet; nothing is checked without a page."""
|
|
|
|
|
if page is None:
|
|
|
|
|
return
|
|
|
|
|
if _drawing_page(conn, packet_id, page)["void_at"] is not None:
|
|
|
|
|
raise ApiError(422, "void_page", MSG_VOID)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _not_void(conn: Any, packet_id: int, page: int | None) -> None:
|
|
|
|
|
"""An existing mark on a VOID sheet is never changed."""
|
|
|
|
|
if page is None:
|
|
|
|
|
return
|
|
|
|
|
pg = _page_row(conn, packet_id, page)
|
|
|
|
|
if pg is not None and pg["void_at"] is not None:
|
|
|
|
|
raise ApiError(422, "void_page", MSG_VOID)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _page_text(pg: dict[str, Any] | None, page: int) -> str:
|
|
|
|
|
"""A sheet in a log note: its drawing number, else "sheet 2"."""
|
|
|
|
|
dwg = (pg or {}).get("dwg")
|
|
|
|
|
return str(dwg) if dwg else f"sheet {page}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _num_text(value: Any) -> str:
|
|
|
|
|
return f"{round(float(value), 2):g}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _co_no_text(no: int) -> str:
|
|
|
|
|
return packets.co_label(no)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _co(conn: Any, packet_id: int, no: int) -> dict[str, Any]:
|
|
|
|
|
row = packets.one(
|
|
|
|
|
conn,
|
|
|
|
@@ -428,7 +514,7 @@ def _co(conn: Any, packet_id: int, no: int) -> dict[str, Any]:
|
|
|
|
|
(packet_id, no),
|
|
|
|
|
)
|
|
|
|
|
if row is None:
|
|
|
|
|
raise ApiError(404, "not_found", f"There is no change order {no} on this packet.")
|
|
|
|
|
raise ApiError(404, "not_found", f"There is no change order {_co_no_text(no)} on this packet.")
|
|
|
|
|
return row
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -437,7 +523,7 @@ def _co_state(row: dict[str, Any], want: str, what: str) -> None:
|
|
|
|
|
raise ApiError(
|
|
|
|
|
409,
|
|
|
|
|
"co_state",
|
|
|
|
|
f"Change order {row['co_no']} is {row['review']}. Only a {want} change order can be {what}.",
|
|
|
|
|
f"Change order {_co_no_text(row['co_no'])} is {row['review']}. Only a {want} change order can be {what}.",
|
|
|
|
|
extra={"current": {"co_no": row["co_no"], "review": row["review"], "hold": row["hold"]}},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -785,12 +871,22 @@ def receive_short(
|
|
|
|
|
|
|
|
|
|
def _weld_values(conn: Any, packet_id: int, body: WeldBody | WeldPatchBody, given: set[str]) -> dict[str, Any]:
|
|
|
|
|
values: dict[str, Any] = {}
|
|
|
|
|
for k in ("page", "fx", "fy", "piece"):
|
|
|
|
|
for k in ("page", "fx", "fy", "bfx", "bfy", "piece"):
|
|
|
|
|
if k in given:
|
|
|
|
|
values[k] = getattr(body, k)
|
|
|
|
|
for k in ("wo_base", "type", "size", "process"):
|
|
|
|
|
if k in given:
|
|
|
|
|
values[k] = clean_opt(getattr(body, k), TEXT_SHORT)
|
|
|
|
|
if values.get("type") is not None and values["type"] not in WELD_TYPES:
|
|
|
|
|
raise ApiError(422, "invalid_weld", "Pick a weld type from the list.")
|
|
|
|
|
if values.get("process") is not None and values["process"] not in PROCESSES:
|
|
|
|
|
raise ApiError(422, "invalid_weld", "Pick a process from the list.")
|
|
|
|
|
if values.get("wo_base") is not None:
|
|
|
|
|
bases = {
|
|
|
|
|
r[0] for r in conn.execute("SELECT wo_base FROM packet_work_orders WHERE packet_id = %s", (packet_id,))
|
|
|
|
|
}
|
|
|
|
|
if values["wo_base"] not in bases:
|
|
|
|
|
raise ApiError(422, "invalid_wo", "Pick one of this packet's work orders.")
|
|
|
|
|
if "stamp" in given:
|
|
|
|
|
values["stamp"] = _stamp(body.stamp)
|
|
|
|
|
if "items" in given:
|
|
|
|
@@ -800,6 +896,33 @@ def _weld_values(conn: Any, packet_id: int, body: WeldBody | WeldPatchBody, give
|
|
|
|
|
return values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _items_text(items: Any) -> str:
|
|
|
|
|
return "items " + " + ".join(map(str, items or []))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _weld_note(values: dict[str, Any]) -> str:
|
|
|
|
|
"""The note of a new weld: Fillet 5/16" · items 1 + 2 · L662001 #1."""
|
|
|
|
|
desc = " ".join(str(x) for x in (values.get("type"), values.get("size")) if x)
|
|
|
|
|
wo = values.get("wo_base")
|
|
|
|
|
if wo and values.get("piece") is not None:
|
|
|
|
|
wo = f"{wo} #{values['piece']}"
|
|
|
|
|
return " · ".join(x for x in (desc, _items_text(values["items"]), wo) if x)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _change_note(changes: dict[str, Any]) -> str | None:
|
|
|
|
|
"""The changed weld fields: type Groove · items 1 + 3."""
|
|
|
|
|
parts: list[str] = []
|
|
|
|
|
for k, name in WELD_NOTE_NAMES.items():
|
|
|
|
|
if k not in changes:
|
|
|
|
|
continue
|
|
|
|
|
v = changes[k]
|
|
|
|
|
if k == "items":
|
|
|
|
|
parts.append(_items_text(v))
|
|
|
|
|
else:
|
|
|
|
|
parts.append(f"{name} {v}" if v not in (None, "") else f"{name} cleared")
|
|
|
|
|
return " · ".join(parts) or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{packet_id}/welds", dependencies=WRITE)
|
|
|
|
|
def add_weld(
|
|
|
|
|
packet_id: int,
|
|
|
|
@@ -816,6 +939,9 @@ def add_weld(
|
|
|
|
|
d, p, packet_id, A.EDIT_WELDS, request=request, it_reason=body.it_reason, policy_principal=pp
|
|
|
|
|
) as w:
|
|
|
|
|
values = _weld_values(conn, packet_id, body, given)
|
|
|
|
|
_live_page(conn, packet_id, values.get("page"))
|
|
|
|
|
if values.get("size") is None:
|
|
|
|
|
values["size"] = DEFAULT_SIZE
|
|
|
|
|
dept = str(w.packet["dept_code"])
|
|
|
|
|
worker = pp.level(dept) == 1
|
|
|
|
|
welder_id = w.user_id if worker else w.packet["welder_id"]
|
|
|
|
@@ -824,15 +950,17 @@ def add_weld(
|
|
|
|
|
stage = _open_stage(conn, packet_id)
|
|
|
|
|
no = next_counter(conn, packet_id, "weld")
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO welds (packet_id, weld_no, page, fx, fy, wo_base, piece, items, type, size, process, stamp,"
|
|
|
|
|
" welder_id, dept_code, stage_id, by) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,"
|
|
|
|
|
" %s)",
|
|
|
|
|
"INSERT INTO welds (packet_id, weld_no, page, fx, fy, bfx, bfy, wo_base, piece, items, type, size, process,"
|
|
|
|
|
" stamp, welder_id, dept_code, stage_id, by) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,"
|
|
|
|
|
" %s, %s, %s, %s, %s)",
|
|
|
|
|
(
|
|
|
|
|
packet_id,
|
|
|
|
|
no,
|
|
|
|
|
values.get("page"),
|
|
|
|
|
values.get("fx"),
|
|
|
|
|
values.get("fy"),
|
|
|
|
|
values.get("bfx"),
|
|
|
|
|
values.get("bfy"),
|
|
|
|
|
values.get("wo_base"),
|
|
|
|
|
values.get("piece"),
|
|
|
|
|
values["items"],
|
|
|
|
@@ -848,8 +976,7 @@ def add_weld(
|
|
|
|
|
)
|
|
|
|
|
w.changed("welds")
|
|
|
|
|
w.detail["weld_no"] = no
|
|
|
|
|
desc = " ".join(x for x in (values.get("type"), values.get("size")) if x)
|
|
|
|
|
w.log(f"Logged W{no}", " · ".join(x for x in (desc, "items " + " + ".join(map(str, values["items"]))) if x))
|
|
|
|
|
w.log(f"Weld W{no} marked", _weld_note(values))
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -870,6 +997,7 @@ def patch_weld(
|
|
|
|
|
) as w:
|
|
|
|
|
row = _weld(conn, packet_id, no)
|
|
|
|
|
check_row_version(row, if_match)
|
|
|
|
|
_not_void(conn, packet_id, row["page"])
|
|
|
|
|
values = _weld_values(conn, packet_id, body, set(body.model_fields_set))
|
|
|
|
|
changes = _diff(row, values)
|
|
|
|
|
if "items" in changes and sorted(row["items"] or []) == changes["items"]:
|
|
|
|
@@ -877,10 +1005,15 @@ def patch_weld(
|
|
|
|
|
if not changes:
|
|
|
|
|
w.skip()
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
if "page" in changes:
|
|
|
|
|
_live_page(conn, packet_id, changes["page"])
|
|
|
|
|
update_row(conn, "welds", row["id"], changes)
|
|
|
|
|
w.changed(*[f"weld.{k}" for k in changes])
|
|
|
|
|
w.detail["weld_no"] = no
|
|
|
|
|
w.log(f"Edited W{no}")
|
|
|
|
|
if set(changes) <= POSITION:
|
|
|
|
|
w.log(f"Weld W{no} moved")
|
|
|
|
|
else:
|
|
|
|
|
w.log(f"Weld W{no} changed", _change_note(changes))
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -905,6 +1038,7 @@ def remove_weld(
|
|
|
|
|
row = _weld(conn, packet_id, no)
|
|
|
|
|
if if_match is not None:
|
|
|
|
|
check_row_version(row, if_match)
|
|
|
|
|
_not_void(conn, packet_id, row["page"])
|
|
|
|
|
conn.execute(
|
|
|
|
|
"UPDATE welds SET removed_at = now(), removed_by = %s, removed_reason = %s, row_version = row_version + 1"
|
|
|
|
|
" WHERE id = %s",
|
|
|
|
@@ -912,7 +1046,7 @@ def remove_weld(
|
|
|
|
|
)
|
|
|
|
|
w.changed("welds")
|
|
|
|
|
w.detail["weld_no"] = no
|
|
|
|
|
w.log(f"Removed W{no}", reason)
|
|
|
|
|
w.log(f"Weld W{no} removed", reason)
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -920,7 +1054,7 @@ def remove_weld(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _tag_values(conn: Any, packet_id: int, body: TagBody | TagPatchBody, given: set[str]) -> dict[str, Any]:
|
|
|
|
|
values: dict[str, Any] = {k: getattr(body, k) for k in ("page", "fx", "fy") if k in given}
|
|
|
|
|
values: dict[str, Any] = {k: getattr(body, k) for k in ("page", "fx", "fy", "bfx", "bfy") if k in given}
|
|
|
|
|
if "item" in given:
|
|
|
|
|
if body.item is None:
|
|
|
|
|
raise ApiError(422, "invalid_items", "Pick the BOM item this tag marks.")
|
|
|
|
@@ -942,10 +1076,22 @@ def add_tag(
|
|
|
|
|
d, p, packet_id, A.EDIT_HEATS, request=request, it_reason=body.it_reason, policy_principal=pp
|
|
|
|
|
) as w:
|
|
|
|
|
values = _tag_values(conn, packet_id, body, set(body.model_fields_set) | {"item"})
|
|
|
|
|
_live_page(conn, packet_id, values.get("page"))
|
|
|
|
|
no = next_counter(conn, packet_id, "heat")
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO heat_tags (packet_id, tag_no, page, item, fx, fy, by) VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
|
|
|
|
(packet_id, no, values.get("page"), values["item"], values.get("fx"), values.get("fy"), w.user_id),
|
|
|
|
|
"INSERT INTO heat_tags (packet_id, tag_no, page, item, fx, fy, bfx, bfy, by)"
|
|
|
|
|
" VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
|
|
|
|
(
|
|
|
|
|
packet_id,
|
|
|
|
|
no,
|
|
|
|
|
values.get("page"),
|
|
|
|
|
values["item"],
|
|
|
|
|
values.get("fx"),
|
|
|
|
|
values.get("fy"),
|
|
|
|
|
values.get("bfx"),
|
|
|
|
|
values.get("bfy"),
|
|
|
|
|
w.user_id,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
w.changed("heat_tags")
|
|
|
|
|
w.detail["tag_no"] = no
|
|
|
|
@@ -970,14 +1116,18 @@ def patch_tag(
|
|
|
|
|
) as w:
|
|
|
|
|
row = _tag(conn, packet_id, no)
|
|
|
|
|
check_row_version(row, if_match)
|
|
|
|
|
_not_void(conn, packet_id, row["page"])
|
|
|
|
|
changes = _diff(row, _tag_values(conn, packet_id, body, set(body.model_fields_set)))
|
|
|
|
|
if not changes:
|
|
|
|
|
w.skip()
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
if "page" in changes:
|
|
|
|
|
_live_page(conn, packet_id, changes["page"])
|
|
|
|
|
update_row(conn, "heat_tags", row["id"], changes)
|
|
|
|
|
w.changed(*[f"heat_tag.{k}" for k in changes])
|
|
|
|
|
w.detail["tag_no"] = no
|
|
|
|
|
w.log(f"Heat tag H{no} changed", f"item {changes['item']}" if "item" in changes else None)
|
|
|
|
|
item = changes.get("item", row["item"])
|
|
|
|
|
w.log(f"Heat tag H{no} {'moved' if set(changes) <= POSITION else 'changed'}", f"item {item}")
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1001,6 +1151,7 @@ def remove_tag(
|
|
|
|
|
row = _tag(conn, packet_id, no)
|
|
|
|
|
if if_match is not None:
|
|
|
|
|
check_row_version(row, if_match)
|
|
|
|
|
_not_void(conn, packet_id, row["page"])
|
|
|
|
|
conn.execute(
|
|
|
|
|
"UPDATE heat_tags SET removed_at = now(), removed_by = %s, removed_reason = %s,"
|
|
|
|
|
" row_version = row_version + 1 WHERE id = %s",
|
|
|
|
@@ -1008,7 +1159,7 @@ def remove_tag(
|
|
|
|
|
)
|
|
|
|
|
w.changed("heat_tags")
|
|
|
|
|
w.detail["tag_no"] = no
|
|
|
|
|
w.log(f"Heat tag H{no} removed", reason)
|
|
|
|
|
w.log(f"Heat tag H{no} removed", " · ".join(x for x in (f"item {row['item']}", reason) if x))
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1021,29 +1172,50 @@ def put_heat_table(
|
|
|
|
|
p: Principal = Depends(current_principal), # noqa: B008
|
|
|
|
|
d: Db = Depends(get_db), # noqa: B008
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Place the heat table on a page. The same placement again changes nothing."""
|
|
|
|
|
"""Place, move or resize the heat table on a drawing page. The same placement again changes nothing."""
|
|
|
|
|
if page < 1:
|
|
|
|
|
raise ApiError(422, "invalid_page", "Pages start at 1.")
|
|
|
|
|
if not (0 <= body.x <= HT_XY_MAX and 0 <= body.y <= HT_XY_MAX and HT_S_MIN <= body.s <= HT_S_MAX):
|
|
|
|
|
raise ApiError(
|
|
|
|
|
422,
|
|
|
|
|
"invalid_heat_table",
|
|
|
|
|
f"Keep the heat table on the sheet: x and y from 0 to {HT_XY_MAX}, size from {HT_S_MIN} to {HT_S_MAX}.",
|
|
|
|
|
)
|
|
|
|
|
pp = _pp(d, p)
|
|
|
|
|
conn = d.conn
|
|
|
|
|
with packet_write(
|
|
|
|
|
d, p, packet_id, A.EDIT_HEAT_TABLE, request=request, it_reason=body.it_reason, policy_principal=pp
|
|
|
|
|
) as w:
|
|
|
|
|
pg = _drawing_page(conn, packet_id, page)
|
|
|
|
|
if pg["void_at"] is not None:
|
|
|
|
|
raise ApiError(422, "void_page", MSG_VOID)
|
|
|
|
|
if body.carried_from is not None and _page_row(conn, packet_id, body.carried_from) is None:
|
|
|
|
|
raise ApiError(422, "invalid_page", f"There is no sheet {body.carried_from} on this packet.")
|
|
|
|
|
cur = packets.one(
|
|
|
|
|
conn, "SELECT x, y, s FROM heat_table_placements WHERE packet_id = %s AND page = %s", (packet_id, page)
|
|
|
|
|
conn,
|
|
|
|
|
"SELECT x, y, s, carried_from FROM heat_table_placements WHERE packet_id = %s AND page = %s",
|
|
|
|
|
(packet_id, page),
|
|
|
|
|
)
|
|
|
|
|
if cur is not None and (cur["x"], cur["y"], cur["s"]) == (body.x, body.y, body.s):
|
|
|
|
|
w.skip()
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
carried = body.carried_from if "carried_from" in body.model_fields_set else (cur or {}).get("carried_from")
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO heat_table_placements (packet_id, page, x, y, s, by) VALUES (%s, %s, %s, %s, %s, %s)"
|
|
|
|
|
"INSERT INTO heat_table_placements (packet_id, page, x, y, s, carried_from, by)"
|
|
|
|
|
" VALUES (%s, %s, %s, %s, %s, %s, %s)"
|
|
|
|
|
" ON CONFLICT (packet_id, page) DO UPDATE SET x = EXCLUDED.x, y = EXCLUDED.y, s = EXCLUDED.s,"
|
|
|
|
|
" by = EXCLUDED.by, at = now()",
|
|
|
|
|
(packet_id, page, body.x, body.y, body.s, w.user_id),
|
|
|
|
|
" carried_from = EXCLUDED.carried_from, by = EXCLUDED.by, at = now()",
|
|
|
|
|
(packet_id, page, body.x, body.y, body.s, carried, w.user_id),
|
|
|
|
|
)
|
|
|
|
|
w.changed("heat_table")
|
|
|
|
|
w.detail["page"] = page
|
|
|
|
|
w.log(f"Heat table placed on page {page}")
|
|
|
|
|
where = _page_text(pg, page)
|
|
|
|
|
if cur is None:
|
|
|
|
|
w.log("Heat table placed on drawing", where)
|
|
|
|
|
elif (cur["x"], cur["y"]) != (body.x, body.y):
|
|
|
|
|
w.log("Heat table moved", where)
|
|
|
|
|
else:
|
|
|
|
|
w.log("Heat table resized", f"{'A+' if body.s > cur['s'] else 'A-'} {_num_text(body.s)}")
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1062,14 +1234,15 @@ def remove_heat_table(
|
|
|
|
|
with packet_write(
|
|
|
|
|
d, p, packet_id, A.EDIT_HEAT_TABLE, request=request, it_reason=body.it_reason, policy_principal=pp
|
|
|
|
|
) as w:
|
|
|
|
|
_not_void(conn, packet_id, page)
|
|
|
|
|
gone = conn.execute(
|
|
|
|
|
"DELETE FROM heat_table_placements WHERE packet_id = %s AND page = %s RETURNING id", (packet_id, page)
|
|
|
|
|
).fetchone()
|
|
|
|
|
if gone is None:
|
|
|
|
|
raise ApiError(404, "not_found", f"There is no heat table on page {page}.")
|
|
|
|
|
raise ApiError(404, "not_found", f"There is no heat table on sheet {page}.")
|
|
|
|
|
w.changed("heat_table")
|
|
|
|
|
w.detail["page"] = page
|
|
|
|
|
w.log(f"Heat table removed from page {page}")
|
|
|
|
|
w.log("Heat table removed from drawing", _page_text(_page_row(conn, packet_id, page), page))
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1084,27 +1257,58 @@ def patch_stage(
|
|
|
|
|
p: Principal = Depends(current_principal), # noqa: B008
|
|
|
|
|
d: Db = Depends(get_db), # noqa: B008
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Filler, filler lot, preheat and notes of the open stage. If-Match is the stage's row_version."""
|
|
|
|
|
if_match = parse_if_match(request)
|
|
|
|
|
limits = {"filler": FILLER_MAX, "filler_lot": FILLER_MAX, "preheat": FILLER_MAX, "notes": LIMITS["notes"]}
|
|
|
|
|
values = {k: clean_opt(getattr(body, k), n) for k, n in limits.items() if k in body.model_fields_set}
|
|
|
|
|
"""Filler, filler lot, preheat and notes of the open stage (the weld record).
|
|
|
|
|
|
|
|
|
|
Either the flat body with If-Match (the stage's row_version), or `{set, base}`: field-level compare-and-set,
|
|
|
|
|
409 field_conflict with `current` for a field someone else changed since `base`.
|
|
|
|
|
"""
|
|
|
|
|
cas = body.set is not None
|
|
|
|
|
if cas:
|
|
|
|
|
assert body.set is not None
|
|
|
|
|
bad = sorted(k for k in body.set if k not in STAGE_FIELDS)
|
|
|
|
|
if bad:
|
|
|
|
|
raise ApiError(422, "invalid_field", f"Not a weld record field: {', '.join(bad)}.")
|
|
|
|
|
values = {k: clean_opt(v, STAGE_FIELDS[k]) for k, v in body.set.items()}
|
|
|
|
|
base = {k: clean_opt(v, STAGE_FIELDS[k]) for k, v in (body.base or {}).items() if k in STAGE_FIELDS}
|
|
|
|
|
if_match = None
|
|
|
|
|
else:
|
|
|
|
|
values = {k: clean_opt(getattr(body, k), n) for k, n in STAGE_FIELDS.items() if k in body.model_fields_set}
|
|
|
|
|
base = {}
|
|
|
|
|
if_match = parse_if_match(request)
|
|
|
|
|
pp = _pp(d, p)
|
|
|
|
|
conn = d.conn
|
|
|
|
|
with packet_write(
|
|
|
|
|
d, p, packet_id, A.EDIT_STAGE_FIELDS, request=request, it_reason=body.it_reason, policy_principal=pp
|
|
|
|
|
) as w:
|
|
|
|
|
stage = _need_stage(conn, packet_id)
|
|
|
|
|
check_row_version(stage, if_match)
|
|
|
|
|
changes = _diff(stage, values)
|
|
|
|
|
if cas:
|
|
|
|
|
changes = apply_field_cas({k: stage[k] for k in STAGE_FIELDS}, values, base)
|
|
|
|
|
else:
|
|
|
|
|
check_row_version(stage, if_match)
|
|
|
|
|
changes = _diff(stage, values)
|
|
|
|
|
if not changes:
|
|
|
|
|
w.skip()
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
update_row(conn, "packet_stages", stage["id"], changes)
|
|
|
|
|
w.changed(*[f"stage.{k}" for k in changes])
|
|
|
|
|
w.log("Stage details changed", f"filler {changes['filler']}" if changes.get("filler") else None)
|
|
|
|
|
w.log("Weld record changed", _stage_note(changes))
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stage_note(changes: dict[str, Any]) -> str | None:
|
|
|
|
|
"""The changed weld record fields: filler ER70S-6 · filler lot 1234."""
|
|
|
|
|
parts: list[str] = []
|
|
|
|
|
for k, name in STAGE_NOTE_NAMES.items():
|
|
|
|
|
if k not in changes:
|
|
|
|
|
continue
|
|
|
|
|
v = changes[k]
|
|
|
|
|
if k == "notes":
|
|
|
|
|
parts.append("notes changed" if v else "notes cleared")
|
|
|
|
|
else:
|
|
|
|
|
parts.append(f"{name} {v}" if v else f"{name} cleared")
|
|
|
|
|
return " · ".join(parts) or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{packet_id}/stage/checklist/{key}", dependencies=WRITE)
|
|
|
|
|
def put_checklist(
|
|
|
|
|
packet_id: int,
|
|
|
|
@@ -1121,7 +1325,8 @@ def put_checklist(
|
|
|
|
|
d, p, packet_id, A.EDIT_CHECKLIST, request=request, it_reason=body.it_reason, policy_principal=pp
|
|
|
|
|
) as w:
|
|
|
|
|
weld = bool(w.state.weld_dept)
|
|
|
|
|
if key not in checklists.checklist_keys(weld):
|
|
|
|
|
templates = w.children.templates if w.children is not None else None
|
|
|
|
|
if key not in checklists.checklist_keys(weld, templates):
|
|
|
|
|
raise ApiError(422, "invalid_key", "That is not an item on this department's checklist.")
|
|
|
|
|
stage = _need_stage(conn, packet_id)
|
|
|
|
|
cur = (stage["checklist"] or {}).get(key)
|
|
|
|
@@ -1140,7 +1345,8 @@ def put_checklist(
|
|
|
|
|
"UPDATE packet_stages SET checklist = jsonb_set(checklist, ARRAY[%s]::text[], %s) WHERE id = %s",
|
|
|
|
|
(key, Jsonb({"done": False}), stage["id"]),
|
|
|
|
|
)
|
|
|
|
|
text = next((i["text"] for i in checklists.checklist_for(weld, None, None, None) if i["key"] == key), key)
|
|
|
|
|
items = checklists.checklist_for(weld, w.packet["drawing"], w.packet["rev"], w.packet["wps"], templates)
|
|
|
|
|
text = next((i["text"] for i in items if i["key"] == key), key)
|
|
|
|
|
w.changed(f"checklist.{key}")
|
|
|
|
|
w.log(("Checked: " if body.done else "Unchecked: ") + text)
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
@@ -1453,7 +1659,7 @@ def issue_co(
|
|
|
|
|
_co_log_detail(w, no)
|
|
|
|
|
w.detail["required"] = required
|
|
|
|
|
verify = f"verify: {', '.join(required)}" if required else "no departments to verify"
|
|
|
|
|
w.log(f"Change order {no} issued", " · ".join(x for x in (reason, ecn, verify) if x))
|
|
|
|
|
w.log(f"Change order {_co_no_text(no)} issued", " · ".join(x for x in (reason, ecn, verify) if x))
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1605,6 +1811,7 @@ def _apply_feed_co(w: Write, co: dict[str, Any]) -> str | None:
|
|
|
|
|
first, count, old = pv.replace_drawing_pages(
|
|
|
|
|
conn, pid, asset, dwg=dwg, rev=rev, co_id=int(co["id"]), void_by=w.user_id
|
|
|
|
|
)
|
|
|
|
|
_carry_heat_tables(w, old, first, count)
|
|
|
|
|
for i in range(max(len(old), count)):
|
|
|
|
|
o = old[i] if i < len(old) else None
|
|
|
|
|
new = i < count
|
|
|
|
@@ -1642,6 +1849,26 @@ def _apply_feed_co(w: Write, co: dict[str, Any]) -> str | None:
|
|
|
|
|
return "; ".join(notes) or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _carry_heat_tables(w: Write, old: list[dict[str, Any]], first: int, count: int) -> None:
|
|
|
|
|
"""Copy each heat table placement from a voided sheet to the sheet that replaces it (the old htCarry), with
|
|
|
|
|
carried_from set to the old sheet. Welds and heat tags stay on the VOID sheet."""
|
|
|
|
|
for i, o in enumerate(old):
|
|
|
|
|
new_n = first + i if i < count else first
|
|
|
|
|
cur = packets.one(
|
|
|
|
|
w.conn, "SELECT x, y, s FROM heat_table_placements WHERE packet_id = %s AND page = %s", (w.id, o["n"])
|
|
|
|
|
)
|
|
|
|
|
if cur is None:
|
|
|
|
|
continue
|
|
|
|
|
done = w.conn.execute(
|
|
|
|
|
"INSERT INTO heat_table_placements (packet_id, page, x, y, s, carried_from, by)"
|
|
|
|
|
" VALUES (%s, %s, %s, %s, %s, %s, %s) ON CONFLICT (packet_id, page) DO NOTHING RETURNING id",
|
|
|
|
|
(w.id, new_n, cur["x"], cur["y"], cur["s"], o["n"], w.user_id),
|
|
|
|
|
).fetchone()
|
|
|
|
|
if done is not None:
|
|
|
|
|
w.changed("heat_table")
|
|
|
|
|
w.log_also("Heat table copied to new sheet", "from the VOID copy, check its position on the new revision")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{packet_id}/change-orders/{no}/approve", dependencies=WRITE)
|
|
|
|
|
def approve_co(
|
|
|
|
|
packet_id: int,
|
|
|
|
@@ -1699,7 +1926,7 @@ def approve_co(
|
|
|
|
|
for code in co_touched(conn, w.packet):
|
|
|
|
|
conn.execute("INSERT INTO co_required_depts (co_id, dept_code) VALUES (%s, %s)", (co["id"], code))
|
|
|
|
|
_co_log_detail(w, no)
|
|
|
|
|
w.log(f"Change order {no} approved", note)
|
|
|
|
|
w.log(f"Change order {_co_no_text(no)} approved", note)
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1743,7 +1970,7 @@ def reject_co(
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
_co_log_detail(w, no)
|
|
|
|
|
w.log(f"Change order {no} rejected", reason)
|
|
|
|
|
w.log(f"Change order {_co_no_text(no)} rejected", reason)
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1775,7 +2002,7 @@ def hold_co(
|
|
|
|
|
{"hold": body.hold, "held_by": w.user_id if body.hold else None, "held_at": now if body.hold else None},
|
|
|
|
|
)
|
|
|
|
|
_co_log_detail(w, no)
|
|
|
|
|
w.log(f"Change order {no} {'holds the packet' if body.hold else 'no longer holds the packet'}")
|
|
|
|
|
w.log(f"Change order {_co_no_text(no)} {'holds the packet' if body.hold else 'no longer holds the packet'}")
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1811,12 +2038,12 @@ def verify_co(
|
|
|
|
|
_co_state(co, "approved", "verified")
|
|
|
|
|
required = {r[0] for r in conn.execute("SELECT dept_code FROM co_required_depts WHERE co_id = %s", (co["id"],))}
|
|
|
|
|
if dept not in required:
|
|
|
|
|
raise ApiError(422, "invalid_dept", f"Change order {no} does not need verification by {dept}.")
|
|
|
|
|
raise ApiError(422, "invalid_dept", f"Change order {_co_no_text(no)} does not need verification by {dept}.")
|
|
|
|
|
done = conn.execute(
|
|
|
|
|
"SELECT 1 FROM co_verifications WHERE co_id = %s AND dept_code = %s", (co["id"], dept)
|
|
|
|
|
).fetchone()
|
|
|
|
|
if done is not None:
|
|
|
|
|
raise ApiError(409, "already_verified", f"{dept} already verified change order {no}.")
|
|
|
|
|
raise ApiError(409, "already_verified", f"{dept} already verified change order {_co_no_text(no)}.")
|
|
|
|
|
try:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO co_verifications (co_id, dept_code, by, note, signature_id, it_reason)"
|
|
|
|
@@ -1824,9 +2051,17 @@ def verify_co(
|
|
|
|
|
(co["id"], dept, w.user_id, note, w.signature.id if w.signature else None, w.it_reason),
|
|
|
|
|
)
|
|
|
|
|
except psycopg.errors.UniqueViolation:
|
|
|
|
|
raise ApiError(409, "already_verified", f"{dept} already verified change order {no}.") from None
|
|
|
|
|
raise ApiError(
|
|
|
|
|
409, "already_verified", f"{dept} already verified change order {_co_no_text(no)}."
|
|
|
|
|
) from None
|
|
|
|
|
_co_log_detail(w, no)
|
|
|
|
|
w.detail["dept"] = dept
|
|
|
|
|
name = packets.dept_names(conn).get(dept, dept)
|
|
|
|
|
w.log(f"Change order {no} verified for {name}", note)
|
|
|
|
|
names = packets.dept_names(conn)
|
|
|
|
|
verified = {r[0] for r in conn.execute("SELECT dept_code FROM co_verifications WHERE co_id = %s", (co["id"],))}
|
|
|
|
|
left = [names.get(c, c) for c in sorted(required) if c not in verified]
|
|
|
|
|
tail = f"still waiting on {', '.join(left)}" if left else "change order fully verified"
|
|
|
|
|
w.log(
|
|
|
|
|
f"Change order {_co_no_text(no)} verified by {names.get(dept, dept)}",
|
|
|
|
|
" · ".join(x for x in (note, tail) if x),
|
|
|
|
|
)
|
|
|
|
|
return _read(d, pp, packet_id)
|
|
|
|
|