makea-cli 0.2.11__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.
- {makea_cli-0.2.11 → makea_cli-0.2.12}/CHANGELOG.md +23 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/PKG-INFO +1 -1
- makea_cli-0.2.12/makea_cli/__init__.py +1 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/api_client.py +218 -33
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/brand/cmd.py +67 -20
- {makea_cli-0.2.11 → makea_cli-0.2.12}/pyproject.toml +1 -1
- {makea_cli-0.2.11 → makea_cli-0.2.12}/tests/golden_manifest.json +106 -15
- {makea_cli-0.2.11 → makea_cli-0.2.12}/tests/test_persona_commands.py +14 -0
- makea_cli-0.2.12/tests/test_pm_reads.py +227 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/uv.lock +1 -1
- makea_cli-0.2.11/makea_cli/__init__.py +0 -1
- makea_cli-0.2.11/tests/test_pm_reads.py +0 -69
- {makea_cli-0.2.11 → makea_cli-0.2.12}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/.claude/skills/link-supplier-user/SKILL.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/.gitignore +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/README.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea-collection-upload/SKILL.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea-collection-upload/scripts/create_products.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/__main__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/auth_pkce.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/broker.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/_util.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/analytics/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/analytics/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/auth/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/auth/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/brand/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/designer/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/designer/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/directory/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/directory/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/document/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/document/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/email/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/email/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/factory/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/factory/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/library/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/library/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/metrics/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/metrics/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/orders/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/orders/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/product/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/product/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/production/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/production/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/production/pricing_spec.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/supplier/__init__.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/commands/supplier/cmd.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/config.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/errors.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/identity.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/main.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/output.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/posthog_client.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/makea_cli/registry.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/npm/makea-cli/README.md +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/npm/makea-cli/makea-cli.mjs +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/npm/makea-cli/package.json +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/tests/test_broker.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/tests/test_output_contract.py +0 -0
- {makea_cli-0.2.11 → makea_cli-0.2.12}/tests/test_registry.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
3
26
|
## 0.2.11
|
|
4
27
|
|
|
5
28
|
- Order reads for a factory (and its Beeboo):
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
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,55 @@ 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 =
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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}
|
|
1490
1675
|
|
|
1491
1676
|
|
|
1492
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(
|
|
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(
|
|
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=
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
115
|
-
"with their
|
|
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
|
|
160
|
-
"product
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
295
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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,
|