makea-cli 0.1.21__tar.gz → 0.1.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {makea_cli-0.1.21 → makea_cli-0.1.22}/PKG-INFO +3 -2
  2. {makea_cli-0.1.21 → makea_cli-0.1.22}/README.md +2 -1
  3. makea_cli-0.1.22/makea_cli/__init__.py +1 -0
  4. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/api_client.py +21 -0
  5. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/supplier/cmd.py +464 -57
  6. {makea_cli-0.1.21 → makea_cli-0.1.22}/pyproject.toml +1 -1
  7. makea_cli-0.1.21/makea_cli/__init__.py +0 -1
  8. {makea_cli-0.1.21 → makea_cli-0.1.22}/.claude/skills/create-supplier-profile/SKILL.md +0 -0
  9. {makea_cli-0.1.21 → makea_cli-0.1.22}/.claude/skills/lark-cli-setup/SKILL.md +0 -0
  10. {makea_cli-0.1.21 → makea_cli-0.1.22}/.claude/skills/link-supplier-user/SKILL.md +0 -0
  11. {makea_cli-0.1.21 → makea_cli-0.1.22}/.claude/skills/makea-cli-setup/SKILL.md +0 -0
  12. {makea_cli-0.1.21 → makea_cli-0.1.22}/.claude/skills/payment-revenue-report/SKILL.md +0 -0
  13. {makea_cli-0.1.21 → makea_cli-0.1.22}/.claude/skills/supplier-user-id-migration/SKILL.md +0 -0
  14. {makea_cli-0.1.21 → makea_cli-0.1.22}/.gitignore +0 -0
  15. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea-collection-upload/SKILL.md +0 -0
  16. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea-collection-upload/scripts/create_products.py +0 -0
  17. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/__main__.py +0 -0
  18. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/auth_pkce.py +0 -0
  19. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/__init__.py +0 -0
  20. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/_util.py +0 -0
  21. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/analytics/__init__.py +0 -0
  22. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/analytics/cmd.py +0 -0
  23. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/auth/__init__.py +0 -0
  24. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/auth/cmd.py +0 -0
  25. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/designer/__init__.py +0 -0
  26. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/designer/cmd.py +0 -0
  27. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/directory/__init__.py +0 -0
  28. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/directory/cmd.py +0 -0
  29. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/document/__init__.py +0 -0
  30. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/document/cmd.py +0 -0
  31. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/email/__init__.py +0 -0
  32. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/email/cmd.py +0 -0
  33. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/library/__init__.py +0 -0
  34. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/library/cmd.py +0 -0
  35. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/metrics/__init__.py +0 -0
  36. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/metrics/cmd.py +0 -0
  37. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/orders/__init__.py +0 -0
  38. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/orders/cmd.py +0 -0
  39. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/product/__init__.py +0 -0
  40. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/product/cmd.py +0 -0
  41. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/production/__init__.py +0 -0
  42. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/production/cmd.py +0 -0
  43. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/production/pricing_spec.py +0 -0
  44. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/commands/supplier/__init__.py +0 -0
  45. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/config.py +0 -0
  46. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/main.py +0 -0
  47. {makea_cli-0.1.21 → makea_cli-0.1.22}/makea_cli/posthog_client.py +0 -0
  48. {makea_cli-0.1.21 → makea_cli-0.1.22}/npm/makea-cli/README.md +0 -0
  49. {makea_cli-0.1.21 → makea_cli-0.1.22}/npm/makea-cli/makea-cli.mjs +0 -0
  50. {makea_cli-0.1.21 → makea_cli-0.1.22}/npm/makea-cli/package.json +0 -0
  51. {makea_cli-0.1.21 → makea_cli-0.1.22}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: makea-cli
3
- Version: 0.1.21
3
+ Version: 0.1.22
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
@@ -82,7 +82,8 @@ makea-cli auth --logout
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
83
  - `makea-cli get-quote-requests-by-supplier-id <supplier_id> [--status <status>] [-n <limit>]` — 直连后端按 supplier_id 查询 quote requests
84
84
  - `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-amount ... --sampling-price-valid-until ... --production-price-tiers-json ... --sample-lead-time ... --lead-time ... --deliverables-json ... --customer-note ...` — 以供应商身份向指定 quote request 新增 quote(完整参数说明见 `--help`)
85
+ - `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` 补
86
+ - `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
87
  - `makea-cli list-product-orders` — 列出所有设计师产品订单(GET `/admin/designer/get_all_product_orders`)
87
88
  - `makea-cli list-sampling-orders-by-product <id>` — 按产品 reference id 列打样单
88
89
  - `makea-cli list-production-orders-by-product <id>` — 按产品 reference id 列大货单
@@ -71,7 +71,8 @@ makea-cli auth --logout
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
72
  - `makea-cli get-quote-requests-by-supplier-id <supplier_id> [--status <status>] [-n <limit>]` — 直连后端按 supplier_id 查询 quote requests
73
73
  - `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-amount ... --sampling-price-valid-until ... --production-price-tiers-json ... --sample-lead-time ... --lead-time ... --deliverables-json ... --customer-note ...` — 以供应商身份向指定 quote request 新增 quote(完整参数说明见 `--help`)
74
+ - `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` 补
75
+ - `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
76
  - `makea-cli list-product-orders` — 列出所有设计师产品订单(GET `/admin/designer/get_all_product_orders`)
76
77
  - `makea-cli list-sampling-orders-by-product <id>` — 按产品 reference id 列打样单
77
78
  - `makea-cli list-production-orders-by-product <id>` — 按产品 reference id 列大货单
@@ -0,0 +1 @@
1
+ __version__ = "0.1.22"
@@ -390,6 +390,27 @@ def submit_admin_quote_for_quote_request(
390
390
  return post_json(f"/admin/quote_requests/{quote_request_id}/submit_quote", body)
391
391
 
392
392
 
393
+ def edit_supplier_link_override_info(
394
+ *,
395
+ product_id: str,
396
+ link_id: str,
397
+ override_info: dict[str, Any],
398
+ ) -> Any:
399
+ """
400
+ POST /admin/product/edit_supplier_link_override_info — admin updates an existing
401
+ supplier link (a submitted quote) in place. Only the keys present in
402
+ ``override_info`` are applied; the rest of the link is left alone.
403
+ """
404
+ return post_json(
405
+ "/admin/product/edit_supplier_link_override_info",
406
+ {
407
+ "product_id": product_id,
408
+ "link_id": link_id,
409
+ "override_info": override_info,
410
+ },
411
+ )
412
+
413
+
393
414
  def upload_admin_link_document(
394
415
  *,
395
416
  file_path: Path,
@@ -20,6 +20,52 @@ 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
+
26
+ def _parse_json_option(raw: str, option_name: str) -> object:
27
+ """Parse a JSON-valued option, failing with the option's own name."""
28
+ try:
29
+ return json.loads(raw)
30
+ except json.JSONDecodeError as e:
31
+ console.print(f"[red]{option_name} must be valid JSON: {e}[/red]")
32
+ raise typer.Exit(1) from e
33
+
34
+
35
+ def _parse_json_list_option(raw: str, option_name: str) -> list:
36
+ parsed = _parse_json_option(raw, option_name)
37
+ if not isinstance(parsed, list):
38
+ console.print(f"[red]{option_name} must be a JSON array (list).[/red]")
39
+ raise typer.Exit(1)
40
+ return parsed
41
+
42
+
43
+ def _parse_json_object_option(raw: str, option_name: str) -> dict:
44
+ parsed = _parse_json_option(raw, option_name)
45
+ if not isinstance(parsed, dict):
46
+ console.print(f"[red]{option_name} must be a JSON object.[/red]")
47
+ raise typer.Exit(1)
48
+ return parsed
49
+
50
+
51
+ def _validate_moq_applies_to(values: list[str]) -> list[str]:
52
+ """Normalize --moq-applies-to; the backend silently drops unknown values."""
53
+ normalized = [str(v).strip().lower() for v in values if str(v).strip()]
54
+ unknown = [v for v in normalized if v not in MOQ_APPLIES_TO_VALUES]
55
+ if unknown:
56
+ console.print(
57
+ f"[red]--moq-applies-to accepts only {', '.join(MOQ_APPLIES_TO_VALUES)} "
58
+ f"(got: {', '.join(unknown)}).[/red]"
59
+ )
60
+ raise typer.Exit(1)
61
+ return [v for v in MOQ_APPLIES_TO_VALUES if v in set(normalized)]
62
+
63
+
64
+ def _none_if_null_literal(value: str) -> str | None:
65
+ """Let an option clear a stored value by passing the literal `null`."""
66
+ return None if value.strip().lower() in ("null", "none") else value
67
+
68
+
23
69
  def register(app: typer.Typer) -> None:
24
70
  @app.command(
25
71
  "list-suppliers",
@@ -472,7 +518,9 @@ def register(app: typer.Typer) -> None:
472
518
  "submit-supplier-quote",
473
519
  help=(
474
520
  "POST /admin/quote_requests/{quote_request_id}/submit_quote — "
475
- "admin submits a new quote (link) to a quote request."
521
+ "admin submits a new quote (link) to a quote request. "
522
+ "Pricing groups cannot be sent here (the submit endpoint ignores them) — "
523
+ "set them afterwards with `update-supplier-quote`."
476
524
  ),
477
525
  )
478
526
  def submit_supplier_quote(
@@ -490,10 +538,14 @@ def register(app: typer.Typer) -> None:
490
538
  "--currency",
491
539
  help="currency: quote currency (used by sampling price and fees).",
492
540
  ),
493
- sampling_price_amount: float = typer.Option(
541
+ sampling_price_value: float = typer.Option(
494
542
  ...,
543
+ "--sampling-price-value",
495
544
  "--sampling-price-amount",
496
- help="sampling_price.amount: sampling price amount (required, number).",
545
+ help=(
546
+ "sampling_price.value: sampling price (required, number). "
547
+ "Sent as `value` — the backend reads no other key."
548
+ ),
497
549
  ),
498
550
  sampling_price_valid_until: str = typer.Option(
499
551
  ...,
@@ -503,25 +555,11 @@ def register(app: typer.Typer) -> None:
503
555
  "(required, ISO datetime string, e.g. 2026-05-01T00:00:00+00:00)."
504
556
  ),
505
557
  ),
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
558
  sample_lead_time: str = typer.Option(
516
559
  ...,
517
560
  "--sample-lead-time",
518
561
  help="sample_lead_time: sampling lead time (required, string).",
519
562
  ),
520
- lead_time: str = typer.Option(
521
- ...,
522
- "--lead-time",
523
- help="lead_time: production lead time (required, string).",
524
- ),
525
563
  deliverables_json: str = typer.Option(
526
564
  ...,
527
565
  "--deliverables-json",
@@ -535,6 +573,28 @@ def register(app: typer.Typer) -> None:
535
573
  "--customer-note",
536
574
  help="customer_note: note for customer/platform (required, string; empty string allowed).",
537
575
  ),
576
+ production_price_tiers_json: str | None = typer.Option(
577
+ None,
578
+ "--production-price-tiers-json",
579
+ help=(
580
+ "production_price_tiers: tiered production price array "
581
+ "(optional since TECH-93 — a factory may quote the sample only). "
582
+ 'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
583
+ ),
584
+ ),
585
+ lead_time: str | None = typer.Option(
586
+ None,
587
+ "--lead-time",
588
+ help="lead_time: production lead time (optional since TECH-93, string).",
589
+ ),
590
+ extra_sample_price_value: float | None = typer.Option(
591
+ None,
592
+ "--extra-sample-price-value",
593
+ help=(
594
+ "extra_sample_price.value: per-unit charge for extra samples "
595
+ "(optional, number; currency defaults to --currency)."
596
+ ),
597
+ ),
538
598
  link_id: str | None = typer.Option(
539
599
  None,
540
600
  "--link-id",
@@ -550,6 +610,14 @@ def register(app: typer.Typer) -> None:
550
610
  "--moq",
551
611
  help="moq: minimum order quantity (optional, integer).",
552
612
  ),
613
+ moq_applies_to: list[str] | None = typer.Option(
614
+ None,
615
+ "--moq-applies-to",
616
+ help=(
617
+ "moq_applies_to: what the MOQ counts (optional, repeatable): "
618
+ "style, size, color. Empty means the supplier did not say."
619
+ ),
620
+ ),
553
621
  production_price_incoterm: str | None = typer.Option(
554
622
  None,
555
623
  "--production-price-incoterm",
@@ -560,6 +628,63 @@ def register(app: typer.Typer) -> None:
560
628
  "--production-payment-term",
561
629
  help='production_payment_term: production payment term/deposit ratio (optional, e.g. "40" or "40%").',
562
630
  ),
631
+ material_ship_to_json: str | None = typer.Option(
632
+ None,
633
+ "--material-ship-to-json",
634
+ help=(
635
+ "material_ship_to_address: where the buyer ships materials "
636
+ "(JSON object with street, city, state, postal_code, country). "
637
+ "REQUIRED by the backend for a CMT or full-package SAMPLING quote — "
638
+ "street, city and country must be non-empty. "
639
+ 'Example: \'{"street":"1 Test Rd","city":"Guangzhou","country":"CN"}\''
640
+ ),
641
+ ),
642
+ available_sizes_json: str | None = typer.Option(
643
+ None,
644
+ "--available-sizes-json",
645
+ help=(
646
+ "available_sizes: sizes the piece comes in (optional, JSON array of strings). "
647
+ 'Example: \'["S","M","L"]\''
648
+ ),
649
+ ),
650
+ available_colors_json: str | None = typer.Option(
651
+ None,
652
+ "--available-colors-json",
653
+ help=(
654
+ "available_colors: colours the piece comes in (optional, JSON array of objects). "
655
+ 'Example: \'[{"color_name":"Black","hex_code":"#000000","pantone_code":"Black 6 C"}]\''
656
+ ),
657
+ ),
658
+ size_chart_document_id: str | None = typer.Option(
659
+ None,
660
+ "--size-chart-document-id",
661
+ help=(
662
+ "size_chart_document_id: the quote's size chart document (optional). "
663
+ 'Pass an empty string ("") to send it as null and leave the link without one.'
664
+ ),
665
+ ),
666
+ component_specifications_json: str | None = typer.Option(
667
+ None,
668
+ "--component-specifications-json",
669
+ help=(
670
+ "component_specifications: free-form specification rows (optional, JSON array). "
671
+ 'Example: \'[{"component":"Fabric","specification":"100% cotton, 220gsm"}]\''
672
+ ),
673
+ ),
674
+ link_type_kind: str | None = typer.Option(
675
+ None,
676
+ "--type",
677
+ help=(
678
+ "type: typed-link kind (optional; default MANUFACTURE). "
679
+ "MANUFACTURE, PRODUCT_DEVELOP, or a material kind (FABRIC, BUTTON, ZIPPER, "
680
+ "ELASTIC, LABEL, HOOK, SNAP, CORD, BUCKLE, TAPE, PATCH, LACE, EYELET)."
681
+ ),
682
+ ),
683
+ attributes_json: str | None = typer.Option(
684
+ None,
685
+ "--attributes-json",
686
+ help="attributes: LinkAttributes-shaped SKU metadata (optional, JSON object).",
687
+ ),
563
688
  document_ids_json: str | None = typer.Option(
564
689
  None,
565
690
  "--document-ids-json",
@@ -594,76 +719,78 @@ def register(app: typer.Typer) -> None:
594
719
  """
595
720
  Submit a new quote link to a quote request as admin.
596
721
  """
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)
722
+ deliverables = _parse_json_list_option(deliverables_json, "--deliverables-json")
615
723
 
616
724
  body: dict[str, object] = {
617
725
  "link_name": link_name,
618
726
  "currency": currency,
619
727
  "sampling_price": {
620
- "amount": sampling_price_amount,
728
+ # `value`, not `amount`: Price.to_domain reads `value` and nothing
729
+ # else, so an `amount` key used to land the quote at zero.
730
+ "value": sampling_price_value,
621
731
  "currency": currency,
622
732
  "valid_until": sampling_price_valid_until,
623
733
  },
624
- "production_price_tiers": production_price_tiers,
625
734
  "sample_lead_time": sample_lead_time,
626
- "lead_time": lead_time,
627
735
  "deliverables": deliverables,
628
736
  "customer_note": customer_note,
629
737
  }
630
738
 
739
+ if production_price_tiers_json is not None:
740
+ body["production_price_tiers"] = _parse_json_list_option(
741
+ production_price_tiers_json, "--production-price-tiers-json"
742
+ )
743
+ if lead_time is not None:
744
+ body["lead_time"] = lead_time
745
+ if extra_sample_price_value is not None:
746
+ body["extra_sample_price"] = {
747
+ "value": extra_sample_price_value,
748
+ "currency": currency,
749
+ }
631
750
  if link_id:
632
751
  body["link_id"] = link_id
633
752
  if link_type:
634
753
  body["link_type"] = link_type
635
754
  if moq is not None:
636
755
  body["moq"] = moq
756
+ if moq_applies_to:
757
+ body["moq_applies_to"] = _validate_moq_applies_to(moq_applies_to)
637
758
  if production_price_incoterm is not None:
638
759
  body["production_price_incoterm"] = production_price_incoterm
639
760
  if production_payment_term is not None:
640
761
  body["production_payment_term"] = production_payment_term
762
+ if material_ship_to_json is not None:
763
+ body["material_ship_to_address"] = _parse_json_object_option(
764
+ material_ship_to_json, "--material-ship-to-json"
765
+ )
766
+ if available_sizes_json is not None:
767
+ body["available_sizes"] = _parse_json_list_option(
768
+ available_sizes_json, "--available-sizes-json"
769
+ )
770
+ if available_colors_json is not None:
771
+ body["available_colors"] = _parse_json_list_option(
772
+ available_colors_json, "--available-colors-json"
773
+ )
774
+ if size_chart_document_id is not None:
775
+ body["size_chart_document_id"] = size_chart_document_id or None
776
+ if component_specifications_json is not None:
777
+ body["component_specifications"] = _parse_json_list_option(
778
+ component_specifications_json, "--component-specifications-json"
779
+ )
780
+ if link_type_kind is not None:
781
+ body["type"] = link_type_kind
782
+ if attributes_json is not None:
783
+ body["attributes"] = _parse_json_object_option(attributes_json, "--attributes-json")
641
784
  if document_ids_json is not None:
642
- parsed = _parse_json_option(document_ids_json, "--document-ids-json")
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
785
+ body["document_ids"] = _parse_json_list_option(document_ids_json, "--document-ids-json")
647
786
  if additional_deliverables_json is not None:
648
- parsed = _parse_json_option(
787
+ body["additional_deliverables"] = _parse_json_list_option(
649
788
  additional_deliverables_json, "--additional-deliverables-json"
650
789
  )
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
790
  if one_time_production_fees_json is not None:
658
- parsed = _parse_json_option(
791
+ body["one_time_production_fees"] = _parse_json_list_option(
659
792
  one_time_production_fees_json, "--one-time-production-fees-json"
660
793
  )
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
794
  if refundable_terms_json is not None:
668
795
  body["refundable_terms"] = _parse_json_option(
669
796
  refundable_terms_json, "--refundable-terms-json"
@@ -686,3 +813,283 @@ def register(app: typer.Typer) -> None:
686
813
  body=body,
687
814
  ),
688
815
  )
816
+
817
+ @app.command(
818
+ "update-supplier-quote",
819
+ help=(
820
+ "POST /admin/product/edit_supplier_link_override_info — admin updates an "
821
+ "existing quote (supplier link) in place. Only the options you pass are "
822
+ "sent, and the backend leaves every field you omit alone. "
823
+ "The override door does not read extra_sample_price, moq_applies_to, "
824
+ "material_ship_to_address or size_chart_document_id — those can only be "
825
+ "set at submit time."
826
+ ),
827
+ )
828
+ def update_supplier_quote(
829
+ product_id: str = typer.Argument(
830
+ ...,
831
+ help="product_id: the product the quote (supplier link) hangs off.",
832
+ ),
833
+ link_id: str = typer.Argument(
834
+ ...,
835
+ help="link_id: the supplier link to update.",
836
+ ),
837
+ supplier_id: str | None = typer.Option(
838
+ None,
839
+ "--supplier-id",
840
+ help="supplier_id: reassign the link to another supplier (validated by the backend).",
841
+ ),
842
+ link_name: str | None = typer.Option(
843
+ None,
844
+ "--link-name",
845
+ help="link_name: quote name.",
846
+ ),
847
+ currency: str | None = typer.Option(
848
+ None,
849
+ "--currency",
850
+ help="currency: quote currency (also the fallback currency for fees).",
851
+ ),
852
+ sampling_price_value: float | None = typer.Option(
853
+ None,
854
+ "--sampling-price-value",
855
+ help=(
856
+ "sampling_price.value: new sampling price. The whole sampling_price "
857
+ "object is replaced, so --sampling-price-valid-until is required with it."
858
+ ),
859
+ ),
860
+ sampling_price_valid_until: str | None = typer.Option(
861
+ None,
862
+ "--sampling-price-valid-until",
863
+ help="sampling_price.valid_until: expiry to store with the new sampling price.",
864
+ ),
865
+ sampling_price_options_json: str | None = typer.Option(
866
+ None,
867
+ "--sampling-price-options-json",
868
+ help="sampling_price_options: multi-option sampling prices (JSON array).",
869
+ ),
870
+ production_price_tiers_json: str | None = typer.Option(
871
+ None,
872
+ "--production-price-tiers-json",
873
+ help=(
874
+ "production_price_tiers: the link's single tier schedule (JSON array; "
875
+ "pass [] to clear it). "
876
+ 'Example: \'[{"min_qty":100,"max_qty":499,"unit_price":12.5}]\''
877
+ ),
878
+ ),
879
+ pricing_groups_json: str | None = typer.Option(
880
+ None,
881
+ "--pricing-groups-json",
882
+ help=(
883
+ "pricing_groups: several tier schedules on one quote (JSON array; "
884
+ "pass [] to clear them). A group names the cart keys it prices "
885
+ '("{size},{color_hex}") and carries its own tiers, MOQ and incoterm; '
886
+ "tier_basis/moq_basis are VARIANT, GROUP (default) or ORDER, and "
887
+ "max_qty 0 means an open-ended top tier. "
888
+ 'Example: \'[{"name":"Core","variants":["S,#000000"],'
889
+ '"price_tiers":[{"min_qty":100,"max_qty":0,"unit_price":12.5}],'
890
+ '"tier_basis":"VARIANT"}]\''
891
+ ),
892
+ ),
893
+ moq: int | None = typer.Option(
894
+ None,
895
+ "--moq",
896
+ help="moq: minimum order quantity.",
897
+ ),
898
+ sample_lead_time: str | None = typer.Option(
899
+ None,
900
+ "--sample-lead-time",
901
+ help="sample_lead_time: sampling lead time.",
902
+ ),
903
+ lead_time: str | None = typer.Option(
904
+ None,
905
+ "--lead-time",
906
+ help="lead_time: production lead time.",
907
+ ),
908
+ production_price_incoterm: str | None = typer.Option(
909
+ None,
910
+ "--production-price-incoterm",
911
+ help=(
912
+ "production_price_incoterm: EXW or FOB; pass `null` to clear it. "
913
+ "An unrecognised value is rejected by the backend."
914
+ ),
915
+ ),
916
+ production_payment_term: str | None = typer.Option(
917
+ None,
918
+ "--production-payment-term",
919
+ help='production_payment_term: deposit ratio, e.g. "40". Pass `null` to clear it.',
920
+ ),
921
+ customer_note: str | None = typer.Option(
922
+ None,
923
+ "--customer-note",
924
+ help="customer_note: note for customer/platform (empty string allowed).",
925
+ ),
926
+ deliverables_json: str | None = typer.Option(
927
+ None,
928
+ "--deliverables-json",
929
+ help="deliverables: deliverables array (JSON), e.g. '[\"size_set\"]'.",
930
+ ),
931
+ additional_deliverables_json: str | None = typer.Option(
932
+ None,
933
+ "--additional-deliverables-json",
934
+ help="additional_deliverables: extra deliverables array (JSON).",
935
+ ),
936
+ one_time_production_fees_json: str | None = typer.Option(
937
+ None,
938
+ "--one-time-production-fees-json",
939
+ help="one_time_production_fees: one-time production fees array (JSON).",
940
+ ),
941
+ refundable_terms_json: str | None = typer.Option(
942
+ None,
943
+ "--refundable-terms-json",
944
+ help="refundable_terms: refundable terms (JSON object; pass `null` to clear).",
945
+ ),
946
+ available_sizes_json: str | None = typer.Option(
947
+ None,
948
+ "--available-sizes-json",
949
+ help=(
950
+ "available_sizes: sizes the quote is priced against (JSON array of strings). "
951
+ "Pricing groups claim cart keys built from these."
952
+ ),
953
+ ),
954
+ available_colors_json: str | None = typer.Option(
955
+ None,
956
+ "--available-colors-json",
957
+ help=(
958
+ "available_colors: colours the quote is priced against (JSON array of "
959
+ "{color_name, hex_code, pantone_code?})."
960
+ ),
961
+ ),
962
+ component_specifications_json: str | None = typer.Option(
963
+ None,
964
+ "--component-specifications-json",
965
+ help="component_specifications: free-form specification rows (JSON array).",
966
+ ),
967
+ link_type_kind: str | None = typer.Option(
968
+ None,
969
+ "--type",
970
+ help="type: typed-link kind (MANUFACTURE, PRODUCT_DEVELOP, FABRIC, BUTTON, ...).",
971
+ ),
972
+ attributes_json: str | None = typer.Option(
973
+ None,
974
+ "--attributes-json",
975
+ help="attributes: LinkAttributes-shaped SKU metadata (JSON object).",
976
+ ),
977
+ document_ids_json: str | None = typer.Option(
978
+ None,
979
+ "--document-ids-json",
980
+ help="document_ids: related document ID array (JSON), e.g. '[\"doc_1\"]'.",
981
+ ),
982
+ ) -> None:
983
+ """
984
+ Update an existing quote (supplier link) as admin.
985
+ """
986
+ override: dict[str, object] = {}
987
+
988
+ if (sampling_price_value is None) != (sampling_price_valid_until is None):
989
+ console.print(
990
+ "[red]--sampling-price-value and --sampling-price-valid-until must be "
991
+ "passed together: the backend replaces the whole sampling_price object, "
992
+ "so sending one without the other drops the other.[/red]"
993
+ )
994
+ raise typer.Exit(1)
995
+
996
+ if supplier_id is not None:
997
+ override["supplier_id"] = supplier_id
998
+ if link_name is not None:
999
+ override["link_name"] = link_name
1000
+ if currency is not None:
1001
+ override["currency"] = currency
1002
+ if sampling_price_value is not None:
1003
+ override["sampling_price"] = {
1004
+ "value": sampling_price_value,
1005
+ "valid_until": sampling_price_valid_until,
1006
+ **({"currency": currency} if currency else {}),
1007
+ }
1008
+ if sampling_price_options_json is not None:
1009
+ override["sampling_price_options"] = _parse_json_list_option(
1010
+ sampling_price_options_json, "--sampling-price-options-json"
1011
+ )
1012
+ if production_price_tiers_json is not None:
1013
+ override["production_price_tiers"] = _parse_json_list_option(
1014
+ production_price_tiers_json, "--production-price-tiers-json"
1015
+ )
1016
+ if pricing_groups_json is not None:
1017
+ override["pricing_groups"] = _parse_json_list_option(
1018
+ pricing_groups_json, "--pricing-groups-json"
1019
+ )
1020
+ if moq is not None:
1021
+ override["moq"] = moq
1022
+ if sample_lead_time is not None:
1023
+ override["sample_lead_time"] = sample_lead_time
1024
+ if lead_time is not None:
1025
+ override["lead_time"] = lead_time
1026
+ if production_price_incoterm is not None:
1027
+ override["production_price_incoterm"] = _none_if_null_literal(production_price_incoterm)
1028
+ if production_payment_term is not None:
1029
+ override["production_payment_term"] = _none_if_null_literal(production_payment_term)
1030
+ if customer_note is not None:
1031
+ override["customer_note"] = customer_note
1032
+ if deliverables_json is not None:
1033
+ override["deliverables"] = _parse_json_list_option(
1034
+ deliverables_json, "--deliverables-json"
1035
+ )
1036
+ if additional_deliverables_json is not None:
1037
+ override["additional_deliverables"] = _parse_json_list_option(
1038
+ additional_deliverables_json, "--additional-deliverables-json"
1039
+ )
1040
+ if one_time_production_fees_json is not None:
1041
+ override["one_time_production_fees"] = _parse_json_list_option(
1042
+ one_time_production_fees_json, "--one-time-production-fees-json"
1043
+ )
1044
+ if refundable_terms_json is not None:
1045
+ override["refundable_terms"] = _parse_json_option(
1046
+ refundable_terms_json, "--refundable-terms-json"
1047
+ )
1048
+ if available_sizes_json is not None:
1049
+ override["available_sizes"] = _parse_json_list_option(
1050
+ available_sizes_json, "--available-sizes-json"
1051
+ )
1052
+ if available_colors_json is not None:
1053
+ override["available_colors"] = _parse_json_list_option(
1054
+ available_colors_json, "--available-colors-json"
1055
+ )
1056
+ if component_specifications_json is not None:
1057
+ override["component_specifications"] = _parse_json_list_option(
1058
+ component_specifications_json, "--component-specifications-json"
1059
+ )
1060
+ if link_type_kind is not None:
1061
+ override["type"] = link_type_kind
1062
+ if attributes_json is not None:
1063
+ override["attributes"] = _parse_json_object_option(attributes_json, "--attributes-json")
1064
+ if document_ids_json is not None:
1065
+ override["document_ids"] = _parse_json_list_option(
1066
+ document_ids_json, "--document-ids-json"
1067
+ )
1068
+
1069
+ if not override:
1070
+ console.print(
1071
+ "[red]Nothing to update: pass at least one field option "
1072
+ "(the backend rejects an empty override_info).[/red]"
1073
+ )
1074
+ raise typer.Exit(1)
1075
+
1076
+ # Pricing groups claim cart keys built from the link's own variant lists.
1077
+ # Groups without them price variants no order can contain, so say so
1078
+ # rather than let the quote look fine and misprice later.
1079
+ if override.get("pricing_groups") and not (
1080
+ "available_sizes" in override or "available_colors" in override
1081
+ ):
1082
+ console.print(
1083
+ "[yellow]Warning: --pricing-groups-json without --available-sizes-json / "
1084
+ "--available-colors-json relies on the variant lists already stored on the "
1085
+ "link. Check them with `get-supplier-links-by-supplier-id`.[/yellow]"
1086
+ )
1087
+
1088
+ print_json_or_exit(
1089
+ console,
1090
+ lambda: api_client.edit_supplier_link_override_info(
1091
+ product_id=product_id,
1092
+ link_id=link_id,
1093
+ override_info=override,
1094
+ ),
1095
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "makea-cli"
7
- version = "0.1.21"
7
+ version = "0.1.22"
8
8
  description = "Makea admin CLI (browser login + Makea admin HTTP API)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1 +0,0 @@
1
- __version__ = "0.1.21"
File without changes
File without changes
File without changes