makea-cli 0.1.21__tar.gz → 0.1.23__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.1.21 → makea_cli-0.1.23}/PKG-INFO +5 -2
- {makea_cli-0.1.21 → makea_cli-0.1.23}/README.md +4 -1
- makea_cli-0.1.23/makea_cli/__init__.py +1 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/api_client.py +40 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/supplier/cmd.py +616 -57
- {makea_cli-0.1.21 → makea_cli-0.1.23}/pyproject.toml +1 -1
- makea_cli-0.1.21/makea_cli/__init__.py +0 -1
- {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/link-supplier-user/SKILL.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/.gitignore +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea-collection-upload/SKILL.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea-collection-upload/scripts/create_products.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/__main__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/auth_pkce.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/_util.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/analytics/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/analytics/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/auth/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/auth/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/designer/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/designer/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/directory/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/directory/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/document/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/document/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/email/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/email/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/library/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/library/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/metrics/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/metrics/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/orders/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/orders/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/product/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/product/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/production/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/production/cmd.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/production/pricing_spec.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/commands/supplier/__init__.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/config.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/main.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/makea_cli/posthog_client.py +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/npm/makea-cli/README.md +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/npm/makea-cli/makea-cli.mjs +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/npm/makea-cli/package.json +0 -0
- {makea_cli-0.1.21 → makea_cli-0.1.23}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: makea-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.23
|
|
4
4
|
Summary: Makea admin CLI (browser login + Makea admin HTTP API)
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: httpx>=0.27
|
|
@@ -80,9 +80,12 @@ makea-cli auth --logout
|
|
|
80
80
|
- `makea-cli create-user <email> --user-type "<type>" [--password ...] [--name ...] [--company-name ...] [--username ...]` — 注册用户并直接确认 Cognito 账号(POST `/admin/users/create`,不发验证码邮件,创建后即可用该密码登录)
|
|
81
81
|
- `makea-cli list-suppliers` — 列出供应商(需管理员权限)
|
|
82
82
|
- `makea-cli create-quote-request -s <supplier_id> -p <product_reference_id> [--quote-type SAMPLING|PRODUCTION] [--message ...] [--due-date YYYY-MM-DD]` — 创建并发送 quote request(POST `/admin/supplier/create_quote_request`)
|
|
83
|
+
- `makea-cli get-quote-requests [--product-id <product_id>] [--quote-request-id <id>]` — 列某个产品下的 quote requests,或读单条完整内容(GET `/admin/quote_requests`)
|
|
84
|
+
- `makea-cli update-quote-request <quote_request_id> [--stage SAMPLING] [--status PENDING_SUPPLIER] [--due-date YYYY-MM-DD | --clear-due-date]` — 改 quote request 的 stage / status / 截止日(PATCH `/admin/quote_requests/{id}/update`)。**代供应商报价前常常要先用它**:quote request 停在 ENGAGEMENT(供应商还没接单)时 `submit-supplier-quote` 会被拒;过了 due date 也会被拒
|
|
83
85
|
- `makea-cli get-quote-requests-by-supplier-id <supplier_id> [--status <status>] [-n <limit>]` — 直连后端按 supplier_id 查询 quote requests
|
|
84
86
|
- `makea-cli get-supplier-links-by-supplier-id <supplier_id> [--link-type SAMPLING|PRODUCTION]` — 直连后端按 supplier_id 查询 supplier links
|
|
85
|
-
- `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-
|
|
87
|
+
- `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-value ... --sampling-price-valid-until ... --sample-lead-time ... --deliverables-json ... --customer-note ...` — 以 admin 身份代供应商向指定 quote request 新增 quote。大货阶梯价(`--production-price-tiers-json`)和 `--lead-time` 现在是可选的;样衣加做费、MOQ 口径、寄料地址(CMT / full package 打样报价必填)、可选尺码颜色、规格行、typed link 等字段见 `--help`。pricing groups 提交时给不进去,提交完用 `update-supplier-quote` 补
|
|
88
|
+
- `makea-cli update-supplier-quote <product_id> <link_id> [--sampling-price-value ... --sampling-price-valid-until ...] [--production-price-tiers-json ...] [--pricing-groups-json ...] [...]` — 改一条已提交的 quote(supplier link):只发你传的字段,没传的后端不动;pricing groups(多套阶梯价)只有这条命令能写(POST `/admin/product/edit_supplier_link_override_info`)
|
|
86
89
|
- `makea-cli list-product-orders` — 列出所有设计师产品订单(GET `/admin/designer/get_all_product_orders`)
|
|
87
90
|
- `makea-cli list-sampling-orders-by-product <id>` — 按产品 reference id 列打样单
|
|
88
91
|
- `makea-cli list-production-orders-by-product <id>` — 按产品 reference id 列大货单
|
|
@@ -69,9 +69,12 @@ makea-cli auth --logout
|
|
|
69
69
|
- `makea-cli create-user <email> --user-type "<type>" [--password ...] [--name ...] [--company-name ...] [--username ...]` — 注册用户并直接确认 Cognito 账号(POST `/admin/users/create`,不发验证码邮件,创建后即可用该密码登录)
|
|
70
70
|
- `makea-cli list-suppliers` — 列出供应商(需管理员权限)
|
|
71
71
|
- `makea-cli create-quote-request -s <supplier_id> -p <product_reference_id> [--quote-type SAMPLING|PRODUCTION] [--message ...] [--due-date YYYY-MM-DD]` — 创建并发送 quote request(POST `/admin/supplier/create_quote_request`)
|
|
72
|
+
- `makea-cli get-quote-requests [--product-id <product_id>] [--quote-request-id <id>]` — 列某个产品下的 quote requests,或读单条完整内容(GET `/admin/quote_requests`)
|
|
73
|
+
- `makea-cli update-quote-request <quote_request_id> [--stage SAMPLING] [--status PENDING_SUPPLIER] [--due-date YYYY-MM-DD | --clear-due-date]` — 改 quote request 的 stage / status / 截止日(PATCH `/admin/quote_requests/{id}/update`)。**代供应商报价前常常要先用它**:quote request 停在 ENGAGEMENT(供应商还没接单)时 `submit-supplier-quote` 会被拒;过了 due date 也会被拒
|
|
72
74
|
- `makea-cli get-quote-requests-by-supplier-id <supplier_id> [--status <status>] [-n <limit>]` — 直连后端按 supplier_id 查询 quote requests
|
|
73
75
|
- `makea-cli get-supplier-links-by-supplier-id <supplier_id> [--link-type SAMPLING|PRODUCTION]` — 直连后端按 supplier_id 查询 supplier links
|
|
74
|
-
- `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-
|
|
76
|
+
- `makea-cli submit-supplier-quote <quote_request_id> --link-name ... --currency ... --sampling-price-value ... --sampling-price-valid-until ... --sample-lead-time ... --deliverables-json ... --customer-note ...` — 以 admin 身份代供应商向指定 quote request 新增 quote。大货阶梯价(`--production-price-tiers-json`)和 `--lead-time` 现在是可选的;样衣加做费、MOQ 口径、寄料地址(CMT / full package 打样报价必填)、可选尺码颜色、规格行、typed link 等字段见 `--help`。pricing groups 提交时给不进去,提交完用 `update-supplier-quote` 补
|
|
77
|
+
- `makea-cli update-supplier-quote <product_id> <link_id> [--sampling-price-value ... --sampling-price-valid-until ...] [--production-price-tiers-json ...] [--pricing-groups-json ...] [...]` — 改一条已提交的 quote(supplier link):只发你传的字段,没传的后端不动;pricing groups(多套阶梯价)只有这条命令能写(POST `/admin/product/edit_supplier_link_override_info`)
|
|
75
78
|
- `makea-cli list-product-orders` — 列出所有设计师产品订单(GET `/admin/designer/get_all_product_orders`)
|
|
76
79
|
- `makea-cli list-sampling-orders-by-product <id>` — 按产品 reference id 列打样单
|
|
77
80
|
- `makea-cli list-production-orders-by-product <id>` — 按产品 reference id 列大货单
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.23"
|
|
@@ -294,6 +294,25 @@ def get_quote_requests(*, product_id: str | None = None) -> Any:
|
|
|
294
294
|
return get_json("/admin/quote_requests", params=params)
|
|
295
295
|
|
|
296
296
|
|
|
297
|
+
def get_quote_request(*, quote_request_id: str) -> Any:
|
|
298
|
+
"""
|
|
299
|
+
GET /admin/quote_requests/{quote_request_id} — one quote request in full.
|
|
300
|
+
"""
|
|
301
|
+
return get_json(f"/admin/quote_requests/{quote_request_id}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def update_quote_request(
|
|
305
|
+
*,
|
|
306
|
+
quote_request_id: str,
|
|
307
|
+
body: dict[str, Any],
|
|
308
|
+
) -> Any:
|
|
309
|
+
"""
|
|
310
|
+
PATCH /admin/quote_requests/{quote_request_id}/update — set a quote request's
|
|
311
|
+
stage, status and/or due date. Only the keys present in ``body`` are applied.
|
|
312
|
+
"""
|
|
313
|
+
return patch_json(f"/admin/quote_requests/{quote_request_id}/update", body)
|
|
314
|
+
|
|
315
|
+
|
|
297
316
|
def create_quote_request(
|
|
298
317
|
*,
|
|
299
318
|
supplier_id: str,
|
|
@@ -390,6 +409,27 @@ def submit_admin_quote_for_quote_request(
|
|
|
390
409
|
return post_json(f"/admin/quote_requests/{quote_request_id}/submit_quote", body)
|
|
391
410
|
|
|
392
411
|
|
|
412
|
+
def edit_supplier_link_override_info(
|
|
413
|
+
*,
|
|
414
|
+
product_id: str,
|
|
415
|
+
link_id: str,
|
|
416
|
+
override_info: dict[str, Any],
|
|
417
|
+
) -> Any:
|
|
418
|
+
"""
|
|
419
|
+
POST /admin/product/edit_supplier_link_override_info — admin updates an existing
|
|
420
|
+
supplier link (a submitted quote) in place. Only the keys present in
|
|
421
|
+
``override_info`` are applied; the rest of the link is left alone.
|
|
422
|
+
"""
|
|
423
|
+
return post_json(
|
|
424
|
+
"/admin/product/edit_supplier_link_override_info",
|
|
425
|
+
{
|
|
426
|
+
"product_id": product_id,
|
|
427
|
+
"link_id": link_id,
|
|
428
|
+
"override_info": override_info,
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
393
433
|
def upload_admin_link_document(
|
|
394
434
|
*,
|
|
395
435
|
file_path: Path,
|
|
@@ -20,6 +20,86 @@ def _normalize_add_supplier_body(raw: dict) -> dict:
|
|
|
20
20
|
return {"supplier": raw}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
MOQ_APPLIES_TO_VALUES = ("style", "size", "color")
|
|
24
|
+
|
|
25
|
+
# Mirrors garmentby_service.domain.QuoteRequest — kept here only so `--help` can
|
|
26
|
+
# name the values; the backend is the one that validates.
|
|
27
|
+
QUOTE_REQUEST_STAGES = ("ENGAGEMENT", "SAMPLING", "PRODUCTION")
|
|
28
|
+
QUOTE_REQUEST_STATUSES = (
|
|
29
|
+
"PENDING_MERCHANT",
|
|
30
|
+
"PENDING_SUPPLIER",
|
|
31
|
+
"NEEDS_CLARIFICATION",
|
|
32
|
+
"COMPLETED",
|
|
33
|
+
"ORDER_PLACED",
|
|
34
|
+
"TERMINATED",
|
|
35
|
+
"RESAMPLE_QUOTE_REQUESTED",
|
|
36
|
+
)
|
|
37
|
+
PRODUCTION_FLOW_STATUSES = (
|
|
38
|
+
"PENDING_QUOTE",
|
|
39
|
+
"QUOTE_SUBMITTED",
|
|
40
|
+
"QUOTE_CONFIRMED",
|
|
41
|
+
"AWAITING_DEPOSIT",
|
|
42
|
+
"DEPOSIT_PAID",
|
|
43
|
+
"IN_PRODUCTION",
|
|
44
|
+
"AWAITING_FINAL_PAYMENT",
|
|
45
|
+
"FINAL_PAYMENT_PAID",
|
|
46
|
+
"SHIPPED",
|
|
47
|
+
"COMPLETED",
|
|
48
|
+
"CANCELLED",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_json_option(raw: str, option_name: str) -> object:
|
|
53
|
+
"""Parse a JSON-valued option, failing with the option's own name."""
|
|
54
|
+
try:
|
|
55
|
+
return json.loads(raw)
|
|
56
|
+
except json.JSONDecodeError as e:
|
|
57
|
+
console.print(f"[red]{option_name} must be valid JSON: {e}[/red]")
|
|
58
|
+
raise typer.Exit(1) from e
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_json_list_option(raw: str, option_name: str) -> list:
|
|
62
|
+
parsed = _parse_json_option(raw, option_name)
|
|
63
|
+
if not isinstance(parsed, list):
|
|
64
|
+
console.print(f"[red]{option_name} must be a JSON array (list).[/red]")
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
return parsed
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_json_object_option(raw: str, option_name: str) -> dict:
|
|
70
|
+
parsed = _parse_json_option(raw, option_name)
|
|
71
|
+
if not isinstance(parsed, dict):
|
|
72
|
+
console.print(f"[red]{option_name} must be a JSON object.[/red]")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
return parsed
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validate_moq_applies_to(values: list[str]) -> list[str]:
|
|
78
|
+
"""Normalize --moq-applies-to; the backend silently drops unknown values."""
|
|
79
|
+
normalized = [str(v).strip().lower() for v in values if str(v).strip()]
|
|
80
|
+
unknown = [v for v in normalized if v not in MOQ_APPLIES_TO_VALUES]
|
|
81
|
+
if unknown:
|
|
82
|
+
console.print(
|
|
83
|
+
f"[red]--moq-applies-to accepts only {', '.join(MOQ_APPLIES_TO_VALUES)} "
|
|
84
|
+
f"(got: {', '.join(unknown)}).[/red]"
|
|
85
|
+
)
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
return [v for v in MOQ_APPLIES_TO_VALUES if v in set(normalized)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_choice(value: str, allowed: tuple[str, ...], option_name: str) -> str:
|
|
91
|
+
"""Reject an out-of-range option before it costs a round trip."""
|
|
92
|
+
if value not in allowed:
|
|
93
|
+
console.print(f"[red]{option_name} must be one of {', '.join(allowed)}.[/red]")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _none_if_null_literal(value: str) -> str | None:
|
|
99
|
+
"""Let an option clear a stored value by passing the literal `null`."""
|
|
100
|
+
return None if value.strip().lower() in ("null", "none") else value
|
|
101
|
+
|
|
102
|
+
|
|
23
103
|
def register(app: typer.Typer) -> None:
|
|
24
104
|
@app.command(
|
|
25
105
|
"list-suppliers",
|
|
@@ -468,11 +548,131 @@ def register(app: typer.Typer) -> None:
|
|
|
468
548
|
),
|
|
469
549
|
)
|
|
470
550
|
|
|
551
|
+
@app.command(
|
|
552
|
+
"get-quote-requests",
|
|
553
|
+
help=(
|
|
554
|
+
"GET /admin/quote_requests — list quote requests, optionally for one product. "
|
|
555
|
+
"Pass --quote-request-id to read a single one in full "
|
|
556
|
+
"(GET /admin/quote_requests/{id})."
|
|
557
|
+
),
|
|
558
|
+
)
|
|
559
|
+
def get_quote_requests(
|
|
560
|
+
product_id: str | None = typer.Option(
|
|
561
|
+
None,
|
|
562
|
+
"--product-id",
|
|
563
|
+
"-p",
|
|
564
|
+
help="product_id: only quote requests on this product (product_reference_id).",
|
|
565
|
+
),
|
|
566
|
+
quote_request_id: str | None = typer.Option(
|
|
567
|
+
None,
|
|
568
|
+
"--quote-request-id",
|
|
569
|
+
help="quote_request_id: read this one quote request in full instead of a list.",
|
|
570
|
+
),
|
|
571
|
+
) -> None:
|
|
572
|
+
"""
|
|
573
|
+
List quote requests, or read one.
|
|
574
|
+
"""
|
|
575
|
+
if quote_request_id:
|
|
576
|
+
print_json_or_exit(
|
|
577
|
+
console,
|
|
578
|
+
lambda: api_client.get_quote_request(quote_request_id=quote_request_id),
|
|
579
|
+
)
|
|
580
|
+
return
|
|
581
|
+
print_json_or_exit(
|
|
582
|
+
console,
|
|
583
|
+
lambda: api_client.get_quote_requests(product_id=product_id),
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
@app.command(
|
|
587
|
+
"update-quote-request",
|
|
588
|
+
help=(
|
|
589
|
+
"PATCH /admin/quote_requests/{quote_request_id}/update — set a quote "
|
|
590
|
+
"request's stage, status and/or due date. Only what you pass is changed. "
|
|
591
|
+
"This is the door that unblocks a submit: submit-supplier-quote requires "
|
|
592
|
+
"stage SAMPLING or PRODUCTION, and a quote request sits in ENGAGEMENT "
|
|
593
|
+
"until the supplier accepts it."
|
|
594
|
+
),
|
|
595
|
+
)
|
|
596
|
+
def update_quote_request(
|
|
597
|
+
quote_request_id: str = typer.Argument(
|
|
598
|
+
...,
|
|
599
|
+
help="quote_request_id: the quote request to update.",
|
|
600
|
+
),
|
|
601
|
+
stage: str | None = typer.Option(
|
|
602
|
+
None,
|
|
603
|
+
"--stage",
|
|
604
|
+
help=(
|
|
605
|
+
"current_stage: ENGAGEMENT (supplier has not accepted yet), SAMPLING or "
|
|
606
|
+
"PRODUCTION. Moving to SAMPLING/PRODUCTION by hand skips the supplier's "
|
|
607
|
+
"own acceptance, so accepted_at / terms_accepted_at stay empty."
|
|
608
|
+
),
|
|
609
|
+
),
|
|
610
|
+
status: str | None = typer.Option(
|
|
611
|
+
None,
|
|
612
|
+
"--status",
|
|
613
|
+
help=(
|
|
614
|
+
"status: for ENGAGEMENT/SAMPLING one of "
|
|
615
|
+
f"{', '.join(QUOTE_REQUEST_STATUSES)}; "
|
|
616
|
+
"for PRODUCTION one of "
|
|
617
|
+
f"{', '.join(PRODUCTION_FLOW_STATUSES)}. "
|
|
618
|
+
"The backend validates against whichever stage the request ends up in."
|
|
619
|
+
),
|
|
620
|
+
),
|
|
621
|
+
due_date: str | None = typer.Option(
|
|
622
|
+
None,
|
|
623
|
+
"--due-date",
|
|
624
|
+
help=(
|
|
625
|
+
"due_date: ISO date (YYYY-MM-DD). A quote request past its due date "
|
|
626
|
+
"cannot be quoted, so this is the other thing that blocks a submit."
|
|
627
|
+
),
|
|
628
|
+
),
|
|
629
|
+
clear_due_date: bool = typer.Option(
|
|
630
|
+
False,
|
|
631
|
+
"--clear-due-date",
|
|
632
|
+
help="Send due_date as null, leaving the quote request with no deadline.",
|
|
633
|
+
),
|
|
634
|
+
) -> None:
|
|
635
|
+
"""
|
|
636
|
+
Update a quote request's stage, status and/or due date.
|
|
637
|
+
"""
|
|
638
|
+
if due_date and clear_due_date:
|
|
639
|
+
console.print("[red]Pass either --due-date or --clear-due-date, not both.[/red]")
|
|
640
|
+
raise typer.Exit(1)
|
|
641
|
+
|
|
642
|
+
body: dict[str, object] = {}
|
|
643
|
+
if stage is not None:
|
|
644
|
+
body["current_stage"] = _validate_choice(
|
|
645
|
+
stage.strip().upper(), QUOTE_REQUEST_STAGES, "--stage"
|
|
646
|
+
)
|
|
647
|
+
if status is not None:
|
|
648
|
+
body["status"] = status.strip().upper()
|
|
649
|
+
if due_date:
|
|
650
|
+
body["due_date"] = due_date.strip()
|
|
651
|
+
elif clear_due_date:
|
|
652
|
+
body["due_date"] = None
|
|
653
|
+
|
|
654
|
+
if not body:
|
|
655
|
+
console.print(
|
|
656
|
+
"[red]Nothing to update: pass --stage, --status, --due-date or "
|
|
657
|
+
"--clear-due-date.[/red]"
|
|
658
|
+
)
|
|
659
|
+
raise typer.Exit(1)
|
|
660
|
+
|
|
661
|
+
print_json_or_exit(
|
|
662
|
+
console,
|
|
663
|
+
lambda: api_client.update_quote_request(
|
|
664
|
+
quote_request_id=quote_request_id,
|
|
665
|
+
body=body,
|
|
666
|
+
),
|
|
667
|
+
)
|
|
668
|
+
|
|
471
669
|
@app.command(
|
|
472
670
|
"submit-supplier-quote",
|
|
473
671
|
help=(
|
|
474
672
|
"POST /admin/quote_requests/{quote_request_id}/submit_quote — "
|
|
475
|
-
"admin submits a new quote (link) to a quote request."
|
|
673
|
+
"admin submits a new quote (link) to a quote request. "
|
|
674
|
+
"Pricing groups cannot be sent here (the submit endpoint ignores them) — "
|
|
675
|
+
"set them afterwards with `update-supplier-quote`."
|
|
476
676
|
),
|
|
477
677
|
)
|
|
478
678
|
def submit_supplier_quote(
|
|
@@ -490,10 +690,14 @@ def register(app: typer.Typer) -> None:
|
|
|
490
690
|
"--currency",
|
|
491
691
|
help="currency: quote currency (used by sampling price and fees).",
|
|
492
692
|
),
|
|
493
|
-
|
|
693
|
+
sampling_price_value: float = typer.Option(
|
|
494
694
|
...,
|
|
695
|
+
"--sampling-price-value",
|
|
495
696
|
"--sampling-price-amount",
|
|
496
|
-
help=
|
|
697
|
+
help=(
|
|
698
|
+
"sampling_price.value: sampling price (required, number). "
|
|
699
|
+
"Sent as `value` — the backend reads no other key."
|
|
700
|
+
),
|
|
497
701
|
),
|
|
498
702
|
sampling_price_valid_until: str = typer.Option(
|
|
499
703
|
...,
|
|
@@ -503,25 +707,11 @@ def register(app: typer.Typer) -> None:
|
|
|
503
707
|
"(required, ISO datetime string, e.g. 2026-05-01T00:00:00+00:00)."
|
|
504
708
|
),
|
|
505
709
|
),
|
|
506
|
-
production_price_tiers_json: str = typer.Option(
|
|
507
|
-
...,
|
|
508
|
-
"--production-price-tiers-json",
|
|
509
|
-
help=(
|
|
510
|
-
"production_price_tiers: tiered production price array "
|
|
511
|
-
"(required, JSON string). "
|
|
512
|
-
'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
|
|
513
|
-
),
|
|
514
|
-
),
|
|
515
710
|
sample_lead_time: str = typer.Option(
|
|
516
711
|
...,
|
|
517
712
|
"--sample-lead-time",
|
|
518
713
|
help="sample_lead_time: sampling lead time (required, string).",
|
|
519
714
|
),
|
|
520
|
-
lead_time: str = typer.Option(
|
|
521
|
-
...,
|
|
522
|
-
"--lead-time",
|
|
523
|
-
help="lead_time: production lead time (required, string).",
|
|
524
|
-
),
|
|
525
715
|
deliverables_json: str = typer.Option(
|
|
526
716
|
...,
|
|
527
717
|
"--deliverables-json",
|
|
@@ -535,6 +725,28 @@ def register(app: typer.Typer) -> None:
|
|
|
535
725
|
"--customer-note",
|
|
536
726
|
help="customer_note: note for customer/platform (required, string; empty string allowed).",
|
|
537
727
|
),
|
|
728
|
+
production_price_tiers_json: str | None = typer.Option(
|
|
729
|
+
None,
|
|
730
|
+
"--production-price-tiers-json",
|
|
731
|
+
help=(
|
|
732
|
+
"production_price_tiers: tiered production price array "
|
|
733
|
+
"(optional since TECH-93 — a factory may quote the sample only). "
|
|
734
|
+
'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
|
|
735
|
+
),
|
|
736
|
+
),
|
|
737
|
+
lead_time: str | None = typer.Option(
|
|
738
|
+
None,
|
|
739
|
+
"--lead-time",
|
|
740
|
+
help="lead_time: production lead time (optional since TECH-93, string).",
|
|
741
|
+
),
|
|
742
|
+
extra_sample_price_value: float | None = typer.Option(
|
|
743
|
+
None,
|
|
744
|
+
"--extra-sample-price-value",
|
|
745
|
+
help=(
|
|
746
|
+
"extra_sample_price.value: per-unit charge for extra samples "
|
|
747
|
+
"(optional, number; currency defaults to --currency)."
|
|
748
|
+
),
|
|
749
|
+
),
|
|
538
750
|
link_id: str | None = typer.Option(
|
|
539
751
|
None,
|
|
540
752
|
"--link-id",
|
|
@@ -550,6 +762,14 @@ def register(app: typer.Typer) -> None:
|
|
|
550
762
|
"--moq",
|
|
551
763
|
help="moq: minimum order quantity (optional, integer).",
|
|
552
764
|
),
|
|
765
|
+
moq_applies_to: list[str] | None = typer.Option(
|
|
766
|
+
None,
|
|
767
|
+
"--moq-applies-to",
|
|
768
|
+
help=(
|
|
769
|
+
"moq_applies_to: what the MOQ counts (optional, repeatable): "
|
|
770
|
+
"style, size, color. Empty means the supplier did not say."
|
|
771
|
+
),
|
|
772
|
+
),
|
|
553
773
|
production_price_incoterm: str | None = typer.Option(
|
|
554
774
|
None,
|
|
555
775
|
"--production-price-incoterm",
|
|
@@ -560,6 +780,63 @@ def register(app: typer.Typer) -> None:
|
|
|
560
780
|
"--production-payment-term",
|
|
561
781
|
help='production_payment_term: production payment term/deposit ratio (optional, e.g. "40" or "40%").',
|
|
562
782
|
),
|
|
783
|
+
material_ship_to_json: str | None = typer.Option(
|
|
784
|
+
None,
|
|
785
|
+
"--material-ship-to-json",
|
|
786
|
+
help=(
|
|
787
|
+
"material_ship_to_address: where the buyer ships materials "
|
|
788
|
+
"(JSON object with street, city, state, postal_code, country). "
|
|
789
|
+
"REQUIRED by the backend for a CMT or full-package SAMPLING quote — "
|
|
790
|
+
"street, city and country must be non-empty. "
|
|
791
|
+
'Example: \'{"street":"1 Test Rd","city":"Guangzhou","country":"CN"}\''
|
|
792
|
+
),
|
|
793
|
+
),
|
|
794
|
+
available_sizes_json: str | None = typer.Option(
|
|
795
|
+
None,
|
|
796
|
+
"--available-sizes-json",
|
|
797
|
+
help=(
|
|
798
|
+
"available_sizes: sizes the piece comes in (optional, JSON array of strings). "
|
|
799
|
+
'Example: \'["S","M","L"]\''
|
|
800
|
+
),
|
|
801
|
+
),
|
|
802
|
+
available_colors_json: str | None = typer.Option(
|
|
803
|
+
None,
|
|
804
|
+
"--available-colors-json",
|
|
805
|
+
help=(
|
|
806
|
+
"available_colors: colours the piece comes in (optional, JSON array of objects). "
|
|
807
|
+
'Example: \'[{"color_name":"Black","hex_code":"#000000","pantone_code":"Black 6 C"}]\''
|
|
808
|
+
),
|
|
809
|
+
),
|
|
810
|
+
size_chart_document_id: str | None = typer.Option(
|
|
811
|
+
None,
|
|
812
|
+
"--size-chart-document-id",
|
|
813
|
+
help=(
|
|
814
|
+
"size_chart_document_id: the quote's size chart document (optional). "
|
|
815
|
+
'Pass an empty string ("") to send it as null and leave the link without one.'
|
|
816
|
+
),
|
|
817
|
+
),
|
|
818
|
+
component_specifications_json: str | None = typer.Option(
|
|
819
|
+
None,
|
|
820
|
+
"--component-specifications-json",
|
|
821
|
+
help=(
|
|
822
|
+
"component_specifications: free-form specification rows (optional, JSON array). "
|
|
823
|
+
'Example: \'[{"component":"Fabric","specification":"100% cotton, 220gsm"}]\''
|
|
824
|
+
),
|
|
825
|
+
),
|
|
826
|
+
link_type_kind: str | None = typer.Option(
|
|
827
|
+
None,
|
|
828
|
+
"--type",
|
|
829
|
+
help=(
|
|
830
|
+
"type: typed-link kind (optional; default MANUFACTURE). "
|
|
831
|
+
"MANUFACTURE, PRODUCT_DEVELOP, or a material kind (FABRIC, BUTTON, ZIPPER, "
|
|
832
|
+
"ELASTIC, LABEL, HOOK, SNAP, CORD, BUCKLE, TAPE, PATCH, LACE, EYELET)."
|
|
833
|
+
),
|
|
834
|
+
),
|
|
835
|
+
attributes_json: str | None = typer.Option(
|
|
836
|
+
None,
|
|
837
|
+
"--attributes-json",
|
|
838
|
+
help="attributes: LinkAttributes-shaped SKU metadata (optional, JSON object).",
|
|
839
|
+
),
|
|
563
840
|
document_ids_json: str | None = typer.Option(
|
|
564
841
|
None,
|
|
565
842
|
"--document-ids-json",
|
|
@@ -594,76 +871,78 @@ def register(app: typer.Typer) -> None:
|
|
|
594
871
|
"""
|
|
595
872
|
Submit a new quote link to a quote request as admin.
|
|
596
873
|
"""
|
|
597
|
-
|
|
598
|
-
def _parse_json_option(raw: str, option_name: str) -> object:
|
|
599
|
-
try:
|
|
600
|
-
return json.loads(raw)
|
|
601
|
-
except json.JSONDecodeError as e:
|
|
602
|
-
console.print(f"[red]{option_name} must be valid JSON: {e}[/red]")
|
|
603
|
-
raise typer.Exit(1) from e
|
|
604
|
-
|
|
605
|
-
production_price_tiers = _parse_json_option(
|
|
606
|
-
production_price_tiers_json, "--production-price-tiers-json"
|
|
607
|
-
)
|
|
608
|
-
deliverables = _parse_json_option(deliverables_json, "--deliverables-json")
|
|
609
|
-
if not isinstance(production_price_tiers, list):
|
|
610
|
-
console.print("[red]--production-price-tiers-json must be a JSON array (list).[/red]")
|
|
611
|
-
raise typer.Exit(1)
|
|
612
|
-
if not isinstance(deliverables, list):
|
|
613
|
-
console.print("[red]--deliverables-json must be a JSON array (list).[/red]")
|
|
614
|
-
raise typer.Exit(1)
|
|
874
|
+
deliverables = _parse_json_list_option(deliverables_json, "--deliverables-json")
|
|
615
875
|
|
|
616
876
|
body: dict[str, object] = {
|
|
617
877
|
"link_name": link_name,
|
|
618
878
|
"currency": currency,
|
|
619
879
|
"sampling_price": {
|
|
620
|
-
|
|
880
|
+
# `value`, not `amount`: Price.to_domain reads `value` and nothing
|
|
881
|
+
# else, so an `amount` key used to land the quote at zero.
|
|
882
|
+
"value": sampling_price_value,
|
|
621
883
|
"currency": currency,
|
|
622
884
|
"valid_until": sampling_price_valid_until,
|
|
623
885
|
},
|
|
624
|
-
"production_price_tiers": production_price_tiers,
|
|
625
886
|
"sample_lead_time": sample_lead_time,
|
|
626
|
-
"lead_time": lead_time,
|
|
627
887
|
"deliverables": deliverables,
|
|
628
888
|
"customer_note": customer_note,
|
|
629
889
|
}
|
|
630
890
|
|
|
891
|
+
if production_price_tiers_json is not None:
|
|
892
|
+
body["production_price_tiers"] = _parse_json_list_option(
|
|
893
|
+
production_price_tiers_json, "--production-price-tiers-json"
|
|
894
|
+
)
|
|
895
|
+
if lead_time is not None:
|
|
896
|
+
body["lead_time"] = lead_time
|
|
897
|
+
if extra_sample_price_value is not None:
|
|
898
|
+
body["extra_sample_price"] = {
|
|
899
|
+
"value": extra_sample_price_value,
|
|
900
|
+
"currency": currency,
|
|
901
|
+
}
|
|
631
902
|
if link_id:
|
|
632
903
|
body["link_id"] = link_id
|
|
633
904
|
if link_type:
|
|
634
905
|
body["link_type"] = link_type
|
|
635
906
|
if moq is not None:
|
|
636
907
|
body["moq"] = moq
|
|
908
|
+
if moq_applies_to:
|
|
909
|
+
body["moq_applies_to"] = _validate_moq_applies_to(moq_applies_to)
|
|
637
910
|
if production_price_incoterm is not None:
|
|
638
911
|
body["production_price_incoterm"] = production_price_incoterm
|
|
639
912
|
if production_payment_term is not None:
|
|
640
913
|
body["production_payment_term"] = production_payment_term
|
|
914
|
+
if material_ship_to_json is not None:
|
|
915
|
+
body["material_ship_to_address"] = _parse_json_object_option(
|
|
916
|
+
material_ship_to_json, "--material-ship-to-json"
|
|
917
|
+
)
|
|
918
|
+
if available_sizes_json is not None:
|
|
919
|
+
body["available_sizes"] = _parse_json_list_option(
|
|
920
|
+
available_sizes_json, "--available-sizes-json"
|
|
921
|
+
)
|
|
922
|
+
if available_colors_json is not None:
|
|
923
|
+
body["available_colors"] = _parse_json_list_option(
|
|
924
|
+
available_colors_json, "--available-colors-json"
|
|
925
|
+
)
|
|
926
|
+
if size_chart_document_id is not None:
|
|
927
|
+
body["size_chart_document_id"] = size_chart_document_id or None
|
|
928
|
+
if component_specifications_json is not None:
|
|
929
|
+
body["component_specifications"] = _parse_json_list_option(
|
|
930
|
+
component_specifications_json, "--component-specifications-json"
|
|
931
|
+
)
|
|
932
|
+
if link_type_kind is not None:
|
|
933
|
+
body["type"] = link_type_kind
|
|
934
|
+
if attributes_json is not None:
|
|
935
|
+
body["attributes"] = _parse_json_object_option(attributes_json, "--attributes-json")
|
|
641
936
|
if document_ids_json is not None:
|
|
642
|
-
|
|
643
|
-
if not isinstance(parsed, list):
|
|
644
|
-
console.print("[red]--document-ids-json must be a JSON array (list).[/red]")
|
|
645
|
-
raise typer.Exit(1)
|
|
646
|
-
body["document_ids"] = parsed
|
|
937
|
+
body["document_ids"] = _parse_json_list_option(document_ids_json, "--document-ids-json")
|
|
647
938
|
if additional_deliverables_json is not None:
|
|
648
|
-
|
|
939
|
+
body["additional_deliverables"] = _parse_json_list_option(
|
|
649
940
|
additional_deliverables_json, "--additional-deliverables-json"
|
|
650
941
|
)
|
|
651
|
-
if not isinstance(parsed, list):
|
|
652
|
-
console.print(
|
|
653
|
-
"[red]--additional-deliverables-json must be a JSON array (list).[/red]"
|
|
654
|
-
)
|
|
655
|
-
raise typer.Exit(1)
|
|
656
|
-
body["additional_deliverables"] = parsed
|
|
657
942
|
if one_time_production_fees_json is not None:
|
|
658
|
-
|
|
943
|
+
body["one_time_production_fees"] = _parse_json_list_option(
|
|
659
944
|
one_time_production_fees_json, "--one-time-production-fees-json"
|
|
660
945
|
)
|
|
661
|
-
if not isinstance(parsed, list):
|
|
662
|
-
console.print(
|
|
663
|
-
"[red]--one-time-production-fees-json must be a JSON array (list).[/red]"
|
|
664
|
-
)
|
|
665
|
-
raise typer.Exit(1)
|
|
666
|
-
body["one_time_production_fees"] = parsed
|
|
667
946
|
if refundable_terms_json is not None:
|
|
668
947
|
body["refundable_terms"] = _parse_json_option(
|
|
669
948
|
refundable_terms_json, "--refundable-terms-json"
|
|
@@ -686,3 +965,283 @@ def register(app: typer.Typer) -> None:
|
|
|
686
965
|
body=body,
|
|
687
966
|
),
|
|
688
967
|
)
|
|
968
|
+
|
|
969
|
+
@app.command(
|
|
970
|
+
"update-supplier-quote",
|
|
971
|
+
help=(
|
|
972
|
+
"POST /admin/product/edit_supplier_link_override_info — admin updates an "
|
|
973
|
+
"existing quote (supplier link) in place. Only the options you pass are "
|
|
974
|
+
"sent, and the backend leaves every field you omit alone. "
|
|
975
|
+
"The override door does not read extra_sample_price, moq_applies_to, "
|
|
976
|
+
"material_ship_to_address or size_chart_document_id — those can only be "
|
|
977
|
+
"set at submit time."
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
def update_supplier_quote(
|
|
981
|
+
product_id: str = typer.Argument(
|
|
982
|
+
...,
|
|
983
|
+
help="product_id: the product the quote (supplier link) hangs off.",
|
|
984
|
+
),
|
|
985
|
+
link_id: str = typer.Argument(
|
|
986
|
+
...,
|
|
987
|
+
help="link_id: the supplier link to update.",
|
|
988
|
+
),
|
|
989
|
+
supplier_id: str | None = typer.Option(
|
|
990
|
+
None,
|
|
991
|
+
"--supplier-id",
|
|
992
|
+
help="supplier_id: reassign the link to another supplier (validated by the backend).",
|
|
993
|
+
),
|
|
994
|
+
link_name: str | None = typer.Option(
|
|
995
|
+
None,
|
|
996
|
+
"--link-name",
|
|
997
|
+
help="link_name: quote name.",
|
|
998
|
+
),
|
|
999
|
+
currency: str | None = typer.Option(
|
|
1000
|
+
None,
|
|
1001
|
+
"--currency",
|
|
1002
|
+
help="currency: quote currency (also the fallback currency for fees).",
|
|
1003
|
+
),
|
|
1004
|
+
sampling_price_value: float | None = typer.Option(
|
|
1005
|
+
None,
|
|
1006
|
+
"--sampling-price-value",
|
|
1007
|
+
help=(
|
|
1008
|
+
"sampling_price.value: new sampling price. The whole sampling_price "
|
|
1009
|
+
"object is replaced, so --sampling-price-valid-until is required with it."
|
|
1010
|
+
),
|
|
1011
|
+
),
|
|
1012
|
+
sampling_price_valid_until: str | None = typer.Option(
|
|
1013
|
+
None,
|
|
1014
|
+
"--sampling-price-valid-until",
|
|
1015
|
+
help="sampling_price.valid_until: expiry to store with the new sampling price.",
|
|
1016
|
+
),
|
|
1017
|
+
sampling_price_options_json: str | None = typer.Option(
|
|
1018
|
+
None,
|
|
1019
|
+
"--sampling-price-options-json",
|
|
1020
|
+
help="sampling_price_options: multi-option sampling prices (JSON array).",
|
|
1021
|
+
),
|
|
1022
|
+
production_price_tiers_json: str | None = typer.Option(
|
|
1023
|
+
None,
|
|
1024
|
+
"--production-price-tiers-json",
|
|
1025
|
+
help=(
|
|
1026
|
+
"production_price_tiers: the link's single tier schedule (JSON array; "
|
|
1027
|
+
"pass [] to clear it). "
|
|
1028
|
+
'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
|
|
1029
|
+
),
|
|
1030
|
+
),
|
|
1031
|
+
pricing_groups_json: str | None = typer.Option(
|
|
1032
|
+
None,
|
|
1033
|
+
"--pricing-groups-json",
|
|
1034
|
+
help=(
|
|
1035
|
+
"pricing_groups: several tier schedules on one quote (JSON array; "
|
|
1036
|
+
"pass [] to clear them). A group names the cart keys it prices "
|
|
1037
|
+
'("{size},{color_hex}") and carries its own tiers, MOQ and incoterm; '
|
|
1038
|
+
"tier_basis/moq_basis are VARIANT, GROUP (default) or ORDER, and "
|
|
1039
|
+
"max_qty 0 means an open-ended top tier. "
|
|
1040
|
+
'Example: \'[{"name":"Core","variants":["S,#000000"],'
|
|
1041
|
+
'"price_tiers":[{"min_qty":100,"max_qty":0,"unit_price":12.5}],'
|
|
1042
|
+
'"tier_basis":"VARIANT"}]\''
|
|
1043
|
+
),
|
|
1044
|
+
),
|
|
1045
|
+
moq: int | None = typer.Option(
|
|
1046
|
+
None,
|
|
1047
|
+
"--moq",
|
|
1048
|
+
help="moq: minimum order quantity.",
|
|
1049
|
+
),
|
|
1050
|
+
sample_lead_time: str | None = typer.Option(
|
|
1051
|
+
None,
|
|
1052
|
+
"--sample-lead-time",
|
|
1053
|
+
help="sample_lead_time: sampling lead time.",
|
|
1054
|
+
),
|
|
1055
|
+
lead_time: str | None = typer.Option(
|
|
1056
|
+
None,
|
|
1057
|
+
"--lead-time",
|
|
1058
|
+
help="lead_time: production lead time.",
|
|
1059
|
+
),
|
|
1060
|
+
production_price_incoterm: str | None = typer.Option(
|
|
1061
|
+
None,
|
|
1062
|
+
"--production-price-incoterm",
|
|
1063
|
+
help=(
|
|
1064
|
+
"production_price_incoterm: EXW or FOB; pass `null` to clear it. "
|
|
1065
|
+
"An unrecognised value is rejected by the backend."
|
|
1066
|
+
),
|
|
1067
|
+
),
|
|
1068
|
+
production_payment_term: str | None = typer.Option(
|
|
1069
|
+
None,
|
|
1070
|
+
"--production-payment-term",
|
|
1071
|
+
help='production_payment_term: deposit ratio, e.g. "40". Pass `null` to clear it.',
|
|
1072
|
+
),
|
|
1073
|
+
customer_note: str | None = typer.Option(
|
|
1074
|
+
None,
|
|
1075
|
+
"--customer-note",
|
|
1076
|
+
help="customer_note: note for customer/platform (empty string allowed).",
|
|
1077
|
+
),
|
|
1078
|
+
deliverables_json: str | None = typer.Option(
|
|
1079
|
+
None,
|
|
1080
|
+
"--deliverables-json",
|
|
1081
|
+
help="deliverables: deliverables array (JSON), e.g. '[\"size_set\"]'.",
|
|
1082
|
+
),
|
|
1083
|
+
additional_deliverables_json: str | None = typer.Option(
|
|
1084
|
+
None,
|
|
1085
|
+
"--additional-deliverables-json",
|
|
1086
|
+
help="additional_deliverables: extra deliverables array (JSON).",
|
|
1087
|
+
),
|
|
1088
|
+
one_time_production_fees_json: str | None = typer.Option(
|
|
1089
|
+
None,
|
|
1090
|
+
"--one-time-production-fees-json",
|
|
1091
|
+
help="one_time_production_fees: one-time production fees array (JSON).",
|
|
1092
|
+
),
|
|
1093
|
+
refundable_terms_json: str | None = typer.Option(
|
|
1094
|
+
None,
|
|
1095
|
+
"--refundable-terms-json",
|
|
1096
|
+
help="refundable_terms: refundable terms (JSON object; pass `null` to clear).",
|
|
1097
|
+
),
|
|
1098
|
+
available_sizes_json: str | None = typer.Option(
|
|
1099
|
+
None,
|
|
1100
|
+
"--available-sizes-json",
|
|
1101
|
+
help=(
|
|
1102
|
+
"available_sizes: sizes the quote is priced against (JSON array of strings). "
|
|
1103
|
+
"Pricing groups claim cart keys built from these."
|
|
1104
|
+
),
|
|
1105
|
+
),
|
|
1106
|
+
available_colors_json: str | None = typer.Option(
|
|
1107
|
+
None,
|
|
1108
|
+
"--available-colors-json",
|
|
1109
|
+
help=(
|
|
1110
|
+
"available_colors: colours the quote is priced against (JSON array of "
|
|
1111
|
+
"{color_name, hex_code, pantone_code?})."
|
|
1112
|
+
),
|
|
1113
|
+
),
|
|
1114
|
+
component_specifications_json: str | None = typer.Option(
|
|
1115
|
+
None,
|
|
1116
|
+
"--component-specifications-json",
|
|
1117
|
+
help="component_specifications: free-form specification rows (JSON array).",
|
|
1118
|
+
),
|
|
1119
|
+
link_type_kind: str | None = typer.Option(
|
|
1120
|
+
None,
|
|
1121
|
+
"--type",
|
|
1122
|
+
help="type: typed-link kind (MANUFACTURE, PRODUCT_DEVELOP, FABRIC, BUTTON, ...).",
|
|
1123
|
+
),
|
|
1124
|
+
attributes_json: str | None = typer.Option(
|
|
1125
|
+
None,
|
|
1126
|
+
"--attributes-json",
|
|
1127
|
+
help="attributes: LinkAttributes-shaped SKU metadata (JSON object).",
|
|
1128
|
+
),
|
|
1129
|
+
document_ids_json: str | None = typer.Option(
|
|
1130
|
+
None,
|
|
1131
|
+
"--document-ids-json",
|
|
1132
|
+
help="document_ids: related document ID array (JSON), e.g. '[\"doc_1\"]'.",
|
|
1133
|
+
),
|
|
1134
|
+
) -> None:
|
|
1135
|
+
"""
|
|
1136
|
+
Update an existing quote (supplier link) as admin.
|
|
1137
|
+
"""
|
|
1138
|
+
override: dict[str, object] = {}
|
|
1139
|
+
|
|
1140
|
+
if (sampling_price_value is None) != (sampling_price_valid_until is None):
|
|
1141
|
+
console.print(
|
|
1142
|
+
"[red]--sampling-price-value and --sampling-price-valid-until must be "
|
|
1143
|
+
"passed together: the backend replaces the whole sampling_price object, "
|
|
1144
|
+
"so sending one without the other drops the other.[/red]"
|
|
1145
|
+
)
|
|
1146
|
+
raise typer.Exit(1)
|
|
1147
|
+
|
|
1148
|
+
if supplier_id is not None:
|
|
1149
|
+
override["supplier_id"] = supplier_id
|
|
1150
|
+
if link_name is not None:
|
|
1151
|
+
override["link_name"] = link_name
|
|
1152
|
+
if currency is not None:
|
|
1153
|
+
override["currency"] = currency
|
|
1154
|
+
if sampling_price_value is not None:
|
|
1155
|
+
override["sampling_price"] = {
|
|
1156
|
+
"value": sampling_price_value,
|
|
1157
|
+
"valid_until": sampling_price_valid_until,
|
|
1158
|
+
**({"currency": currency} if currency else {}),
|
|
1159
|
+
}
|
|
1160
|
+
if sampling_price_options_json is not None:
|
|
1161
|
+
override["sampling_price_options"] = _parse_json_list_option(
|
|
1162
|
+
sampling_price_options_json, "--sampling-price-options-json"
|
|
1163
|
+
)
|
|
1164
|
+
if production_price_tiers_json is not None:
|
|
1165
|
+
override["production_price_tiers"] = _parse_json_list_option(
|
|
1166
|
+
production_price_tiers_json, "--production-price-tiers-json"
|
|
1167
|
+
)
|
|
1168
|
+
if pricing_groups_json is not None:
|
|
1169
|
+
override["pricing_groups"] = _parse_json_list_option(
|
|
1170
|
+
pricing_groups_json, "--pricing-groups-json"
|
|
1171
|
+
)
|
|
1172
|
+
if moq is not None:
|
|
1173
|
+
override["moq"] = moq
|
|
1174
|
+
if sample_lead_time is not None:
|
|
1175
|
+
override["sample_lead_time"] = sample_lead_time
|
|
1176
|
+
if lead_time is not None:
|
|
1177
|
+
override["lead_time"] = lead_time
|
|
1178
|
+
if production_price_incoterm is not None:
|
|
1179
|
+
override["production_price_incoterm"] = _none_if_null_literal(production_price_incoterm)
|
|
1180
|
+
if production_payment_term is not None:
|
|
1181
|
+
override["production_payment_term"] = _none_if_null_literal(production_payment_term)
|
|
1182
|
+
if customer_note is not None:
|
|
1183
|
+
override["customer_note"] = customer_note
|
|
1184
|
+
if deliverables_json is not None:
|
|
1185
|
+
override["deliverables"] = _parse_json_list_option(
|
|
1186
|
+
deliverables_json, "--deliverables-json"
|
|
1187
|
+
)
|
|
1188
|
+
if additional_deliverables_json is not None:
|
|
1189
|
+
override["additional_deliverables"] = _parse_json_list_option(
|
|
1190
|
+
additional_deliverables_json, "--additional-deliverables-json"
|
|
1191
|
+
)
|
|
1192
|
+
if one_time_production_fees_json is not None:
|
|
1193
|
+
override["one_time_production_fees"] = _parse_json_list_option(
|
|
1194
|
+
one_time_production_fees_json, "--one-time-production-fees-json"
|
|
1195
|
+
)
|
|
1196
|
+
if refundable_terms_json is not None:
|
|
1197
|
+
override["refundable_terms"] = _parse_json_option(
|
|
1198
|
+
refundable_terms_json, "--refundable-terms-json"
|
|
1199
|
+
)
|
|
1200
|
+
if available_sizes_json is not None:
|
|
1201
|
+
override["available_sizes"] = _parse_json_list_option(
|
|
1202
|
+
available_sizes_json, "--available-sizes-json"
|
|
1203
|
+
)
|
|
1204
|
+
if available_colors_json is not None:
|
|
1205
|
+
override["available_colors"] = _parse_json_list_option(
|
|
1206
|
+
available_colors_json, "--available-colors-json"
|
|
1207
|
+
)
|
|
1208
|
+
if component_specifications_json is not None:
|
|
1209
|
+
override["component_specifications"] = _parse_json_list_option(
|
|
1210
|
+
component_specifications_json, "--component-specifications-json"
|
|
1211
|
+
)
|
|
1212
|
+
if link_type_kind is not None:
|
|
1213
|
+
override["type"] = link_type_kind
|
|
1214
|
+
if attributes_json is not None:
|
|
1215
|
+
override["attributes"] = _parse_json_object_option(attributes_json, "--attributes-json")
|
|
1216
|
+
if document_ids_json is not None:
|
|
1217
|
+
override["document_ids"] = _parse_json_list_option(
|
|
1218
|
+
document_ids_json, "--document-ids-json"
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
if not override:
|
|
1222
|
+
console.print(
|
|
1223
|
+
"[red]Nothing to update: pass at least one field option "
|
|
1224
|
+
"(the backend rejects an empty override_info).[/red]"
|
|
1225
|
+
)
|
|
1226
|
+
raise typer.Exit(1)
|
|
1227
|
+
|
|
1228
|
+
# Pricing groups claim cart keys built from the link's own variant lists.
|
|
1229
|
+
# Groups without them price variants no order can contain, so say so
|
|
1230
|
+
# rather than let the quote look fine and misprice later.
|
|
1231
|
+
if override.get("pricing_groups") and not (
|
|
1232
|
+
"available_sizes" in override or "available_colors" in override
|
|
1233
|
+
):
|
|
1234
|
+
console.print(
|
|
1235
|
+
"[yellow]Warning: --pricing-groups-json without --available-sizes-json / "
|
|
1236
|
+
"--available-colors-json relies on the variant lists already stored on the "
|
|
1237
|
+
"link. Check them with `get-supplier-links-by-supplier-id`.[/yellow]"
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
print_json_or_exit(
|
|
1241
|
+
console,
|
|
1242
|
+
lambda: api_client.edit_supplier_link_override_info(
|
|
1243
|
+
product_id=product_id,
|
|
1244
|
+
link_id=link_id,
|
|
1245
|
+
override_info=override,
|
|
1246
|
+
),
|
|
1247
|
+
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.21"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|