makea-cli 0.2.25__py3-none-any.whl → 0.2.26__py3-none-any.whl
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/__init__.py +1 -1
- makea_cli/api_client.py +125 -0
- makea_cli/commands/__init__.py +2 -0
- makea_cli/commands/directory/cmd.py +14 -0
- makea_cli/commands/flags/__init__.py +0 -0
- makea_cli/commands/flags/cmd.py +157 -0
- makea_cli/commands/orders/cmd.py +252 -0
- makea_cli/commands/product/cmd.py +246 -0
- makea_cli/commands/supplier/cmd.py +150 -0
- makea_cli/companion.py +278 -0
- makea_cli/main.py +64 -3
- makea_cli/output.py +9 -1
- {makea_cli-0.2.25.dist-info → makea_cli-0.2.26.dist-info}/METADATA +22 -1
- {makea_cli-0.2.25.dist-info → makea_cli-0.2.26.dist-info}/RECORD +16 -13
- {makea_cli-0.2.25.dist-info → makea_cli-0.2.26.dist-info}/WHEEL +0 -0
- {makea_cli-0.2.25.dist-info → makea_cli-0.2.26.dist-info}/entry_points.txt +0 -0
makea_cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.2.
|
|
1
|
+
__version__ = "0.2.26"
|
makea_cli/api_client.py
CHANGED
|
@@ -1282,6 +1282,131 @@ def admin_playbook_preview(
|
|
|
1282
1282
|
return get_json("/admin/playbook/preview", params=params)
|
|
1283
1283
|
|
|
1284
1284
|
|
|
1285
|
+
# --- admin: what the admin page does by hand (orders, products, people, flags) -----
|
|
1286
|
+
# None of these move money: order statuses that record a payment are refused by
|
|
1287
|
+
# the commands, and change_step refuses money steps server-side.
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def admin_get_supplier(supplier_id: str) -> Any:
|
|
1291
|
+
"""GET /admin/supplier/get_supplier_by_id?supplier_id= — profile plus financial documents."""
|
|
1292
|
+
return get_json("/admin/supplier/get_supplier_by_id", params={"supplier_id": supplier_id})
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def admin_get_user_profile(user_id: str) -> Any:
|
|
1296
|
+
"""GET /admin/user-profiles/{user_id}"""
|
|
1297
|
+
return get_json(f"/admin/user-profiles/{quote(user_id, safe='')}")
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def admin_update_sampling_order_status(sampling_order_id: str, status: str) -> Any:
|
|
1301
|
+
"""PUT /admin/sampling_orders/{id}/status"""
|
|
1302
|
+
return put_json(f"/admin/sampling_orders/{sampling_order_id}/status", {"status": status})
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def admin_update_production_order_status(production_order_id: str, status: str) -> Any:
|
|
1306
|
+
"""PUT /admin/production_orders/{id}/status"""
|
|
1307
|
+
return put_json(f"/admin/production_orders/{production_order_id}/status", {"status": status})
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def admin_set_sampling_order_tracking(sampling_order_id: str, body: dict[str, Any]) -> Any:
|
|
1311
|
+
"""PUT /admin/sampling_orders/{id}/tracking — tracking_number / tracking_url /
|
|
1312
|
+
shipping_method / carrier_id, flat in the body."""
|
|
1313
|
+
return put_json(f"/admin/sampling_orders/{sampling_order_id}/tracking", body)
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
def admin_set_production_order_tracking(production_order_id: str, body: dict[str, Any]) -> Any:
|
|
1317
|
+
"""POST /admin/production_orders/{id}/tracking — as sampling, plus cartons and
|
|
1318
|
+
gross_weight_kg (absent = unchanged, empty string = cleared)."""
|
|
1319
|
+
return post_json(f"/admin/production_orders/{production_order_id}/tracking", body)
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
def admin_get_product_status(product_id: str) -> Any:
|
|
1323
|
+
"""GET /admin/products/{id}/status — the product's stage · step · owner per supplier,
|
|
1324
|
+
with the step catalog."""
|
|
1325
|
+
return get_json(f"/admin/products/{product_id}/status")
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def admin_get_change_step(product_id: str, supplier_id: str) -> Any:
|
|
1329
|
+
"""GET /admin/products/{id}/suppliers/{sid}/change_step — where the factory is
|
|
1330
|
+
and which steps can be picked."""
|
|
1331
|
+
return get_json(f"/admin/products/{product_id}/suppliers/{supplier_id}/change_step")
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def admin_change_step(product_id: str, supplier_id: str, body: dict[str, Any]) -> Any:
|
|
1335
|
+
"""POST /admin/products/{id}/suppliers/{sid}/change_step —
|
|
1336
|
+
{stage, step, note?, notify_brand?, notify_supplier?}."""
|
|
1337
|
+
return post_json(f"/admin/products/{product_id}/suppliers/{supplier_id}/change_step", body)
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def admin_list_product_notes(product_id: str) -> Any:
|
|
1341
|
+
"""GET /admin/products/{id}/notes — the product's customer notes."""
|
|
1342
|
+
return get_json(f"/admin/products/{product_id}/notes")
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def admin_add_product_note(product_id: str, content: str, *, internal: bool) -> Any:
|
|
1346
|
+
"""POST /admin/products/{id}/notes — {content, is_internal}. The backend reads
|
|
1347
|
+
``is_internal``; a customer note (the default) is emailed to the brand."""
|
|
1348
|
+
return post_json(
|
|
1349
|
+
f"/admin/products/{product_id}/notes",
|
|
1350
|
+
{"content": content, "is_internal": internal},
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def admin_update_product_basic_info(product_id: str, body: dict[str, Any]) -> Any:
|
|
1355
|
+
"""PUT /admin/products/{id}/basic-info — only the fields given are changed."""
|
|
1356
|
+
return put_json(f"/admin/products/{product_id}/basic-info", body)
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def admin_update_product_document_status(product_id: str, document_id: str, status: str) -> Any:
|
|
1360
|
+
"""POST /admin/designer/update_document_status — {product_id, document_id, status}."""
|
|
1361
|
+
return post_json(
|
|
1362
|
+
"/admin/designer/update_document_status",
|
|
1363
|
+
{"product_id": product_id, "document_id": document_id, "status": status},
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def admin_link_supplier(body: dict[str, Any]) -> Any:
|
|
1368
|
+
"""POST /admin/product/link_supplier — a bare assignment (no price, so not a quote)."""
|
|
1369
|
+
return post_json("/admin/product/link_supplier", body)
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def admin_unlink_supplier(
|
|
1373
|
+
product_id: str, *, link_id: str | None = None, supplier_id: str | None = None
|
|
1374
|
+
) -> Any:
|
|
1375
|
+
"""POST /admin/product/unlink_supplier?product_id=&link_id=|supplier_id= (query, no body)."""
|
|
1376
|
+
params: dict[str, Any] = {"product_id": product_id}
|
|
1377
|
+
if link_id:
|
|
1378
|
+
params["link_id"] = link_id
|
|
1379
|
+
if supplier_id:
|
|
1380
|
+
params["supplier_id"] = supplier_id
|
|
1381
|
+
return _request("POST", "/admin/product/unlink_supplier", params=params)
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def admin_reply_quote_request(quote_request_id: str, body: dict[str, Any]) -> Any:
|
|
1385
|
+
"""POST /admin/quote_requests/{id}/reply_status_note — {status_note, notification_kind}."""
|
|
1386
|
+
return post_json(f"/admin/quote_requests/{quote_request_id}/reply_status_note", body)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def admin_list_feature_flags() -> Any:
|
|
1390
|
+
"""GET /admin/feature-flags — every flag with its allow-list resolved."""
|
|
1391
|
+
return get_json("/admin/feature-flags")
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def admin_create_feature_flag(body: dict[str, Any]) -> Any:
|
|
1395
|
+
"""POST /admin/feature-flags — {key, description?, enabled?, value?}."""
|
|
1396
|
+
return post_json("/admin/feature-flags", body)
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def admin_update_feature_flag(key: str, body: dict[str, Any]) -> Any:
|
|
1400
|
+
"""PATCH /admin/feature-flags/{key} — enabled / description / value /
|
|
1401
|
+
add_user_id / remove_user_id, one account at a time."""
|
|
1402
|
+
return patch_json(f"/admin/feature-flags/{quote(key, safe='')}", body)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def admin_delete_feature_flag(key: str) -> Any:
|
|
1406
|
+
"""DELETE /admin/feature-flags/{key} — every check of the flag then reads false."""
|
|
1407
|
+
return delete_json(f"/admin/feature-flags/{quote(key, safe='')}")
|
|
1408
|
+
|
|
1409
|
+
|
|
1285
1410
|
# --- supplier persona: a factory acting on its own RFQs, quotes and orders --------
|
|
1286
1411
|
|
|
1287
1412
|
|
makea_cli/commands/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from makea_cli.commands.directory.cmd import register as register_directory
|
|
|
11
11
|
from makea_cli.commands.document.cmd import register as register_document
|
|
12
12
|
from makea_cli.commands.email.cmd import register as register_email
|
|
13
13
|
from makea_cli.commands.factory.cmd import register as register_factory
|
|
14
|
+
from makea_cli.commands.flags.cmd import register as register_flags
|
|
14
15
|
from makea_cli.commands.library.cmd import register as register_library
|
|
15
16
|
from makea_cli.commands.metrics.cmd import register as register_metrics
|
|
16
17
|
from makea_cli.commands.orders.cmd import register as register_orders
|
|
@@ -41,3 +42,4 @@ def register_all(app: typer.Typer) -> None:
|
|
|
41
42
|
register_playbook(app)
|
|
42
43
|
register_about(app)
|
|
43
44
|
register_tasks(app)
|
|
45
|
+
register_flags(app)
|
|
@@ -54,6 +54,20 @@ def register(app: typer.Typer) -> None:
|
|
|
54
54
|
"""
|
|
55
55
|
print_json_or_exit(console, lambda: api_client.get_all_users(limit=limit, offset=offset))
|
|
56
56
|
|
|
57
|
+
@command(
|
|
58
|
+
app,
|
|
59
|
+
"admin",
|
|
60
|
+
"get",
|
|
61
|
+
"user",
|
|
62
|
+
api="GET /admin/user-profiles/{user_id}",
|
|
63
|
+
help="GET /admin/user-profiles/{user_id} — one account's profile (Cognito sub as id).",
|
|
64
|
+
)
|
|
65
|
+
def get_user(
|
|
66
|
+
user_id: str = typer.Argument(..., help="Cognito sub (user_id) of the account."),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Show one user's profile."""
|
|
69
|
+
print_json_or_exit(console, lambda: api_client.admin_get_user_profile(user_id))
|
|
70
|
+
|
|
57
71
|
@command(
|
|
58
72
|
app,
|
|
59
73
|
"admin",
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Admin feature flags: the switches on the admin page's Flags tab.
|
|
2
|
+
|
|
3
|
+
A flag is on globally, or, while the global switch is off, on for the accounts
|
|
4
|
+
on its allow-list. The allow-list holds user ids (Cognito sub), never emails.
|
|
5
|
+
Every change reaches prod users at once, so every write asks first.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from makea_cli import api_client
|
|
17
|
+
from makea_cli.commands._util import print_json_or_exit
|
|
18
|
+
from makea_cli.registry import command
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
# Mirrors garmentby_service.common.feature_flags (FLAG_KEY_PATTERN, MAX_FLAG_KEY_CHARS).
|
|
23
|
+
_FLAG_KEY = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
24
|
+
_FLAG_KEY_MAX = 64
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _flag_key(raw: str) -> str:
|
|
28
|
+
key = raw.strip().upper()
|
|
29
|
+
if not _FLAG_KEY.match(key) or len(key) > _FLAG_KEY_MAX:
|
|
30
|
+
raise typer.BadParameter(
|
|
31
|
+
f"flag key must be ALL_CAPS with underscores, at most {_FLAG_KEY_MAX} characters"
|
|
32
|
+
)
|
|
33
|
+
return key
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def register(app: typer.Typer) -> None:
|
|
37
|
+
@command(
|
|
38
|
+
app,
|
|
39
|
+
"admin",
|
|
40
|
+
"list",
|
|
41
|
+
"feature-flag",
|
|
42
|
+
api="GET /admin/feature-flags",
|
|
43
|
+
help=(
|
|
44
|
+
"GET /admin/feature-flags — every flag: global switch, default value, and the "
|
|
45
|
+
"accounts it is on for (id, email, name)."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
def list_feature_flags() -> None:
|
|
49
|
+
"""List feature flags."""
|
|
50
|
+
print_json_or_exit(console, api_client.admin_list_feature_flags)
|
|
51
|
+
|
|
52
|
+
@command(
|
|
53
|
+
app,
|
|
54
|
+
"admin",
|
|
55
|
+
"create",
|
|
56
|
+
"feature-flag",
|
|
57
|
+
api="POST /admin/feature-flags",
|
|
58
|
+
side_effects=["with --enabled the flag is on for every account at once"],
|
|
59
|
+
approval="ask",
|
|
60
|
+
reversible=True,
|
|
61
|
+
idempotent=False,
|
|
62
|
+
help=(
|
|
63
|
+
"POST /admin/feature-flags — add a flag (off unless --enabled). An existing key "
|
|
64
|
+
"is refused."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
def create_feature_flag(
|
|
68
|
+
key: str = typer.Argument(..., help="Flag key, ALL_CAPS_WITH_UNDERSCORES."),
|
|
69
|
+
description: str | None = typer.Option(None, "--description", help="What it gates."),
|
|
70
|
+
enabled: bool = typer.Option(False, "--enabled", help="Turn it on globally now."),
|
|
71
|
+
value: str | None = typer.Option(
|
|
72
|
+
None,
|
|
73
|
+
"--value",
|
|
74
|
+
help="Default value, for flags that name a choice (e.g. DEEPSEEK).",
|
|
75
|
+
),
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Create a feature flag."""
|
|
78
|
+
body: dict[str, Any] = {"key": _flag_key(key), "enabled": enabled}
|
|
79
|
+
if description is not None:
|
|
80
|
+
body["description"] = description.strip()
|
|
81
|
+
if value is not None:
|
|
82
|
+
body["value"] = value.strip()
|
|
83
|
+
print_json_or_exit(console, lambda: api_client.admin_create_feature_flag(body))
|
|
84
|
+
|
|
85
|
+
@command(
|
|
86
|
+
app,
|
|
87
|
+
"admin",
|
|
88
|
+
"update",
|
|
89
|
+
"feature-flag",
|
|
90
|
+
api="PATCH /admin/feature-flags/{key}",
|
|
91
|
+
side_effects=["the change reaches the affected accounts at once"],
|
|
92
|
+
approval="ask",
|
|
93
|
+
reversible=True,
|
|
94
|
+
idempotent=True,
|
|
95
|
+
help=(
|
|
96
|
+
"PATCH /admin/feature-flags/{key} — flip the global switch, edit the description "
|
|
97
|
+
"or default value, or add / remove one account on the allow-list (by user id; "
|
|
98
|
+
"find it with admin list user)."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
def update_feature_flag(
|
|
102
|
+
key: str = typer.Argument(..., help="Flag key."),
|
|
103
|
+
enabled: bool | None = typer.Option(
|
|
104
|
+
None, "--enabled/--disabled", help="The global switch."
|
|
105
|
+
),
|
|
106
|
+
description: str | None = typer.Option(None, "--description"),
|
|
107
|
+
value: str | None = typer.Option(
|
|
108
|
+
None, "--value", help="Default value; an empty string clears it."
|
|
109
|
+
),
|
|
110
|
+
add_user: str | None = typer.Option(
|
|
111
|
+
None, "--add-user", help="User id (Cognito sub) to put on the allow-list."
|
|
112
|
+
),
|
|
113
|
+
remove_user: str | None = typer.Option(
|
|
114
|
+
None, "--remove-user", help="User id to take off the allow-list."
|
|
115
|
+
),
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Change a feature flag."""
|
|
118
|
+
body: dict[str, Any] = {}
|
|
119
|
+
if enabled is not None:
|
|
120
|
+
body["enabled"] = enabled
|
|
121
|
+
if description is not None:
|
|
122
|
+
body["description"] = description.strip()
|
|
123
|
+
if value is not None:
|
|
124
|
+
body["value"] = value.strip()
|
|
125
|
+
if add_user and add_user.strip():
|
|
126
|
+
body["add_user_id"] = add_user.strip()
|
|
127
|
+
if remove_user and remove_user.strip():
|
|
128
|
+
body["remove_user_id"] = remove_user.strip()
|
|
129
|
+
if not body:
|
|
130
|
+
raise typer.BadParameter(
|
|
131
|
+
"pass at least one of --enabled/--disabled, --description, --value, "
|
|
132
|
+
"--add-user, --remove-user"
|
|
133
|
+
)
|
|
134
|
+
flag = _flag_key(key)
|
|
135
|
+
print_json_or_exit(console, lambda: api_client.admin_update_feature_flag(flag, body))
|
|
136
|
+
|
|
137
|
+
@command(
|
|
138
|
+
app,
|
|
139
|
+
"admin",
|
|
140
|
+
"delete",
|
|
141
|
+
"feature-flag",
|
|
142
|
+
api="DELETE /admin/feature-flags/{key}",
|
|
143
|
+
side_effects=["every check of the flag reads false from then on"],
|
|
144
|
+
approval="ask",
|
|
145
|
+
reversible=False,
|
|
146
|
+
idempotent=True,
|
|
147
|
+
help=(
|
|
148
|
+
"DELETE /admin/feature-flags/{key} — remove a flag. Every check of it then reads "
|
|
149
|
+
"false, so delete it only once the code that reads it is gone."
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
def delete_feature_flag(
|
|
153
|
+
key: str = typer.Argument(..., help="Flag key."),
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Delete a feature flag."""
|
|
156
|
+
flag = _flag_key(key)
|
|
157
|
+
print_json_or_exit(console, lambda: api_client.admin_delete_feature_flag(flag))
|
makea_cli/commands/orders/cmd.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
3
5
|
import typer
|
|
4
6
|
from rich.console import Console
|
|
5
7
|
|
|
@@ -10,6 +12,62 @@ from makea_cli.commands._util import print_json_or_exit
|
|
|
10
12
|
|
|
11
13
|
console = Console()
|
|
12
14
|
|
|
15
|
+
# What an admin may set by hand, mirrored from the backend's SamplingOrderStatus /
|
|
16
|
+
# ProductionOrderStatus. The statuses that record a payment (PAID, DEPOSIT_PAID,
|
|
17
|
+
# FINAL_PAYMENT_PAID) are left out on purpose: money steps are recorded by the
|
|
18
|
+
# payment, never by a command.
|
|
19
|
+
SAMPLING_ORDER_STATUSES = (
|
|
20
|
+
"DRAFT",
|
|
21
|
+
"IN_PROGRESS",
|
|
22
|
+
"SAMPLING_COMPLETED",
|
|
23
|
+
"APPROVAL",
|
|
24
|
+
"SHIPPED",
|
|
25
|
+
"DELIVERED",
|
|
26
|
+
"CANCELLED",
|
|
27
|
+
)
|
|
28
|
+
PRODUCTION_ORDER_STATUSES = (
|
|
29
|
+
"DRAFT",
|
|
30
|
+
"IN_REVIEW",
|
|
31
|
+
"READY_FOR_PRODUCTION",
|
|
32
|
+
"PRODUCTION_IN_PROGRESS",
|
|
33
|
+
"PRODUCTION_COMPLETED",
|
|
34
|
+
"SHIPPED",
|
|
35
|
+
"DELIVERED",
|
|
36
|
+
"COMPLETED",
|
|
37
|
+
"CANCELLED",
|
|
38
|
+
)
|
|
39
|
+
PAID_STATUSES = ("PAID", "DEPOSIT_PAID", "FINAL_PAYMENT_PAID")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _order_status(value: str, allowed: tuple[str, ...]) -> str:
|
|
43
|
+
v = value.strip().upper()
|
|
44
|
+
if v in PAID_STATUSES:
|
|
45
|
+
raise typer.BadParameter(
|
|
46
|
+
f"{v} is recorded by the payment itself; the CLI does not mark orders paid"
|
|
47
|
+
)
|
|
48
|
+
if v not in allowed:
|
|
49
|
+
raise typer.BadParameter(f"--status must be one of {', '.join(allowed)}")
|
|
50
|
+
return v
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _tracking_body(
|
|
54
|
+
tracking_number: str | None,
|
|
55
|
+
tracking_url: str | None,
|
|
56
|
+
shipping_method: str | None,
|
|
57
|
+
carrier_id: int | None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Only the fields given: the backend leaves an absent field unchanged."""
|
|
60
|
+
body: dict[str, Any] = {}
|
|
61
|
+
if tracking_number is not None:
|
|
62
|
+
body["tracking_number"] = tracking_number.strip()
|
|
63
|
+
if tracking_url is not None:
|
|
64
|
+
body["tracking_url"] = tracking_url.strip()
|
|
65
|
+
if shipping_method is not None:
|
|
66
|
+
body["shipping_method"] = shipping_method.strip()
|
|
67
|
+
if carrier_id is not None:
|
|
68
|
+
body["carrier_id"] = carrier_id
|
|
69
|
+
return body
|
|
70
|
+
|
|
13
71
|
|
|
14
72
|
def register(app: typer.Typer) -> None:
|
|
15
73
|
@command(
|
|
@@ -386,3 +444,197 @@ def register(app: typer.Typer) -> None:
|
|
|
386
444
|
},
|
|
387
445
|
),
|
|
388
446
|
)
|
|
447
|
+
|
|
448
|
+
@command(
|
|
449
|
+
app,
|
|
450
|
+
"admin",
|
|
451
|
+
"get",
|
|
452
|
+
"production-order",
|
|
453
|
+
api="GET /admin/production_orders/{production_order_id}/details",
|
|
454
|
+
help=(
|
|
455
|
+
"GET /admin/production_orders/{id}/details — one production (or restock) order in "
|
|
456
|
+
"full: items, status, progress steps, tracking, notes, documents and linked "
|
|
457
|
+
"suppliers. For only its pricing use admin get production-pricing."
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
def get_production_order(
|
|
461
|
+
production_order_id: str = typer.Argument(..., help="Production (or restock) order id."),
|
|
462
|
+
) -> None:
|
|
463
|
+
"""Show one production order."""
|
|
464
|
+
print_json_or_exit(
|
|
465
|
+
console,
|
|
466
|
+
lambda: api_client.get_production_order_details(production_order_id),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@command(
|
|
470
|
+
app,
|
|
471
|
+
"admin",
|
|
472
|
+
"update",
|
|
473
|
+
"sampling-order",
|
|
474
|
+
api="PUT /admin/sampling_orders/{sampling_order_id}/status",
|
|
475
|
+
side_effects=[
|
|
476
|
+
"records the status change in the order's history",
|
|
477
|
+
"the brand and the factory see the new status",
|
|
478
|
+
"SAMPLING_COMPLETED stamps completed_at, adds the completed progress step and "
|
|
479
|
+
"starts the sample analysis",
|
|
480
|
+
],
|
|
481
|
+
approval="ask",
|
|
482
|
+
reversible=True,
|
|
483
|
+
idempotent=True,
|
|
484
|
+
help=(
|
|
485
|
+
"PUT /admin/sampling_orders/{id}/status — move a sampling order to another status "
|
|
486
|
+
f"({', '.join(SAMPLING_ORDER_STATUSES)}). PAID is recorded by the payment and "
|
|
487
|
+
"is refused here. Prefer admin update product-step, which moves the same "
|
|
488
|
+
"statuses through the step model."
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
def update_sampling_order(
|
|
492
|
+
sampling_order_id: str = typer.Argument(..., help="Sampling order id."),
|
|
493
|
+
status: str = typer.Option(
|
|
494
|
+
..., "--status", help=f"One of {', '.join(SAMPLING_ORDER_STATUSES)}."
|
|
495
|
+
),
|
|
496
|
+
) -> None:
|
|
497
|
+
"""Change a sampling order's status."""
|
|
498
|
+
value = _order_status(status, SAMPLING_ORDER_STATUSES)
|
|
499
|
+
print_json_or_exit(
|
|
500
|
+
console,
|
|
501
|
+
lambda: api_client.admin_update_sampling_order_status(sampling_order_id, value),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
@command(
|
|
505
|
+
app,
|
|
506
|
+
"admin",
|
|
507
|
+
"update",
|
|
508
|
+
"production-order",
|
|
509
|
+
api="PUT /admin/production_orders/{production_order_id}/status",
|
|
510
|
+
side_effects=[
|
|
511
|
+
"records the status change in the order's history",
|
|
512
|
+
"SHIPPED / DELIVERED / PRODUCTION_COMPLETED / COMPLETED notify the brand in-app",
|
|
513
|
+
"READY_FOR_PRODUCTION runs the ready transition (quote flow and product status)",
|
|
514
|
+
],
|
|
515
|
+
approval="ask",
|
|
516
|
+
reversible=True,
|
|
517
|
+
idempotent=True,
|
|
518
|
+
help=(
|
|
519
|
+
"PUT /admin/production_orders/{id}/status — move a production order to another "
|
|
520
|
+
f"status ({', '.join(PRODUCTION_ORDER_STATUSES)}). The paid statuses (PAID, "
|
|
521
|
+
"DEPOSIT_PAID, FINAL_PAYMENT_PAID) are recorded by the payment and are refused "
|
|
522
|
+
"here."
|
|
523
|
+
),
|
|
524
|
+
)
|
|
525
|
+
def update_production_order(
|
|
526
|
+
production_order_id: str = typer.Argument(..., help="Production (or restock) order id."),
|
|
527
|
+
status: str = typer.Option(
|
|
528
|
+
..., "--status", help=f"One of {', '.join(PRODUCTION_ORDER_STATUSES)}."
|
|
529
|
+
),
|
|
530
|
+
) -> None:
|
|
531
|
+
"""Change a production order's status."""
|
|
532
|
+
value = _order_status(status, PRODUCTION_ORDER_STATUSES)
|
|
533
|
+
print_json_or_exit(
|
|
534
|
+
console,
|
|
535
|
+
lambda: api_client.admin_update_production_order_status(production_order_id, value),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
@command(
|
|
539
|
+
app,
|
|
540
|
+
"admin",
|
|
541
|
+
"set",
|
|
542
|
+
"sampling-order-tracking",
|
|
543
|
+
api="PUT /admin/sampling_orders/{sampling_order_id}/tracking",
|
|
544
|
+
side_effects=[
|
|
545
|
+
"a new tracking number marks the order SHIPPED and tells the brand it shipped",
|
|
546
|
+
"the parcel is followed by the carrier sweep",
|
|
547
|
+
],
|
|
548
|
+
approval="ask",
|
|
549
|
+
reversible=True,
|
|
550
|
+
idempotent=True,
|
|
551
|
+
help=(
|
|
552
|
+
"PUT /admin/sampling_orders/{id}/tracking — the sample's parcel: tracking number, "
|
|
553
|
+
"URL, shipping method, carrier. Only what you pass is changed; an empty string "
|
|
554
|
+
"clears a field."
|
|
555
|
+
),
|
|
556
|
+
)
|
|
557
|
+
def set_sampling_order_tracking(
|
|
558
|
+
sampling_order_id: str = typer.Argument(..., help="Sampling order id."),
|
|
559
|
+
tracking_number: str | None = typer.Option(None, "--tracking-number"),
|
|
560
|
+
tracking_url: str | None = typer.Option(None, "--tracking-url"),
|
|
561
|
+
shipping_method: str | None = typer.Option(
|
|
562
|
+
None, "--shipping-method", help="e.g. DHL Express, FedEx, Sea freight."
|
|
563
|
+
),
|
|
564
|
+
carrier_id: int | None = typer.Option(
|
|
565
|
+
None, "--carrier-id", help="17track carrier code (number)."
|
|
566
|
+
),
|
|
567
|
+
) -> None:
|
|
568
|
+
"""Set a sampling order's tracking information."""
|
|
569
|
+
body = _tracking_body(tracking_number, tracking_url, shipping_method, carrier_id)
|
|
570
|
+
if not any(body.get(k) for k in ("tracking_number", "tracking_url", "shipping_method")):
|
|
571
|
+
raise typer.BadParameter(
|
|
572
|
+
"pass at least one of --tracking-number, --tracking-url, --shipping-method"
|
|
573
|
+
)
|
|
574
|
+
print_json_or_exit(
|
|
575
|
+
console,
|
|
576
|
+
lambda: api_client.admin_set_sampling_order_tracking(sampling_order_id, body),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
@command(
|
|
580
|
+
app,
|
|
581
|
+
"admin",
|
|
582
|
+
"set",
|
|
583
|
+
"production-order-tracking",
|
|
584
|
+
api="POST /admin/production_orders/{production_order_id}/tracking",
|
|
585
|
+
side_effects=[
|
|
586
|
+
"a new tracking number marks the order SHIPPED and tells the brand it shipped",
|
|
587
|
+
"the parcel is followed by the carrier sweep",
|
|
588
|
+
],
|
|
589
|
+
approval="ask",
|
|
590
|
+
reversible=True,
|
|
591
|
+
idempotent=True,
|
|
592
|
+
help=(
|
|
593
|
+
"POST /admin/production_orders/{id}/tracking — the bulk parcel: tracking number, "
|
|
594
|
+
"URL, shipping method, carrier, plus cartons and gross weight off the packing "
|
|
595
|
+
"list. Only what you pass is changed; an empty string clears a field."
|
|
596
|
+
),
|
|
597
|
+
)
|
|
598
|
+
def set_production_order_tracking(
|
|
599
|
+
production_order_id: str = typer.Argument(..., help="Production (or restock) order id."),
|
|
600
|
+
tracking_number: str | None = typer.Option(None, "--tracking-number"),
|
|
601
|
+
tracking_url: str | None = typer.Option(None, "--tracking-url"),
|
|
602
|
+
shipping_method: str | None = typer.Option(
|
|
603
|
+
None, "--shipping-method", help="e.g. DHL Express, FedEx, Sea freight."
|
|
604
|
+
),
|
|
605
|
+
carrier_id: int | None = typer.Option(
|
|
606
|
+
None, "--carrier-id", help="17track carrier code (number)."
|
|
607
|
+
),
|
|
608
|
+
cartons: str | None = typer.Option(
|
|
609
|
+
None,
|
|
610
|
+
"--cartons",
|
|
611
|
+
help="Number of cartons on the packing list (empty clears).",
|
|
612
|
+
),
|
|
613
|
+
gross_weight_kg: str | None = typer.Option(
|
|
614
|
+
None, "--gross-weight-kg", help="Gross weight in kg (empty clears)."
|
|
615
|
+
),
|
|
616
|
+
) -> None:
|
|
617
|
+
"""Set a production order's tracking information, cartons and gross weight."""
|
|
618
|
+
body = _tracking_body(tracking_number, tracking_url, shipping_method, carrier_id)
|
|
619
|
+
if cartons is not None:
|
|
620
|
+
body["cartons"] = cartons.strip()
|
|
621
|
+
if gross_weight_kg is not None:
|
|
622
|
+
body["gross_weight_kg"] = gross_weight_kg.strip()
|
|
623
|
+
if not any(
|
|
624
|
+
str(body.get(k) or "").strip()
|
|
625
|
+
for k in (
|
|
626
|
+
"tracking_number",
|
|
627
|
+
"tracking_url",
|
|
628
|
+
"shipping_method",
|
|
629
|
+
"cartons",
|
|
630
|
+
"gross_weight_kg",
|
|
631
|
+
)
|
|
632
|
+
):
|
|
633
|
+
raise typer.BadParameter(
|
|
634
|
+
"pass at least one of --tracking-number, --tracking-url, --shipping-method, "
|
|
635
|
+
"--cartons, --gross-weight-kg"
|
|
636
|
+
)
|
|
637
|
+
print_json_or_exit(
|
|
638
|
+
console,
|
|
639
|
+
lambda: api_client.admin_set_production_order_tracking(production_order_id, body),
|
|
640
|
+
)
|