elc-invoice-engine-cli 0.1.0__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.
@@ -0,0 +1,268 @@
1
+ """invoice-request V2 命令组。
2
+
3
+ 接口基路径:/v2/invoice-requests
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import uuid
9
+ import typer
10
+ from typing import Optional
11
+ from cli.session import get_client
12
+ from cli.output import print_result, console
13
+
14
+ app = typer.Typer(help="开票申请单管理 (V2)")
15
+
16
+ # ── 公共 header 选项 ─────────────────────────────────────────────────────────
17
+
18
+ def _idempotency_key() -> str:
19
+ return str(uuid.uuid4())
20
+
21
+
22
+ # ── 命令 ─────────────────────────────────────────────────────────────────────
23
+
24
+ @app.command("list")
25
+ def invoice_list(
26
+ source_system: Optional[str] = typer.Option(None, "--source-system"),
27
+ source_id: Optional[str] = typer.Option(None, "--source-id"),
28
+ status: Optional[str] = typer.Option(None, "--status"),
29
+ page: int = typer.Option(1),
30
+ page_size: int = typer.Option(20, "--page-size"),
31
+ output: str = typer.Option("table", "-o"),
32
+ ):
33
+ """查询开票申请单列表。
34
+
35
+ GET /v2/invoice-requests
36
+ """
37
+ params: dict = {"pageNum": page, "pageSize": page_size}
38
+ if source_system:
39
+ params["sourceSystem"] = source_system
40
+ if source_id:
41
+ params["sourceId"] = source_id
42
+ if status:
43
+ params["status"] = status
44
+ status_code, body = get_client().get("/v2/invoice-requests", params=params)
45
+ # V2 响应:data.invoiceRequests 列表
46
+ data = body.get("data") or {}
47
+ if isinstance(data, dict):
48
+ rows = data.get("invoiceRequests", [])
49
+ body = {"data": rows}
50
+ print_result(status_code, body, output, rows_key="data",
51
+ columns=["invoiceRequestId", "sourceSystem", "sourceId", "invoiceRequestStatus", "createdTime"])
52
+
53
+
54
+ @app.command("get")
55
+ def invoice_get(
56
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
57
+ output: str = typer.Option("json", "-o"),
58
+ ):
59
+ """查询申请单详情。
60
+
61
+ GET /v2/invoice-requests/{invoiceRequestId}
62
+ """
63
+ status, body = get_client().get(f"/v2/invoice-requests/{invoice_request_id}")
64
+ print_result(status, body, output)
65
+
66
+
67
+ @app.command("create")
68
+ def invoice_create(
69
+ file: str = typer.Option(..., "-f", "--file", help="KDUBL XML 文件路径"),
70
+ source_system: str = typer.Option("CLI", "--source-system"),
71
+ source_id: Optional[str] = typer.Option(None, "--source-id"),
72
+ batch_id: Optional[str] = typer.Option(None, "--batch-id", help="批次 ID(UUID),同批次多单共用"),
73
+ idempotency_key: Optional[str] = typer.Option(None, "--idempotency-key", help="幂等键,默认自动生成"),
74
+ timezone: str = typer.Option("UTC+00:00", "--timezone"),
75
+ output: str = typer.Option("json", "-o"),
76
+ ):
77
+ """创建开票申请单(上传 KDUBL XML)。
78
+
79
+ POST /v2/invoice-requests (multipart/form-data)
80
+ """
81
+ import httpx
82
+ from cli.config import BASE_URL, REQUEST_TIMEOUT
83
+
84
+ source_id = source_id or f"CLI-{uuid.uuid4().hex[:8].upper()}"
85
+ ikey = idempotency_key or _idempotency_key()
86
+
87
+ client = get_client()
88
+ headers = {k: v for k, v in client.headers.items() if k.lower() != "content-type"}
89
+ headers.update({
90
+ "X-Request-Id": uuid.uuid4().hex,
91
+ "X-Idempotency-Key": ikey,
92
+ "X-Timezone": timezone,
93
+ })
94
+
95
+ form_data: dict = {"sourceSystem": source_system, "sourceId": source_id}
96
+ if batch_id:
97
+ form_data["batchId"] = batch_id
98
+
99
+ with open(file, "rb") as f:
100
+ files = {"payload": (file.split("/")[-1], f, "application/xml")}
101
+ resp = httpx.post(
102
+ f"{BASE_URL}/v2/invoice-requests",
103
+ headers=headers,
104
+ files=files,
105
+ data=form_data,
106
+ timeout=REQUEST_TIMEOUT,
107
+ )
108
+
109
+ body = resp.json() if "application/json" in resp.headers.get("content-type", "") else {"raw": resp.text}
110
+ console.print(f"[dim]幂等键:{ikey}[/dim]")
111
+ print_result(resp.status_code, body, output)
112
+
113
+
114
+ @app.command("update")
115
+ def invoice_update(
116
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
117
+ file: str = typer.Option(..., "-f", "--file", help="KDUBL XML 文件路径"),
118
+ idempotency_key: Optional[str] = typer.Option(None, "--idempotency-key"),
119
+ timezone: str = typer.Option("UTC+00:00", "--timezone"),
120
+ output: str = typer.Option("json", "-o"),
121
+ ):
122
+ """更新开票申请单(状态为 draft/validate_failed/pending 时可更新)。
123
+
124
+ PUT /v2/invoice-requests/{invoiceRequestId} (multipart/form-data)
125
+ """
126
+ import httpx
127
+ from cli.config import BASE_URL, REQUEST_TIMEOUT
128
+
129
+ ikey = idempotency_key or _idempotency_key()
130
+ client = get_client()
131
+ headers = {k: v for k, v in client.headers.items() if k.lower() != "content-type"}
132
+ headers.update({
133
+ "X-Request-Id": uuid.uuid4().hex,
134
+ "X-Idempotency-Key": ikey,
135
+ "X-Timezone": timezone,
136
+ })
137
+
138
+ with open(file, "rb") as f:
139
+ files = {"payload": (file.split("/")[-1], f, "application/xml")}
140
+ resp = httpx.put(
141
+ f"{BASE_URL}/v2/invoice-requests/{invoice_request_id}",
142
+ headers=headers,
143
+ files=files,
144
+ timeout=REQUEST_TIMEOUT,
145
+ )
146
+
147
+ body = resp.json() if "application/json" in resp.headers.get("content-type", "") else {"raw": resp.text}
148
+ print_result(resp.status_code, body, output)
149
+
150
+
151
+ @app.command("void")
152
+ def invoice_void(
153
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
154
+ output: str = typer.Option("json", "-o"),
155
+ ):
156
+ """作废开票申请单(设置状态为 void)。
157
+
158
+ POST /v2/invoice-requests/{invoiceRequestId}/void
159
+ """
160
+ client = get_client()
161
+ status, body = client.post(f"/v2/invoice-requests/{invoice_request_id}/void")
162
+ print_result(status, body, output)
163
+
164
+
165
+ @app.command("issue")
166
+ def invoice_issue(
167
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
168
+ output: str = typer.Option("json", "-o"),
169
+ ):
170
+ """触发开票(Issue Invoice)。
171
+
172
+ POST /v2/invoice-requests/{invoiceRequestId}/issue
173
+ """
174
+ client = get_client()
175
+ status, body = client.post(f"/v2/invoice-requests/{invoice_request_id}/issue")
176
+ print_result(status, body, output)
177
+
178
+
179
+ @app.command("invoices")
180
+ def invoice_get_invoices(
181
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
182
+ output: str = typer.Option("table", "-o"),
183
+ ):
184
+ """查询申请单下的发票列表。
185
+
186
+ GET /v2/invoice-requests/{invoiceRequestId}/invoices
187
+ """
188
+ status, body = get_client().get(f"/v2/invoice-requests/{invoice_request_id}/invoices")
189
+ print_result(status, body, output, rows_key="data",
190
+ columns=["invoiceId", "invoiceNo", "invoiceStatus", "issueStatus", "totalAmount"])
191
+
192
+
193
+ # ── Credit Note 匹配 ──────────────────────────────────────────────────────────
194
+
195
+ @app.command("match-cn-create")
196
+ def match_cn_create(
197
+ file: str = typer.Option(..., "-f", "--file", help="Credit Note KDUBL XML 文件路径"),
198
+ idempotency_key: Optional[str] = typer.Option(None, "--idempotency-key"),
199
+ output: str = typer.Option("json", "-o"),
200
+ ):
201
+ """创建 Credit Note 蓝冲匹配任务。
202
+
203
+ POST /v2/invoice-requests/match-cn (multipart/form-data)
204
+ """
205
+ import httpx
206
+ from cli.config import BASE_URL, REQUEST_TIMEOUT
207
+
208
+ ikey = idempotency_key or _idempotency_key()
209
+ client = get_client()
210
+ headers = {k: v for k, v in client.headers.items() if k.lower() != "content-type"}
211
+ headers.update({"X-Request-Id": uuid.uuid4().hex, "X-Idempotency-Key": ikey})
212
+
213
+ with open(file, "rb") as f:
214
+ resp = httpx.post(
215
+ f"{BASE_URL}/v2/invoice-requests/match-cn",
216
+ headers=headers,
217
+ files={"payload": (file.split("/")[-1], f, "application/xml")},
218
+ timeout=REQUEST_TIMEOUT,
219
+ )
220
+
221
+ body = resp.json() if "application/json" in resp.headers.get("content-type", "") else {"raw": resp.text}
222
+ console.print(f"[dim]幂等键:{ikey}[/dim]")
223
+ print_result(resp.status_code, body, output)
224
+
225
+
226
+ @app.command("match-cn-run")
227
+ def match_cn_run(
228
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
229
+ reason_code: str = typer.Option("OTHER", "--reason", help="RETURN / ALLOWANCE / OTHER"),
230
+ output: str = typer.Option("json", "-o"),
231
+ ):
232
+ """触发 Credit Note 蓝冲匹配任务。
233
+
234
+ POST /v2/invoice-requests/match-cn/{invoiceRequestId}/run
235
+ """
236
+ status, body = get_client().post(
237
+ f"/v2/invoice-requests/match-cn/{invoice_request_id}/run",
238
+ json={"reasonCode": reason_code},
239
+ )
240
+ print_result(status, body, output)
241
+
242
+
243
+ @app.command("match-cn-results")
244
+ def match_cn_results(
245
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
246
+ output: str = typer.Option("json", "-o"),
247
+ ):
248
+ """查询 Credit Note 蓝冲匹配结果。
249
+
250
+ GET /v2/invoice-requests/{invoiceRequestId}/matchResults
251
+ """
252
+ status, body = get_client().get(f"/v2/invoice-requests/{invoice_request_id}/matchResults")
253
+ print_result(status, body, output)
254
+
255
+
256
+ @app.command("match-cn-unlink")
257
+ def match_cn_unlink(
258
+ invoice_request_id: str = typer.Argument(..., help="申请单 ID"),
259
+ output: str = typer.Option("json", "-o"),
260
+ ):
261
+ """解除 Credit Note 蓝冲匹配绑定。
262
+
263
+ POST /v2/invoice-requests/match-cn/{invoiceRequestId}/unlink
264
+ """
265
+ status, body = get_client().post(
266
+ f"/v2/invoice-requests/match-cn/{invoice_request_id}/unlink"
267
+ )
268
+ print_result(status, body, output)
@@ -0,0 +1,104 @@
1
+ """invoice (发票) 命令组 — 区别于 invoice-request。"""
2
+ from __future__ import annotations
3
+
4
+ import httpx
5
+ import typer
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from cli.config import BASE_URL, REQUEST_TIMEOUT
9
+ from cli.session import get_client
10
+ from cli.output import print_result, console
11
+
12
+ app = typer.Typer(help="发票管理 (V2)")
13
+
14
+
15
+ @app.command("get")
16
+ def invoice_get(
17
+ invoice_id: str = typer.Argument(..., help="发票 ID"),
18
+ output: str = typer.Option("json", "-o"),
19
+ ):
20
+ """查询发票详情。
21
+
22
+ GET /v2/invoices/{invoiceId}
23
+ """
24
+ status, body = get_client().get(f"/v2/invoices/{invoice_id}")
25
+ print_result(status, body, output)
26
+
27
+
28
+ @app.command("file-link")
29
+ def invoice_file_link(
30
+ invoice_id: str = typer.Argument(..., help="发票 ID"),
31
+ file_type: str = typer.Option("Humanreadable", "--type",
32
+ help="Humanreadable(PDF) / Target(电子发票) / Source(KDUBL)"),
33
+ file_format: str = typer.Option("xml", "--format", help="xml / json"),
34
+ link_type: str = typer.Option("EXTERNAL", "--link-type", help="EXTERNAL / INTERNAL"),
35
+ expiry_days: int = typer.Option(7, "--expiry-days"),
36
+ output: str = typer.Option("json", "-o"),
37
+ ):
38
+ """获取发票文件下载链接。
39
+
40
+ GET /v2/invoices/file-link/{invoiceId}
41
+ """
42
+ params = {
43
+ "fileType": file_type,
44
+ "fileFormat": file_format,
45
+ "linkType": link_type,
46
+ "expiryDays": expiry_days,
47
+ }
48
+ status, body = get_client().get(f"/v2/invoices/file-link/{invoice_id}", params=params)
49
+ print_result(status, body, output)
50
+
51
+
52
+ @app.command("cancel")
53
+ def invoice_cancel(
54
+ invoice_id: str = typer.Argument(..., help="发票 ID"),
55
+ reason: str = typer.Option(..., "--reason", help="取消原因(MY 发票必填,72小时内)"),
56
+ output: str = typer.Option("json", "-o"),
57
+ ):
58
+ """取消发票(仅支持马来西亚 MY,72小时内)。
59
+
60
+ POST /v1/invoice/{invoiceId}/cancel
61
+ """
62
+ status, body = get_client().post(f"/v1/invoice/{invoice_id}/cancel", json={"reason": reason})
63
+ print_result(status, body, output)
64
+
65
+
66
+ @app.command("download")
67
+ def invoice_download(
68
+ invoice_id: str = typer.Argument(..., help="发票 ID"),
69
+ file_type: str = typer.Option("Source", "--type",
70
+ help="Humanreadable(PDF) / Target(电子发票) / Source(KDUBL XML)"),
71
+ file_format: str = typer.Option("xml", "--format", help="xml / json"),
72
+ out: str = typer.Option(".", "--out", "-o", help="保存目录,默认当前目录"),
73
+ filename: Optional[str] = typer.Option(None, "--name", "-n", help="文件名,默认用 invoiceId"),
74
+ ):
75
+ """直接下载发票文件到本地。
76
+
77
+ 使用内部接口 /v1/invoices/file/{invoiceId} 下载原始文件。
78
+ """
79
+ client = get_client()
80
+ token = client.headers.get("Authorization", "").removeprefix("Bearer ")
81
+ company = client.headers.get("X-Company-ID", "") or client.headers.get("X-Company-Id", "")
82
+
83
+ headers = {"Authorization": f"Bearer {token}", "X-Company-Id": company}
84
+ params = {"filetype": file_type, "fileFormat": file_format}
85
+
86
+ resp = httpx.get(
87
+ f"{BASE_URL}/v1/invoices/file/{invoice_id}",
88
+ headers=headers,
89
+ params=params,
90
+ timeout=REQUEST_TIMEOUT,
91
+ )
92
+
93
+ ct = resp.headers.get("content-type", "")
94
+ if resp.status_code != 200 or ("xml" not in ct and not resp.content.strip().startswith(b"<")):
95
+ console.print(f"[red]下载失败(HTTP {resp.status_code}):{resp.content[:200]}[/red]")
96
+ raise typer.Exit(1)
97
+
98
+ ext = ".pdf" if file_type == "Humanreadable" else f".{file_format}"
99
+ name = filename or f"{invoice_id}{ext}"
100
+ out_path = Path(out).expanduser() / name
101
+ out_path.parent.mkdir(parents=True, exist_ok=True)
102
+ out_path.write_bytes(resp.content)
103
+
104
+ console.print(f"[green]✓ 已下载:{out_path}({len(resp.content)} 字节)[/green]")
cli/commands/login.py ADDED
@@ -0,0 +1,82 @@
1
+ """login / logout 命令。"""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import typer
6
+ from rich.console import Console
7
+ from cli.token_store import save_token, clear_token, load_token
8
+ from cli.auth import fetch_token
9
+ from cli.session import reset_client
10
+
11
+ app = typer.Typer(help="登录与认证管理", invoke_without_command=True)
12
+ console = Console()
13
+
14
+
15
+ @app.callback(invoke_without_command=True)
16
+ def login(
17
+ ctx: typer.Context,
18
+ domain: str = typer.Option(None, "--domain", help="租户域名,如 kingdee-fpy"),
19
+ app_id: str = typer.Option(None, "--app-id", help="应用 ID"),
20
+ secret: str = typer.Option(None, "--secret", help="应用密钥"),
21
+ mobile: str = typer.Option(None, "--mobile", help="手机号"),
22
+ org_num: str = typer.Option(None, "--org-num", help="组织编号"),
23
+ base_url: str = typer.Option(None, "--url", help="服务地址,如 http://host:port/xm-demo"),
24
+ ):
25
+ """登录并保存凭证到 ~/.elc/token.json。
26
+
27
+ 凭证可通过选项传入,也可以交互式输入。已有 .env.local 配置的字段可直接回车跳过。
28
+ """
29
+ if ctx.invoked_subcommand is not None:
30
+ return
31
+
32
+ import cli.config as cfg
33
+
34
+ console.print("[bold cyan]ELC Invoice Engine CLI — 登录[/bold cyan]")
35
+ console.print("已有 .env.local 配置的字段可直接回车跳过\n")
36
+
37
+ # 覆盖配置(命令行优先,其次交互,再次 .env.local)
38
+ def _ask(label: str, current: str, opt_val: str | None, secret_input: bool = False) -> str:
39
+ if opt_val:
40
+ return opt_val
41
+ display = f"[dim]{current}[/dim]" if current else "[dim](未配置)[/dim]"
42
+ prompt = f"{label} {display}"
43
+ val = typer.prompt(prompt, default="", hide_input=secret_input, show_default=False)
44
+ return val.strip() or current
45
+
46
+ if base_url:
47
+ os.environ["BASE_URL"] = base_url
48
+ cfg.BASE_URL = base_url.rstrip("/")
49
+
50
+ cfg.SSO_CONFIG["domain"] = _ask("租户域名 (domain) ", cfg.SSO_CONFIG["domain"], domain)
51
+ cfg.SSO_CONFIG["app_id"] = _ask("应用 ID (appId) ", cfg.SSO_CONFIG["app_id"], app_id)
52
+ cfg.SSO_CONFIG["secret"] = _ask("应用密钥 (secret) ", cfg.SSO_CONFIG["secret"], secret, secret_input=True)
53
+ cfg.SSO_CONFIG["mobile"] = _ask("手机号 (mobile) ", cfg.SSO_CONFIG["mobile"], mobile)
54
+ cfg.SSO_CONFIG["org_num"] = _ask("组织编号 (orgNum) ", cfg.SSO_CONFIG["org_num"], org_num)
55
+
56
+ console.print("\n正在登录...")
57
+ try:
58
+ token, expires_in = fetch_token()
59
+ save_token(token, expires_in)
60
+ reset_client()
61
+ console.print(f"[green]✓ 登录成功[/green],token 已保存至 ~/.elc/token.json(有效期 {expires_in // 3600}h)")
62
+ except Exception as e:
63
+ console.print(f"[red]✗ 登录失败:{e}[/red]")
64
+ raise typer.Exit(1)
65
+
66
+
67
+ @app.command("logout")
68
+ def logout():
69
+ """清除本地保存的 token。"""
70
+ clear_token()
71
+ reset_client()
72
+ console.print("[green]✓ 已退出登录[/green]")
73
+
74
+
75
+ @app.command("status")
76
+ def status():
77
+ """查看当前登录状态。"""
78
+ token = load_token()
79
+ if token:
80
+ console.print(f"[green]✓ 已登录[/green],token 前缀:{token[:20]}...")
81
+ else:
82
+ console.print("[yellow]未登录或 token 已过期[/yellow],请运行 elc login")
cli/commands/party.py ADDED
@@ -0,0 +1,72 @@
1
+ """party V2 命令组。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import typer
6
+ from typing import Optional
7
+ from cli.session import get_client
8
+ from cli.output import print_result
9
+
10
+ app = typer.Typer(help="注册主体(Party)管理 - V2")
11
+
12
+
13
+ @app.command("list")
14
+ def party_list(
15
+ keyword: Optional[str] = typer.Option(None, "-k", "--keyword", help="关键字搜索"),
16
+ type: Optional[int] = typer.Option(None, "--type", help="主体类型:1=供应商 2=客户"),
17
+ page: int = typer.Option(1, help="页码"),
18
+ page_size: int = typer.Option(20, "--page-size"),
19
+ output: str = typer.Option("table", "-o"),
20
+ ):
21
+ """查询注册主体列表。"""
22
+ payload = {"pageNum": page, "pageSize": page_size}
23
+ if keyword:
24
+ payload["keyword"] = keyword
25
+ if type is not None:
26
+ payload["type"] = type
27
+ status, body = get_client().post("/api/party/page", json=payload)
28
+ print_result(status, body, output, rows_key="rows",
29
+ columns=["id", "name", "organizationNo", "country", "type", "status"])
30
+
31
+
32
+ @app.command("get")
33
+ def party_get(
34
+ id: int = typer.Argument(..., help="主体 ID"),
35
+ output: str = typer.Option("json", "-o"),
36
+ ):
37
+ """查询注册主体详情。"""
38
+ status, body = get_client().get(f"/api/party/{id}")
39
+ print_result(status, body, output)
40
+
41
+
42
+ @app.command("upsert")
43
+ def party_upsert(
44
+ file: Optional[str] = typer.Option(None, "-f", "--file", help="JSON 文件路径"),
45
+ data: Optional[str] = typer.Option(None, "-d", "--data", help="内联 JSON 字符串"),
46
+ output: str = typer.Option("json", "-o"),
47
+ ):
48
+ """创建或更新注册主体(传入 JSON)。
49
+
50
+ 示例:
51
+ elc party upsert -f party.json
52
+ elc party upsert -d '{"type":1,"name":"ACME","country":"DE",...}'
53
+ """
54
+ if file:
55
+ with open(file) as f:
56
+ payload = json.load(f)
57
+ elif data:
58
+ payload = json.loads(data)
59
+ else:
60
+ typer.echo("需要 --file 或 --data 参数", err=True)
61
+ raise typer.Exit(1)
62
+ status, body = get_client().post("/v2/parties/create-or-update", json=payload)
63
+ print_result(status, body, output)
64
+
65
+
66
+ @app.command("delete")
67
+ def party_delete(
68
+ id: int = typer.Argument(..., help="主体 ID"),
69
+ ):
70
+ """删除注册主体。"""
71
+ status, body = get_client().delete(f"/api/party/{id}")
72
+ print_result(status, body, "json")
@@ -0,0 +1,67 @@
1
+ """product V2 命令组。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import typer
6
+ from typing import Optional
7
+ from cli.session import get_client
8
+ from cli.output import print_result
9
+
10
+ app = typer.Typer(help="商品/物料管理 - V2")
11
+
12
+
13
+ @app.command("list")
14
+ def product_list(
15
+ keyword: Optional[str] = typer.Option(None, "-k", "--keyword"),
16
+ category: Optional[str] = typer.Option(None, "--category", help="GOODS / SERVICE"),
17
+ page: int = typer.Option(1),
18
+ page_size: int = typer.Option(20, "--page-size"),
19
+ output: str = typer.Option("table", "-o"),
20
+ ):
21
+ """查询商品列表。"""
22
+ payload: dict = {"pageNum": page, "pageSize": page_size}
23
+ if keyword:
24
+ payload["keyword"] = keyword
25
+ if category:
26
+ payload["categoryType"] = category
27
+ status, body = get_client().post("/api/product/page", json=payload)
28
+ print_result(status, body, output, rows_key="rows",
29
+ columns=["id", "productCode", "productName", "uomCode", "categoryType", "status"])
30
+
31
+
32
+ @app.command("get")
33
+ def product_get(
34
+ id: int = typer.Argument(...),
35
+ output: str = typer.Option("json", "-o"),
36
+ ):
37
+ """查询商品详情。"""
38
+ status, body = get_client().get(f"/api/product/{id}")
39
+ print_result(status, body, output)
40
+
41
+
42
+ @app.command("upsert")
43
+ def product_upsert(
44
+ file: Optional[str] = typer.Option(None, "-f", "--file"),
45
+ data: Optional[str] = typer.Option(None, "-d", "--data"),
46
+ output: str = typer.Option("json", "-o"),
47
+ ):
48
+ """创建或更新商品(传入 JSON)。"""
49
+ if file:
50
+ with open(file) as f:
51
+ payload = json.load(f)
52
+ elif data:
53
+ payload = json.loads(data)
54
+ else:
55
+ typer.echo("需要 --file 或 --data 参数", err=True)
56
+ raise typer.Exit(1)
57
+ status, body = get_client().post("/v2/products/create-or-update", json=payload)
58
+ print_result(status, body, output)
59
+
60
+
61
+ @app.command("delete")
62
+ def product_delete(
63
+ id: int = typer.Argument(...),
64
+ ):
65
+ """删除商品。"""
66
+ status, body = get_client().delete(f"/api/product/{id}")
67
+ print_result(status, body, "json")
cli/commands/tax.py ADDED
@@ -0,0 +1,67 @@
1
+ """tax-category 命令组。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import typer
6
+ from typing import Optional
7
+ from cli.session import get_client
8
+ from cli.output import print_result
9
+
10
+ app = typer.Typer(help="税目管理")
11
+
12
+
13
+ @app.command("list")
14
+ def tax_list(
15
+ country: Optional[str] = typer.Option(None, "--country", help="国家代码,如 MY"),
16
+ keyword: Optional[str] = typer.Option(None, "-k", "--keyword"),
17
+ page: int = typer.Option(1),
18
+ page_size: int = typer.Option(20, "--page-size"),
19
+ output: str = typer.Option("table", "-o"),
20
+ ):
21
+ """查询税目列表。"""
22
+ payload = {"pageNum": page, "pageSize": page_size}
23
+ if country:
24
+ payload["country"] = country
25
+ if keyword:
26
+ payload["keyword"] = keyword
27
+ status, body = get_client().post("/api/taxCategory/page", json=payload)
28
+ print_result(status, body, output, rows_key="rows",
29
+ columns=["id", "code", "name", "country", "taxType", "schemeId"])
30
+
31
+
32
+ @app.command("get")
33
+ def tax_get(
34
+ id: int = typer.Argument(...),
35
+ output: str = typer.Option("json", "-o"),
36
+ ):
37
+ """查询税目详情。"""
38
+ status, body = get_client().get(f"/api/taxCategory/{id}")
39
+ print_result(status, body, output)
40
+
41
+
42
+ @app.command("create")
43
+ def tax_create(
44
+ file: Optional[str] = typer.Option(None, "-f", "--file"),
45
+ data: Optional[str] = typer.Option(None, "-d", "--data"),
46
+ output: str = typer.Option("json", "-o"),
47
+ ):
48
+ """新增税目(传入 JSON)。"""
49
+ if file:
50
+ with open(file) as f:
51
+ payload = json.load(f)
52
+ elif data:
53
+ payload = json.loads(data)
54
+ else:
55
+ typer.echo("需要 --file 或 --data 参数", err=True)
56
+ raise typer.Exit(1)
57
+ status, body = get_client().post("/api/taxCategory", json=payload)
58
+ print_result(status, body, output)
59
+
60
+
61
+ @app.command("delete")
62
+ def tax_delete(
63
+ id: int = typer.Argument(...),
64
+ ):
65
+ """删除税目。"""
66
+ status, body = get_client().delete(f"/api/taxCategory/{id}")
67
+ print_result(status, body, "json")