makea-cli 0.2.11__tar.gz → 0.2.13__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 (69) hide show
  1. {makea_cli-0.2.11 → makea_cli-0.2.13}/CHANGELOG.md +34 -0
  2. {makea_cli-0.2.11 → makea_cli-0.2.13}/PKG-INFO +1 -1
  3. makea_cli-0.2.13/makea_cli/__init__.py +1 -0
  4. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/api_client.py +230 -33
  5. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/brand/cmd.py +88 -20
  6. {makea_cli-0.2.11 → makea_cli-0.2.13}/pyproject.toml +1 -1
  7. {makea_cli-0.2.11 → makea_cli-0.2.13}/tests/golden_manifest.json +137 -15
  8. {makea_cli-0.2.11 → makea_cli-0.2.13}/tests/test_persona_commands.py +14 -0
  9. makea_cli-0.2.13/tests/test_pm_reads.py +227 -0
  10. makea_cli-0.2.13/tests/test_supplier_scorecard.py +48 -0
  11. {makea_cli-0.2.11 → makea_cli-0.2.13}/uv.lock +1 -1
  12. makea_cli-0.2.11/makea_cli/__init__.py +0 -1
  13. makea_cli-0.2.11/tests/test_pm_reads.py +0 -69
  14. {makea_cli-0.2.11 → makea_cli-0.2.13}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
  15. {makea_cli-0.2.11 → makea_cli-0.2.13}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
  16. {makea_cli-0.2.11 → makea_cli-0.2.13}/.claude/skills/link-supplier-user/SKILL.md +0 -0
  17. {makea_cli-0.2.11 → makea_cli-0.2.13}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
  18. {makea_cli-0.2.11 → makea_cli-0.2.13}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
  19. {makea_cli-0.2.11 → makea_cli-0.2.13}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
  20. {makea_cli-0.2.11 → makea_cli-0.2.13}/.gitignore +0 -0
  21. {makea_cli-0.2.11 → makea_cli-0.2.13}/README.md +0 -0
  22. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea-collection-upload/SKILL.md +0 -0
  23. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea-collection-upload/scripts/create_products.py +0 -0
  24. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/__main__.py +0 -0
  25. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/auth_pkce.py +0 -0
  26. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/broker.py +0 -0
  27. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/__init__.py +0 -0
  28. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/_util.py +0 -0
  29. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/analytics/__init__.py +0 -0
  30. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/analytics/cmd.py +0 -0
  31. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/auth/__init__.py +0 -0
  32. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/auth/cmd.py +0 -0
  33. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/brand/__init__.py +0 -0
  34. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/designer/__init__.py +0 -0
  35. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/designer/cmd.py +0 -0
  36. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/directory/__init__.py +0 -0
  37. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/directory/cmd.py +0 -0
  38. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/document/__init__.py +0 -0
  39. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/document/cmd.py +0 -0
  40. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/email/__init__.py +0 -0
  41. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/email/cmd.py +0 -0
  42. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/factory/__init__.py +0 -0
  43. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/factory/cmd.py +0 -0
  44. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/library/__init__.py +0 -0
  45. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/library/cmd.py +0 -0
  46. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/metrics/__init__.py +0 -0
  47. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/metrics/cmd.py +0 -0
  48. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/orders/__init__.py +0 -0
  49. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/orders/cmd.py +0 -0
  50. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/product/__init__.py +0 -0
  51. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/product/cmd.py +0 -0
  52. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/production/__init__.py +0 -0
  53. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/production/cmd.py +0 -0
  54. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/production/pricing_spec.py +0 -0
  55. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/supplier/__init__.py +0 -0
  56. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/commands/supplier/cmd.py +0 -0
  57. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/config.py +0 -0
  58. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/errors.py +0 -0
  59. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/identity.py +0 -0
  60. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/main.py +0 -0
  61. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/output.py +0 -0
  62. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/posthog_client.py +0 -0
  63. {makea_cli-0.2.11 → makea_cli-0.2.13}/makea_cli/registry.py +0 -0
  64. {makea_cli-0.2.11 → makea_cli-0.2.13}/npm/makea-cli/README.md +0 -0
  65. {makea_cli-0.2.11 → makea_cli-0.2.13}/npm/makea-cli/makea-cli.mjs +0 -0
  66. {makea_cli-0.2.11 → makea_cli-0.2.13}/npm/makea-cli/package.json +0 -0
  67. {makea_cli-0.2.11 → makea_cli-0.2.13}/tests/test_broker.py +0 -0
  68. {makea_cli-0.2.11 → makea_cli-0.2.13}/tests/test_output_contract.py +0 -0
  69. {makea_cli-0.2.11 → makea_cli-0.2.13}/tests/test_registry.py +0 -0
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.13
4
+
5
+ - `designer get supplier-scorecard [SUPPLIER_ID]`: a factory's track record
6
+ with the brand's team: orders on time against the factory's confirmed date,
7
+ milestone proofs rejected, inspections with High/Critical defects, fit
8
+ rounds that needed a new sample, confirmed dates moved later, median time to
9
+ first quote and to answer the brand's asks, chases, and orders, units and
10
+ styles. Every figure carries its n and period; "too few to judge" below the
11
+ minimum. No id: every factory the brand works with. A read (auto), and
12
+ brand-private: Makea refuses it in a factory's thread.
13
+
14
+ ## 0.2.12
15
+
16
+ Fixes from a live exam of Beeboo (2026-10-10):
17
+
18
+ - `designer list supplier-link --product <id>` without `--link-type` now reads
19
+ SAMPLING and PRODUCTION quotes (the backend defaults to sampling, so a
20
+ confirmed production quote never showed). Each link carries its
21
+ `link_type`, and a new `quotes` list gives every quote's factory, type,
22
+ whether it is the awarded production quote, and its bulk tiers with the
23
+ quantities they cover; a sampling quote's tiers are marked indicative.
24
+ - `designer list production-order`: each order has `this_order`, its own
25
+ units by colour and size and the colours it does not include. A restock's
26
+ `items[].quantity` is the run it was raised from, not its own.
27
+ - `designer list note` / `designer search note`: a factory's message names its
28
+ factory inline (`[Dongguan Heritage, factory] ...`).
29
+ - `designer get tna --product <id>` keeps one product's orders.
30
+ - `designer get costing --product <id>` finds the product's card itself
31
+ (`--style` still works).
32
+ - `designer get product`, `get lifecycle`, `get tech-pack`,
33
+ `get tech-pack-history` take the id as the argument or as `--product`.
34
+ - `designer get tech-pack --readable`: section, field label and value, and
35
+ what is still blank (needs the backend's `view=readable`).
36
+
3
37
  ## 0.2.11
4
38
 
5
39
  - Order reads for a factory (and its Beeboo):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: makea-cli
3
- Version: 0.2.11
3
+ Version: 0.2.13
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.13"
@@ -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
@@ -1139,9 +1139,13 @@ def designer_list_products() -> Any:
1139
1139
  return get_json("/designer/get_all_products")
1140
1140
 
1141
1141
 
1142
- def designer_get_tech_spec(product_id: str) -> Any:
1143
- """GET /designer/products/{id}/technical-specifications — the tech pack."""
1144
- 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)
1145
1149
 
1146
1150
 
1147
1151
  def designer_get_spec_history(product_id: str) -> Any:
@@ -1149,12 +1153,94 @@ def designer_get_spec_history(product_id: str) -> Any:
1149
1153
  return get_json(f"/designer/products/{product_id}/specification-change-history")
1150
1154
 
1151
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
+
1152
1213
  def designer_list_links(product_id: str, *, link_type: str | None = None) -> Any:
1153
- """GET /designer/product/linked_suppliers — the suppliers quoted onto one product."""
1154
- params: dict[str, Any] = {"product_id": product_id}
1155
- if link_type:
1156
- params["link_type"] = link_type
1157
- 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
+ }
1158
1244
 
1159
1245
 
1160
1246
  def designer_list_channels(product_id: str) -> Any:
@@ -1167,7 +1253,11 @@ def designer_search_channel(product_id: str, query: str, *, key: str | None = No
1167
1253
  params: dict[str, Any] = {"q": query}
1168
1254
  if key:
1169
1255
  params["key"] = key
1170
- 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
1171
1261
 
1172
1262
 
1173
1263
  def designer_list_sampling_orders(
@@ -1180,14 +1270,68 @@ def designer_list_sampling_orders(
1180
1270
  return get_json("/designer/sampling_orders/user", params=params)
1181
1271
 
1182
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
+
1183
1319
  def designer_list_production_orders(
1184
1320
  *, product_order_id: str | None = None, status: str | None = None
1185
1321
  ) -> Any:
1186
- """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."""
1187
1325
  if product_order_id:
1188
- return get_json(f"/designer/product_orders/{product_order_id}/production_orders")
1189
- params = {"status": status} if status else None
1190
- 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
1191
1335
 
1192
1336
 
1193
1337
  def designer_list_shipping_orders() -> Any:
@@ -1396,10 +1540,32 @@ def supplier_list_quote_edit_requests(quote_request_id: str) -> Any:
1396
1540
  return get_json(f"/supplier/quote_requests/{quote_request_id}/quote_edit_requests")
1397
1541
 
1398
1542
 
1399
- def designer_get_tna() -> Any:
1543
+ def designer_get_tna(product_id: str | None = None) -> Any:
1400
1544
  """GET /designer/tna_calendar — every pack's five steps and arrival, with
1401
- the launch it is measured against when the arrival is confirmed."""
1402
- 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
+ }
1403
1569
 
1404
1570
 
1405
1571
  def designer_list_milestones(product_id: str) -> Any:
@@ -1457,36 +1623,67 @@ def designer_list_notes(
1457
1623
  "at": n.get("created_at"),
1458
1624
  "from": n.get("created_by_type"),
1459
1625
  "supplier": n.get("supplier_name"),
1460
- "text": n.get("content"),
1626
+ "text": _said_by(n, n.get("content")),
1461
1627
  "replies": len(n.get("replies") or []),
1462
1628
  }
1463
1629
  )
1464
1630
  return {"product_id": product_id, "channel": channel_key, "notes": out}
1465
1631
 
1466
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
+
1467
1643
  def designer_list_fit_rounds(product_id: str) -> Any:
1468
1644
  """GET /designer/products/{id}/fit_rounds — every fit round on the pack and
1469
1645
  the corrections carried into bulk."""
1470
1646
  return get_json(f"/designer/products/{product_id}/fit_rounds")
1471
1647
 
1472
1648
 
1473
- def designer_get_costing(style_id: str) -> Any:
1649
+ def designer_get_costing(style_id: str | None = None, *, product_id: str | None = None) -> Any:
1474
1650
  """One unit of each pack on a product card: factory price, fees, the
1475
- 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>.")
1476
1662
  raw = get_json(f"/designer/styles/{style_id}/overview")
1477
- packs = raw.get("packs") if isinstance(raw, dict) else []
1478
- return {
1479
- "style_id": style_id,
1480
- "packs": [
1481
- {
1482
- "product_id": p.get("product_id"),
1483
- "pack": p.get("pack_label"),
1484
- "costing": p.get("costing"),
1485
- }
1486
- for p in packs or []
1487
- if isinstance(p, dict)
1488
- ],
1489
- }
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}
1675
+
1676
+
1677
+ def designer_get_supplier_scorecard(supplier_id: str | None = None) -> Any:
1678
+ """A factory's track record with the brand's team: on time against its
1679
+ confirmed dates, quality, responsiveness, volume. Each figure has its n and
1680
+ period; "too few to judge" below the minimum. No id: every factory the
1681
+ brand works with. Brand-private."""
1682
+ if supplier_id:
1683
+ raw = get_json(f"/designer/suppliers/{supplier_id}/scorecard")
1684
+ return raw.get("scorecard") if isinstance(raw, dict) else raw
1685
+ raw = get_json("/designer/suppliers/scorecards")
1686
+ return {"scorecards": raw.get("scorecards") if isinstance(raw, dict) else raw}
1490
1687
 
1491
1688
 
1492
1689
  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,42 @@ 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
+ )
350
+
351
+ @command(
352
+ app,
353
+ "designer",
354
+ "get",
355
+ "supplier-scorecard",
356
+ api="GET /designer/suppliers/{supplier_id}/scorecard",
357
+ help=(
358
+ "GET /designer/suppliers/{id}/scorecard — a factory's track record with the "
359
+ "brand's team (on time vs confirmed dates, quality, responsiveness, volume; "
360
+ "each figure with its n and period). No id: every factory the brand works "
361
+ "with. Brand-private: never shared with the factory."
362
+ ),
363
+ )
364
+ def get_supplier_scorecard(
365
+ supplier_id: str | None = typer.Argument(
366
+ None, help="Supplier id; omit for every factory the brand works with."
367
+ ),
368
+ ) -> None:
369
+ """A factory's track record with this brand."""
370
+ print_json_or_exit(console, lambda: api_client.designer_get_supplier_scorecard(supplier_id))
303
371
 
304
372
  @command(
305
373
  app,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "makea-cli"
7
- version = "0.2.11"
7
+ version = "0.2.13"
8
8
  description = "Makea admin CLI (browser login + Makea admin HTTP API)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"