makea-cli 0.2.10__tar.gz → 0.2.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {makea_cli-0.2.10 → makea_cli-0.2.12}/CHANGELOG.md +37 -0
  2. {makea_cli-0.2.10 → makea_cli-0.2.12}/PKG-INFO +1 -1
  3. makea_cli-0.2.12/makea_cli/__init__.py +1 -0
  4. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/api_client.py +255 -33
  5. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/brand/cmd.py +67 -20
  6. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/factory/cmd.py +104 -3
  7. {makea_cli-0.2.10 → makea_cli-0.2.12}/pyproject.toml +1 -1
  8. {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/golden_manifest.json +275 -18
  9. {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/test_persona_commands.py +14 -0
  10. makea_cli-0.2.12/tests/test_pm_reads.py +227 -0
  11. {makea_cli-0.2.10 → makea_cli-0.2.12}/uv.lock +4 -2
  12. makea_cli-0.2.10/makea_cli/__init__.py +0 -1
  13. makea_cli-0.2.10/tests/test_pm_reads.py +0 -69
  14. {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
  15. {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
  16. {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/link-supplier-user/SKILL.md +0 -0
  17. {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
  18. {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
  19. {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
  20. {makea_cli-0.2.10 → makea_cli-0.2.12}/.gitignore +0 -0
  21. {makea_cli-0.2.10 → makea_cli-0.2.12}/README.md +0 -0
  22. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea-collection-upload/SKILL.md +0 -0
  23. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea-collection-upload/scripts/create_products.py +0 -0
  24. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/__main__.py +0 -0
  25. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/auth_pkce.py +0 -0
  26. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/broker.py +0 -0
  27. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/__init__.py +0 -0
  28. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/_util.py +0 -0
  29. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/analytics/__init__.py +0 -0
  30. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/analytics/cmd.py +0 -0
  31. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/auth/__init__.py +0 -0
  32. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/auth/cmd.py +0 -0
  33. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/brand/__init__.py +0 -0
  34. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/designer/__init__.py +0 -0
  35. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/designer/cmd.py +0 -0
  36. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/directory/__init__.py +0 -0
  37. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/directory/cmd.py +0 -0
  38. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/document/__init__.py +0 -0
  39. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/document/cmd.py +0 -0
  40. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/email/__init__.py +0 -0
  41. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/email/cmd.py +0 -0
  42. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/factory/__init__.py +0 -0
  43. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/library/__init__.py +0 -0
  44. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/library/cmd.py +0 -0
  45. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/metrics/__init__.py +0 -0
  46. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/metrics/cmd.py +0 -0
  47. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/orders/__init__.py +0 -0
  48. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/orders/cmd.py +0 -0
  49. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/product/__init__.py +0 -0
  50. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/product/cmd.py +0 -0
  51. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/production/__init__.py +0 -0
  52. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/production/cmd.py +0 -0
  53. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/production/pricing_spec.py +0 -0
  54. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/supplier/__init__.py +0 -0
  55. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/supplier/cmd.py +0 -0
  56. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/config.py +0 -0
  57. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/errors.py +0 -0
  58. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/identity.py +0 -0
  59. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/main.py +0 -0
  60. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/output.py +0 -0
  61. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/posthog_client.py +0 -0
  62. {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/registry.py +0 -0
  63. {makea_cli-0.2.10 → makea_cli-0.2.12}/npm/makea-cli/README.md +0 -0
  64. {makea_cli-0.2.10 → makea_cli-0.2.12}/npm/makea-cli/makea-cli.mjs +0 -0
  65. {makea_cli-0.2.10 → makea_cli-0.2.12}/npm/makea-cli/package.json +0 -0
  66. {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/test_broker.py +0 -0
  67. {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/test_output_contract.py +0 -0
  68. {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/test_registry.py +0 -0
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.12
4
+
5
+ Fixes from a live exam of Beeboo (2026-10-10):
6
+
7
+ - `designer list supplier-link --product <id>` without `--link-type` now reads
8
+ SAMPLING and PRODUCTION quotes (the backend defaults to sampling, so a
9
+ confirmed production quote never showed). Each link carries its
10
+ `link_type`, and a new `quotes` list gives every quote's factory, type,
11
+ whether it is the awarded production quote, and its bulk tiers with the
12
+ quantities they cover; a sampling quote's tiers are marked indicative.
13
+ - `designer list production-order`: each order has `this_order`, its own
14
+ units by colour and size and the colours it does not include. A restock's
15
+ `items[].quantity` is the run it was raised from, not its own.
16
+ - `designer list note` / `designer search note`: a factory's message names its
17
+ factory inline (`[Dongguan Heritage, factory] ...`).
18
+ - `designer get tna --product <id>` keeps one product's orders.
19
+ - `designer get costing --product <id>` finds the product's card itself
20
+ (`--style` still works).
21
+ - `designer get product`, `get lifecycle`, `get tech-pack`,
22
+ `get tech-pack-history` take the id as the argument or as `--product`.
23
+ - `designer get tech-pack --readable`: section, field label and value, and
24
+ what is still blank (needs the backend's `view=readable`).
25
+
26
+ ## 0.2.11
27
+
28
+ - Order reads for a factory (and its Beeboo):
29
+ - `supplier get order <rfq> [--readable]`: the sample and bulk orders on one
30
+ RFQ; `--readable` gives each order's state, dates, quantity and what has
31
+ been paid, with no ids or product payload.
32
+ - `supplier list milestone --production-order <id>`: the bulk order's
33
+ milestones, which came back and why, and the proof review's view.
34
+ - `supplier get payment-summary <rfq>`: whether the buyer has paid the
35
+ sample, deposit and balance, and the factory's own price on each PO.
36
+ - `supplier list purchase-order`, `supplier list order-note`.
37
+ - `supplier list production-order` now requires `--rfq` (the backend always
38
+ did; without it the command failed).
39
+
3
40
  ## 0.2.10
4
41
 
5
42
  - PM reads for a brand (and Beeboo working for one):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: makea-cli
3
- Version: 0.2.10
3
+ Version: 0.2.12
4
4
  Summary: Makea admin CLI (browser login + Makea admin HTTP API)
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click<9,>=8.1
@@ -0,0 +1 @@
1
+ __version__ = "0.2.12"
@@ -15,7 +15,7 @@ from dataclasses import dataclass
15
15
  from makea_cli import broker
16
16
  from makea_cli.auth_pkce import ensure_fresh_id_token
17
17
  from makea_cli.config import api_base_url
18
- from makea_cli.errors import ApiError, AuthError, UnknownOutcomeError
18
+ from makea_cli.errors import ApiError, AuthError, MakeaCliError, UnknownOutcomeError
19
19
 
20
20
 
21
21
  @dataclass
@@ -1062,6 +1062,43 @@ def supplier_list_production_orders(*, quote_request_id: str | None = None) -> A
1062
1062
  return get_json("/supplier/production_orders", params=params)
1063
1063
 
1064
1064
 
1065
+ def supplier_get_orders(quote_request_id: str, *, readable: bool = False) -> Any:
1066
+ """GET /supplier/quote_requests/{id}/orders_view — the sample and bulk orders
1067
+ on one RFQ. ``readable``: each order's state, dates and what was paid, with
1068
+ no ids or product payload (the full view runs past 100KB)."""
1069
+ params = {"view": "readable"} if readable else None
1070
+ return get_json(f"/supplier/quote_requests/{quote_request_id}/orders_view", params=params)
1071
+
1072
+
1073
+ def supplier_list_milestones(production_order_id: str) -> Any:
1074
+ """GET /supplier/production_orders/{id}/progress?view=readable — each step's
1075
+ milestones, which one came back and why, and the proof review's view."""
1076
+ return get_json(
1077
+ f"/supplier/production_orders/{production_order_id}/progress",
1078
+ params={"view": "readable"},
1079
+ )
1080
+
1081
+
1082
+ def supplier_get_payment_summary(quote_request_id: str) -> Any:
1083
+ """GET /supplier/quote_requests/{id}/payment_summary — whether the buyer has
1084
+ paid each stage, and the factory's own price on its purchase orders."""
1085
+ return get_json(f"/supplier/quote_requests/{quote_request_id}/payment_summary")
1086
+
1087
+
1088
+ def supplier_list_purchase_orders() -> Any:
1089
+ """GET /supplier/purchase-orders — purchase orders raised to this factory."""
1090
+ return get_json("/supplier/purchase-orders")
1091
+
1092
+
1093
+ def supplier_list_order_notes(
1094
+ *, production_order_id: str | None = None, sampling_order_id: str | None = None
1095
+ ) -> Any:
1096
+ """GET /supplier/{production|sampling}_orders/{id}/notes — an order's notes."""
1097
+ if production_order_id:
1098
+ return get_json(f"/supplier/production_orders/{production_order_id}/notes")
1099
+ return get_json(f"/supplier/sampling_orders/{sampling_order_id}/notes")
1100
+
1101
+
1065
1102
  def supplier_list_actions() -> Any:
1066
1103
  """GET /supplier/actions — what the platform is waiting on this factory for."""
1067
1104
  return get_json("/supplier/actions")
@@ -1102,9 +1139,13 @@ def designer_list_products() -> Any:
1102
1139
  return get_json("/designer/get_all_products")
1103
1140
 
1104
1141
 
1105
- def designer_get_tech_spec(product_id: str) -> Any:
1106
- """GET /designer/products/{id}/technical-specifications — the tech pack."""
1107
- return get_json(f"/designer/products/{product_id}/technical-specifications")
1142
+ def designer_get_tech_spec(product_id: str, *, readable: bool = False) -> Any:
1143
+ """GET /designer/products/{id}/technical-specifications — the tech pack.
1144
+
1145
+ ``readable`` asks for ``view=readable``: section, field label, value and the
1146
+ essentials still blank, a tenth of the raw payload."""
1147
+ params = {"view": "readable"} if readable else None
1148
+ return get_json(f"/designer/products/{product_id}/technical-specifications", params=params)
1108
1149
 
1109
1150
 
1110
1151
  def designer_get_spec_history(product_id: str) -> Any:
@@ -1112,12 +1153,94 @@ def designer_get_spec_history(product_id: str) -> Any:
1112
1153
  return get_json(f"/designer/products/{product_id}/specification-change-history")
1113
1154
 
1114
1155
 
1156
+ QUOTES_HOW_TO_READ = (
1157
+ "Each quote is SAMPLING or PRODUCTION. A SAMPLING quote's bulk tiers are "
1158
+ "indicative (priced before the sample). The bulk price is the PRODUCTION "
1159
+ "quote's, the awarded one (awarded: true) first, at the tier covering the "
1160
+ "order's quantity. `designer get costing --product <id>` is the server's "
1161
+ "unit cost."
1162
+ )
1163
+
1164
+
1165
+ def _tier_lines(link: dict[str, Any]) -> list[str]:
1166
+ """Each bulk tier with the quantities it covers: "301-500 pcs: EUR 71.11"."""
1167
+ tiers: list[Any] = []
1168
+ for group in link.get("pricing_groups") or []:
1169
+ if isinstance(group, dict):
1170
+ tiers += group.get("price_tiers") or []
1171
+ tiers = tiers or link.get("production_price_tiers") or []
1172
+ out = []
1173
+ for tier in tiers:
1174
+ if not isinstance(tier, dict) or tier.get("unit_price") in (None, ""):
1175
+ continue
1176
+ lo = tier.get("min_qty") or 1
1177
+ hi = tier.get("max_qty")
1178
+ span = f"{lo}-{hi}" if hi not in (None, "", 0, "0") else f"{lo}+"
1179
+ currency = tier.get("currency") or link.get("currency") or ""
1180
+ out.append(f"{span} pcs: {currency} {tier.get('unit_price')}".replace(" ", " "))
1181
+ return out
1182
+
1183
+
1184
+ def _quote_summary(link: dict[str, Any]) -> dict[str, Any]:
1185
+ """One quote in a line or two: whose, which kind, what it costs at which quantity."""
1186
+ kind = str(link.get("link_type") or "SAMPLING").upper()
1187
+ info = link.get("supplier_info") if isinstance(link.get("supplier_info"), dict) else {}
1188
+ sample = link.get("sampling_price")
1189
+ if isinstance(sample, dict) and sample.get("value") not in (None, ""):
1190
+ sample = (
1191
+ f"{sample.get('currency') or link.get('currency') or ''} {sample.get('value')}".strip()
1192
+ )
1193
+ return {
1194
+ "factory": link.get("supplier_name") or info.get("supplier_name") or info.get("name"),
1195
+ "link_id": link.get("link_id"),
1196
+ "type": kind,
1197
+ "awarded": bool(link.get("is_awarded")),
1198
+ "confirmed_at": link.get("confirmed_at"),
1199
+ "bulk_price": (
1200
+ "the bulk price"
1201
+ if kind == "PRODUCTION"
1202
+ else "indicative only, priced before the sample"
1203
+ ),
1204
+ "bulk_tiers": _tier_lines(link),
1205
+ "sample_price": sample if kind == "SAMPLING" else None,
1206
+ "moq": link.get("moq"),
1207
+ "lead_time": link.get("lead_time"),
1208
+ "sample_lead_time": link.get("sample_lead_time") if kind == "SAMPLING" else None,
1209
+ "incoterm": link.get("production_price_incoterm"),
1210
+ }
1211
+
1212
+
1115
1213
  def designer_list_links(product_id: str, *, link_type: str | None = None) -> Any:
1116
- """GET /designer/product/linked_suppliers — the suppliers quoted onto one product."""
1117
- params: dict[str, Any] = {"product_id": product_id}
1118
- if link_type:
1119
- params["link_type"] = link_type
1120
- return get_json("/designer/product/linked_suppliers", params=params)
1214
+ """GET /designer/product/linked_suppliers — the quotes on one product.
1215
+
1216
+ The backend answers one link type per call and defaults to SAMPLING, so
1217
+ "omit for both" meant sampling only: a factory's confirmed PRODUCTION
1218
+ quote never showed, and its sampling quote's indicative tiers were read as
1219
+ the bulk price. Without ``link_type`` both are read. Each link says which
1220
+ kind it is, and ``quotes`` puts every quote's tiers with their quantities
1221
+ first."""
1222
+ links: list[dict[str, Any]] = []
1223
+ for kind in [link_type.upper()] if link_type else ["PRODUCTION", "SAMPLING"]:
1224
+ raw = get_json(
1225
+ "/designer/product/linked_suppliers",
1226
+ params={"product_id": product_id, "link_type": kind},
1227
+ )
1228
+ rows = raw.get("supplier_links") if isinstance(raw, dict) else raw
1229
+ for link in rows or []:
1230
+ if isinstance(link, dict):
1231
+ link.setdefault("link_type", kind)
1232
+ links.append(link)
1233
+ quotes = [_quote_summary(link) for link in links]
1234
+ # The awarded production quote first, then other production quotes.
1235
+ quotes.sort(key=lambda q: (q["type"] != "PRODUCTION", not q["awarded"]))
1236
+ return {
1237
+ "success": True,
1238
+ "product_id": product_id,
1239
+ "how_to_read": QUOTES_HOW_TO_READ,
1240
+ "quotes": quotes,
1241
+ "supplier_links": links,
1242
+ "count": len(links),
1243
+ }
1121
1244
 
1122
1245
 
1123
1246
  def designer_list_channels(product_id: str) -> Any:
@@ -1130,7 +1253,11 @@ def designer_search_channel(product_id: str, query: str, *, key: str | None = No
1130
1253
  params: dict[str, Any] = {"q": query}
1131
1254
  if key:
1132
1255
  params["key"] = key
1133
- return get_json(f"/designer/products/{product_id}/channel_search", params=params)
1256
+ raw = get_json(f"/designer/products/{product_id}/channel_search", params=params)
1257
+ for hit in (raw.get("hits") if isinstance(raw, dict) else None) or []:
1258
+ if isinstance(hit, dict):
1259
+ hit["excerpt"] = _said_by(hit, hit.get("excerpt"))
1260
+ return raw
1134
1261
 
1135
1262
 
1136
1263
  def designer_list_sampling_orders(
@@ -1143,14 +1270,68 @@ def designer_list_sampling_orders(
1143
1270
  return get_json("/designer/sampling_orders/user", params=params)
1144
1271
 
1145
1272
 
1273
+ def _this_order(order: dict[str, Any]) -> dict[str, Any]:
1274
+ """What this one order is for: its own colour x size quantities, read off
1275
+ its cart. ``items[].quantity`` on a restock is copied from the run it was
1276
+ raised from and is not this order's quantity; the colours it could take
1277
+ (``available_colors``) are not the colours it has. Facts about one order
1278
+ never describe another (exam 2026-10-10: a 340-unit run's two-colour split
1279
+ was given to a one-colour 120-unit order)."""
1280
+ names = {}
1281
+ for colour in order.get("available_colors") or []:
1282
+ if isinstance(colour, dict) and colour.get("hex_code"):
1283
+ names[str(colour["hex_code"]).upper()] = colour.get("color_name") or colour["hex_code"]
1284
+ by_colour: dict[str, dict[str, int]] = {}
1285
+ total = 0
1286
+ for key, qty in (order.get("cart_items") or {}).items():
1287
+ try:
1288
+ n = int(float(qty or 0))
1289
+ except (TypeError, ValueError):
1290
+ continue
1291
+ if n <= 0:
1292
+ continue
1293
+ size, _, hex_code = str(key).partition(",")
1294
+ colour = names.get(hex_code.strip().upper(), hex_code.strip() or "one colour")
1295
+ by_colour.setdefault(colour, {})[size.strip() or "one size"] = n
1296
+ total += n
1297
+ status = str(order.get("status") or "")
1298
+ kind = str(order.get("order_type") or "PRODUCTION")
1299
+ if not total:
1300
+ return {
1301
+ "units": None,
1302
+ "colours": [],
1303
+ "matrix": {},
1304
+ "summary": f"{kind.title()} order ({status}): no quantities set on this order.",
1305
+ }
1306
+ cells = "; ".join(
1307
+ f"{colour} " + ", ".join(f"{size} {n}" for size, n in sizes.items())
1308
+ for colour, sizes in by_colour.items()
1309
+ )
1310
+ not_ordered = [n for n in names.values() if n not in by_colour]
1311
+ summary = (
1312
+ f"{kind.title()} order ({status}): {total} units, {len(by_colour)} colour(s): {cells}."
1313
+ )
1314
+ if not_ordered:
1315
+ summary += f" Not in this order: {', '.join(not_ordered)}."
1316
+ return {"units": total, "colours": list(by_colour), "matrix": by_colour, "summary": summary}
1317
+
1318
+
1146
1319
  def designer_list_production_orders(
1147
1320
  *, product_order_id: str | None = None, status: str | None = None
1148
1321
  ) -> Any:
1149
- """GET /designer/production_orders (or the per-product list) — the caller's production orders."""
1322
+ """GET /designer/production_orders (or the per-product list) — the caller's
1323
+ production orders, each with ``this_order``: its own quantities by colour and
1324
+ size, said plainly."""
1150
1325
  if product_order_id:
1151
- return get_json(f"/designer/product_orders/{product_order_id}/production_orders")
1152
- params = {"status": status} if status else None
1153
- return get_json("/designer/production_orders", params=params)
1326
+ raw = get_json(f"/designer/product_orders/{product_order_id}/production_orders")
1327
+ else:
1328
+ params = {"status": status} if status else None
1329
+ raw = get_json("/designer/production_orders", params=params)
1330
+ orders = raw.get("production_orders") if isinstance(raw, dict) else raw
1331
+ for order in orders if isinstance(orders, list) else []:
1332
+ if isinstance(order, dict):
1333
+ order["this_order"] = _this_order(order)
1334
+ return raw
1154
1335
 
1155
1336
 
1156
1337
  def designer_list_shipping_orders() -> Any:
@@ -1359,10 +1540,32 @@ def supplier_list_quote_edit_requests(quote_request_id: str) -> Any:
1359
1540
  return get_json(f"/supplier/quote_requests/{quote_request_id}/quote_edit_requests")
1360
1541
 
1361
1542
 
1362
- def designer_get_tna() -> Any:
1543
+ def designer_get_tna(product_id: str | None = None) -> Any:
1363
1544
  """GET /designer/tna_calendar — every pack's five steps and arrival, with
1364
- the launch it is measured against when the arrival is confirmed."""
1365
- return get_json("/designer/tna_calendar")
1545
+ the launch it is measured against when the arrival is confirmed.
1546
+
1547
+ ``product_id`` keeps that product's rows only (the calendar is account-wide)."""
1548
+ raw = get_json("/designer/tna_calendar")
1549
+ if not product_id or not isinstance(raw, dict):
1550
+ return raw
1551
+ groups = []
1552
+ for group in raw.get("groups") or []:
1553
+ if not isinstance(group, dict):
1554
+ continue
1555
+ rows = [
1556
+ {k: v for k, v in row.items() if k != "image_url"}
1557
+ for row in group.get("rows") or []
1558
+ if isinstance(row, dict) and row.get("product_id") == product_id
1559
+ ]
1560
+ if rows:
1561
+ groups.append({**group, "rows": rows})
1562
+ return {
1563
+ "success": raw.get("success", True),
1564
+ "today": raw.get("today"),
1565
+ "rules": raw.get("rules"),
1566
+ "product_id": product_id,
1567
+ "groups": groups,
1568
+ }
1366
1569
 
1367
1570
 
1368
1571
  def designer_list_milestones(product_id: str) -> Any:
@@ -1420,36 +1623,55 @@ def designer_list_notes(
1420
1623
  "at": n.get("created_at"),
1421
1624
  "from": n.get("created_by_type"),
1422
1625
  "supplier": n.get("supplier_name"),
1423
- "text": n.get("content"),
1626
+ "text": _said_by(n, n.get("content")),
1424
1627
  "replies": len(n.get("replies") or []),
1425
1628
  }
1426
1629
  )
1427
1630
  return {"product_id": product_id, "channel": channel_key, "notes": out}
1428
1631
 
1429
1632
 
1633
+ def _said_by(note: dict[str, Any], text: Any) -> Any:
1634
+ """A factory's words with the factory named inline: "[Dongguan Heritage,
1635
+ factory] ...". The role alone let one factory's line be read as another's."""
1636
+ role = str(note.get("created_by_type") or note.get("actor_role") or "").lower()
1637
+ if role not in ("supplier", "factory") or not isinstance(text, str):
1638
+ return text
1639
+ name = str(note.get("supplier_name") or "").strip()
1640
+ return f"[{name}, factory] {text}" if name else f"[factory] {text}"
1641
+
1642
+
1430
1643
  def designer_list_fit_rounds(product_id: str) -> Any:
1431
1644
  """GET /designer/products/{id}/fit_rounds — every fit round on the pack and
1432
1645
  the corrections carried into bulk."""
1433
1646
  return get_json(f"/designer/products/{product_id}/fit_rounds")
1434
1647
 
1435
1648
 
1436
- def designer_get_costing(style_id: str) -> Any:
1649
+ def designer_get_costing(style_id: str | None = None, *, product_id: str | None = None) -> Any:
1437
1650
  """One unit of each pack on a product card: factory price, fees, the
1438
- brand's freight and duty estimates, target, RRP, margin."""
1651
+ brand's freight and duty estimates, target, RRP, margin.
1652
+
1653
+ ``product_id`` finds the product's card itself and keeps that pack."""
1654
+ if not style_id and product_id:
1655
+ product = designer_get_product(product_id, show_details=False)
1656
+ row = product.get("product") if isinstance(product, dict) else None
1657
+ style_id = (row or {}).get("style_id")
1658
+ if not style_id:
1659
+ raise MakeaCliError(f"Product {product_id} is not on a product card (no style_id).")
1660
+ if not style_id:
1661
+ raise MakeaCliError("Pass --product <product id> or --style <style id>.")
1439
1662
  raw = get_json(f"/designer/styles/{style_id}/overview")
1440
- packs = raw.get("packs") if isinstance(raw, dict) else []
1441
- return {
1442
- "style_id": style_id,
1443
- "packs": [
1444
- {
1445
- "product_id": p.get("product_id"),
1446
- "pack": p.get("pack_label"),
1447
- "costing": p.get("costing"),
1448
- }
1449
- for p in packs or []
1450
- if isinstance(p, dict)
1451
- ],
1452
- }
1663
+ packs = [
1664
+ {
1665
+ "product_id": p.get("product_id"),
1666
+ "pack": p.get("pack_label"),
1667
+ "costing": p.get("costing"),
1668
+ }
1669
+ for p in (raw.get("packs") if isinstance(raw, dict) else None) or []
1670
+ if isinstance(p, dict)
1671
+ ]
1672
+ if product_id:
1673
+ packs = [p for p in packs if p["product_id"] == product_id] or packs
1674
+ return {"style_id": style_id, "packs": packs}
1453
1675
 
1454
1676
 
1455
1677
  def designer_list_actions(*, product_id: str | None = None) -> Any:
@@ -39,6 +39,20 @@ def _json_object(raw: str | None, path: Path | None, what: str) -> dict[str, Any
39
39
  return data
40
40
 
41
41
 
42
+ def _product_id(argument: str | None, option: str | None) -> str:
43
+ """A product id given either way: ``get product <id>`` or ``--product <id>``."""
44
+ if argument and option and argument != option:
45
+ raise typer.BadParameter("give the product id once, as the argument or as --product")
46
+ value = argument or option
47
+ if not value:
48
+ raise typer.BadParameter("pass the product id (as the argument or as --product)")
49
+ return value
50
+
51
+
52
+ _PRODUCT_ARG_HELP = "Product id (or pass --product)."
53
+ _PRODUCT_OPT_HELP = "Same as the argument."
54
+
55
+
42
56
  def register(app: typer.Typer) -> None:
43
57
  # --- reads ------------------------------------------------------------------
44
58
 
@@ -63,14 +77,18 @@ def register(app: typer.Typer) -> None:
63
77
  help="GET /designer/get_product — one product: status, documents, prices, links.",
64
78
  )
65
79
  def get_product(
66
- product_id: str = typer.Argument(..., help="Product id (== product order reference id)."),
80
+ product_id: str | None = typer.Argument(
81
+ None, help="Product id (== product order reference id); or pass --product."
82
+ ),
83
+ product: str | None = typer.Option(None, "--product", "-p", help=_PRODUCT_OPT_HELP),
67
84
  summary: bool = typer.Option(
68
85
  False, "--summary", help="Ask the backend for the short form (show_details=false)."
69
86
  ),
70
87
  ) -> None:
71
88
  """Read one product."""
89
+ pid = _product_id(product_id, product)
72
90
  print_json_or_exit(
73
- console, lambda: api_client.designer_get_product(product_id, show_details=not summary)
91
+ console, lambda: api_client.designer_get_product(pid, show_details=not summary)
74
92
  )
75
93
 
76
94
  @command(
@@ -79,13 +97,23 @@ def register(app: typer.Typer) -> None:
79
97
  "get",
80
98
  "tech-pack",
81
99
  api="GET /designer/products/{product_id}/technical-specifications",
82
- help="GET /designer/products/{id}/technical-specifications — the product's tech pack.",
100
+ help=(
101
+ "GET /designer/products/{id}/technical-specifications — the product's tech pack; "
102
+ "--readable gives section, field and value only (a tenth of the size)."
103
+ ),
83
104
  )
84
105
  def get_tech_pack(
85
- product_id: str = typer.Argument(..., help="Product id."),
106
+ product_id: str | None = typer.Argument(None, help=_PRODUCT_ARG_HELP),
107
+ product: str | None = typer.Option(None, "--product", "-p", help=_PRODUCT_OPT_HELP),
108
+ readable: bool = typer.Option(
109
+ False, "--readable", help="Section, field label and value; what is still blank."
110
+ ),
86
111
  ) -> None:
87
112
  """Read a product's technical specification."""
88
- print_json_or_exit(console, lambda: api_client.designer_get_tech_spec(product_id))
113
+ pid = _product_id(product_id, product)
114
+ print_json_or_exit(
115
+ console, lambda: api_client.designer_get_tech_spec(pid, readable=readable)
116
+ )
89
117
 
90
118
  @command(
91
119
  app,
@@ -99,10 +127,12 @@ def register(app: typer.Typer) -> None:
99
127
  ),
100
128
  )
101
129
  def get_tech_pack_history(
102
- product_id: str = typer.Argument(..., help="Product id."),
130
+ product_id: str | None = typer.Argument(None, help=_PRODUCT_ARG_HELP),
131
+ product: str | None = typer.Option(None, "--product", "-p", help=_PRODUCT_OPT_HELP),
103
132
  ) -> None:
104
133
  """Read a product's specification change history."""
105
- print_json_or_exit(console, lambda: api_client.designer_get_spec_history(product_id))
134
+ pid = _product_id(product_id, product)
135
+ print_json_or_exit(console, lambda: api_client.designer_get_spec_history(pid))
106
136
 
107
137
  @command(
108
138
  app,
@@ -111,8 +141,9 @@ def register(app: typer.Typer) -> None:
111
141
  "supplier-link",
112
142
  api="GET /designer/product/linked_suppliers",
113
143
  help=(
114
- "GET /designer/product/linked_suppliers — the suppliers linked to one product, "
115
- "with their quotes."
144
+ "GET /designer/product/linked_suppliers — the quotes on one product, SAMPLING and "
145
+ "PRODUCTION, each labelled; `quotes` lists every quote's bulk tiers with their "
146
+ "quantities, the awarded production quote first."
116
147
  ),
117
148
  )
118
149
  def list_links(
@@ -156,8 +187,9 @@ def register(app: typer.Typer) -> None:
156
187
  "production-order",
157
188
  api="GET /designer/production_orders",
158
189
  help=(
159
- "GET /designer/production_orders — your 大货 orders; --product narrows to one "
160
- "product (GET /designer/product_orders/{id}/production_orders)."
190
+ "GET /designer/production_orders — your 大货 orders, each with `this_order`: its "
191
+ "own units by colour and size; --product narrows to one product "
192
+ "(GET /designer/product_orders/{id}/production_orders)."
161
193
  ),
162
194
  )
163
195
  def list_production_orders(
@@ -193,10 +225,14 @@ def register(app: typer.Typer) -> None:
193
225
  help="GET /designer/product_orders/{id}/lifecycle — which stage this product is in.",
194
226
  )
195
227
  def get_lifecycle(
196
- product_order_id: str = typer.Argument(..., help="Product order id."),
228
+ product_order_id: str | None = typer.Argument(
229
+ None, help="Product (order) id; or pass --product."
230
+ ),
231
+ product: str | None = typer.Option(None, "--product", "-p", help=_PRODUCT_OPT_HELP),
197
232
  ) -> None:
198
233
  """Read a product's lifecycle state."""
199
- print_json_or_exit(console, lambda: api_client.designer_get_lifecycle(product_order_id))
234
+ pid = _product_id(product_order_id, product)
235
+ print_json_or_exit(console, lambda: api_client.designer_get_lifecycle(pid))
200
236
 
201
237
  @command(
202
238
  app,
@@ -220,12 +256,17 @@ def register(app: typer.Typer) -> None:
220
256
  api="GET /designer/tna_calendar",
221
257
  help=(
222
258
  "GET /designer/tna_calendar — when each pack arrives and how sure that is "
223
- "(done / confirmed / estimated), with its launch day."
259
+ "(done / confirmed / estimated), with its launch day; --product keeps one "
260
+ "product's orders."
224
261
  ),
225
262
  )
226
- def get_tna() -> None:
263
+ def get_tna(
264
+ product: str | None = typer.Option(
265
+ None, "--product", "-p", help="Only this product's orders."
266
+ ),
267
+ ) -> None:
227
268
  """The Time & Action calendar."""
228
- print_json_or_exit(console, lambda: api_client.designer_get_tna())
269
+ print_json_or_exit(console, lambda: api_client.designer_get_tna(product))
229
270
 
230
271
  @command(
231
272
  app,
@@ -291,15 +332,21 @@ def register(app: typer.Typer) -> None:
291
332
  "costing",
292
333
  api="GET /designer/styles/{style_id}/overview",
293
334
  help=(
294
- "GET /designer/styles/{id}/overview — what one unit of each pack costs and "
295
- "the margin at RRP (freight and duty are the brand's estimates)."
335
+ "GET /designer/styles/{id}/overview — what one unit of a pack costs and the "
336
+ "margin at RRP (freight and duty are the brand's estimates). Pass --product "
337
+ "(the product's own card is found) or --style."
296
338
  ),
297
339
  )
298
340
  def get_costing(
299
- style: str = typer.Option(..., "--style", "-s", help="Product card (style) id."),
341
+ product: str | None = typer.Option(None, "--product", "-p", help="Product id."),
342
+ style: str | None = typer.Option(None, "--style", "-s", help="Product card (style) id."),
300
343
  ) -> None:
301
344
  """A product card's unit cost and margin."""
302
- print_json_or_exit(console, lambda: api_client.designer_get_costing(style))
345
+ if not product and not style:
346
+ raise typer.BadParameter("pass --product <product id> or --style <style id>")
347
+ print_json_or_exit(
348
+ console, lambda: api_client.designer_get_costing(style, product_id=product)
349
+ )
303
350
 
304
351
  @command(
305
352
  app,