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.
- {makea_cli-0.2.10 → makea_cli-0.2.12}/CHANGELOG.md +37 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/PKG-INFO +1 -1
- makea_cli-0.2.12/makea_cli/__init__.py +1 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/api_client.py +255 -33
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/brand/cmd.py +67 -20
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/factory/cmd.py +104 -3
- {makea_cli-0.2.10 → makea_cli-0.2.12}/pyproject.toml +1 -1
- {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/golden_manifest.json +275 -18
- {makea_cli-0.2.10 → 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.10 → makea_cli-0.2.12}/uv.lock +4 -2
- makea_cli-0.2.10/makea_cli/__init__.py +0 -1
- makea_cli-0.2.10/tests/test_pm_reads.py +0 -69
- {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/link-supplier-user/SKILL.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/.gitignore +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/README.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea-collection-upload/SKILL.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea-collection-upload/scripts/create_products.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/__main__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/auth_pkce.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/broker.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/_util.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/analytics/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/analytics/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/auth/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/auth/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/brand/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/designer/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/designer/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/directory/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/directory/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/document/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/document/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/email/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/email/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/factory/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/library/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/library/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/metrics/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/metrics/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/orders/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/orders/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/product/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/product/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/production/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/production/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/production/pricing_spec.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/supplier/__init__.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/commands/supplier/cmd.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/config.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/errors.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/identity.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/main.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/output.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/posthog_client.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/makea_cli/registry.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/npm/makea-cli/README.md +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/npm/makea-cli/makea-cli.mjs +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/npm/makea-cli/package.json +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/test_broker.py +0 -0
- {makea_cli-0.2.10 → makea_cli-0.2.12}/tests/test_output_contract.py +0 -0
- {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):
|
|
@@ -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
|
-
|
|
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
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
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 =
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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(
|
|
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,
|