scanye-py 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheUndefined
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: scanye-py
3
+ Version: 0.1.0
4
+ Summary: Unofficial Python library and CLI for the Scanye accounting API
5
+ Author: TheUndefined
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/theundefined/scanye-py
8
+ Project-URL: Repository, https://github.com/theundefined/scanye-py
9
+ Project-URL: Issues, https://github.com/theundefined/scanye-py/issues
10
+ Keywords: scanye,invoicing,ksef,accounting,api-client
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: httpx>=0.27.0
26
+ Requires-Dist: python-dotenv>=1.0.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
30
+ Requires-Dist: respx>=0.21.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
32
+ Requires-Dist: black>=24.2.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.9.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # Scanye Python Library and CLI
37
+
38
+ A Python library and CLI tool for interacting with the Scanye API.
39
+
40
+ > **Unofficial project.** This library is not created, maintained, sponsored, or endorsed by Scanye. It's a community-built client for the unofficial Scanye API (`api.scanye.pl`), reverse-engineered from the Scanye web app's network traffic. Use at your own risk.
41
+
42
+ ## Features
43
+ - Login and authentication
44
+ - List sales and purchase invoices
45
+ - Filter invoices by KSeF status
46
+ - Download invoices as PDF
47
+ - CLI for easy access
48
+
49
+ ## Installation
50
+ ```bash
51
+ pip install scanye-py
52
+ ```
53
+
54
+ ## CLI Usage
55
+ First, login to your account:
56
+ ```bash
57
+ scanye login --email your@email.com
58
+ ```
59
+
60
+ List your sales invoices:
61
+ ```bash
62
+ scanye invoices list --type sales
63
+ ```
64
+
65
+ List purchase invoices that are not yet sent to KSeF:
66
+ ```bash
67
+ scanye invoices list --type purchase --unsent
68
+ ```
69
+
70
+ Download all sales and purchase invoices from a given month as PDF (multiple invoices are downloaded as a single ZIP and extracted automatically):
71
+ ```bash
72
+ scanye invoices download --type sales --month 2026-07 -o ./invoices/2026-07/sales
73
+ scanye invoices download --type purchase --month 2026-07 -o ./invoices/2026-07/purchase
74
+ ```
75
+
76
+ Download a single invoice by ID:
77
+ ```bash
78
+ scanye invoices download <invoice-id> -o ./invoices
79
+ ```
80
+
81
+ ## Library Usage
82
+ ```python
83
+ from scanye.client import ScanyeClient
84
+
85
+ client = ScanyeClient()
86
+ client.login("your@email.com", "your_password")
87
+
88
+ invoices = client.fetch_invoices(is_sales=True)
89
+ for inv in invoices:
90
+ print(f"{inv.invoice_no}: {inv.ksef_status}")
91
+ ```
@@ -0,0 +1,56 @@
1
+ # Scanye Python Library and CLI
2
+
3
+ A Python library and CLI tool for interacting with the Scanye API.
4
+
5
+ > **Unofficial project.** This library is not created, maintained, sponsored, or endorsed by Scanye. It's a community-built client for the unofficial Scanye API (`api.scanye.pl`), reverse-engineered from the Scanye web app's network traffic. Use at your own risk.
6
+
7
+ ## Features
8
+ - Login and authentication
9
+ - List sales and purchase invoices
10
+ - Filter invoices by KSeF status
11
+ - Download invoices as PDF
12
+ - CLI for easy access
13
+
14
+ ## Installation
15
+ ```bash
16
+ pip install scanye-py
17
+ ```
18
+
19
+ ## CLI Usage
20
+ First, login to your account:
21
+ ```bash
22
+ scanye login --email your@email.com
23
+ ```
24
+
25
+ List your sales invoices:
26
+ ```bash
27
+ scanye invoices list --type sales
28
+ ```
29
+
30
+ List purchase invoices that are not yet sent to KSeF:
31
+ ```bash
32
+ scanye invoices list --type purchase --unsent
33
+ ```
34
+
35
+ Download all sales and purchase invoices from a given month as PDF (multiple invoices are downloaded as a single ZIP and extracted automatically):
36
+ ```bash
37
+ scanye invoices download --type sales --month 2026-07 -o ./invoices/2026-07/sales
38
+ scanye invoices download --type purchase --month 2026-07 -o ./invoices/2026-07/purchase
39
+ ```
40
+
41
+ Download a single invoice by ID:
42
+ ```bash
43
+ scanye invoices download <invoice-id> -o ./invoices
44
+ ```
45
+
46
+ ## Library Usage
47
+ ```python
48
+ from scanye.client import ScanyeClient
49
+
50
+ client = ScanyeClient()
51
+ client.login("your@email.com", "your_password")
52
+
53
+ invoices = client.fetch_invoices(is_sales=True)
54
+ for inv in invoices:
55
+ print(f"{inv.invoice_no}: {inv.ksef_status}")
56
+ ```
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scanye-py"
7
+ version = "0.1.0"
8
+ description = "Unofficial Python library and CLI for the Scanye accounting API"
9
+ readme = "README.md"
10
+ authors = [{ name = "TheUndefined" }]
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ requires-python = ">=3.10"
14
+ keywords = ["scanye", "invoicing", "ksef", "accounting", "api-client"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Office/Business :: Financial :: Accounting",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "httpx>=0.27.0",
30
+ "python-dotenv>=1.0.1",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/theundefined/scanye-py"
35
+ Repository = "https://github.com/theundefined/scanye-py"
36
+ Issues = "https://github.com/theundefined/scanye-py/issues"
37
+
38
+ [project.scripts]
39
+ scanye = "scanye.cli:main"
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=8.0.0",
44
+ "pytest-asyncio>=0.23.0",
45
+ "respx>=0.21.0",
46
+ "ruff>=0.3.0",
47
+ "black>=24.2.0",
48
+ "mypy>=1.9.0",
49
+ ]
50
+
51
+ [tool.setuptools.package-data]
52
+ scanye = ["py.typed"]
53
+
54
+ [tool.black]
55
+ line-length = 120
56
+ target-version = ['py310']
57
+
58
+ [tool.ruff]
59
+ line-length = 120
60
+ target-version = "py310"
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "I", "W"]
64
+ ignore = []
65
+
66
+ [tool.mypy]
67
+ python_version = "3.10"
68
+ warn_return_any = true
69
+ warn_unused_configs = true
70
+ disallow_untyped_defs = true
71
+ files = "src"
72
+ mypy_path = "src"
73
+ explicit_package_bases = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,364 @@
1
+ import argparse
2
+ import io
3
+ import json
4
+ import os
5
+ import sys
6
+ import zipfile
7
+ from datetime import datetime
8
+ from getpass import getpass
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ from .client import ScanyeClient
13
+ from .exceptions import ScanyeError
14
+
15
+ CONFIG_DIR = Path.home() / ".config" / "scanye"
16
+ CONFIG_FILE = CONFIG_DIR / "config.json"
17
+
18
+
19
+ def save_config(config: dict) -> None:
20
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
21
+ os.chmod(CONFIG_DIR, 0o700)
22
+ fd = os.open(CONFIG_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
23
+ with os.fdopen(fd, "w") as f:
24
+ json.dump(config, f)
25
+ os.chmod(CONFIG_FILE, 0o600)
26
+
27
+
28
+ def load_config() -> dict:
29
+ if not CONFIG_FILE.exists():
30
+ return {}
31
+ with open(CONFIG_FILE, "r") as f:
32
+ config = json.load(f)
33
+ if isinstance(config, dict):
34
+ return config
35
+ return {}
36
+
37
+
38
+ def build_client(config: dict, debug: bool) -> ScanyeClient:
39
+ return ScanyeClient(
40
+ token=config.get("token"),
41
+ debug=debug,
42
+ email=config.get("email"),
43
+ password=config.get("password"),
44
+ )
45
+
46
+
47
+ def persist_token(config: dict, client: ScanyeClient) -> None:
48
+ """Save the client's current token if it changed (e.g. after an automatic re-login)."""
49
+ if client.token and client.token != config.get("token"):
50
+ config["token"] = client.token
51
+ save_config(config)
52
+
53
+
54
+ def require_credentials(config: dict) -> None:
55
+ if not config.get("token") and not (config.get("email") and config.get("password")):
56
+ print("Not logged in. Run 'scanye login' first.", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+
60
+ def handle_login(args: argparse.Namespace) -> None:
61
+ email = args.email
62
+ password = getpass(f"Password for {email}: ")
63
+
64
+ client = ScanyeClient(debug=args.debug)
65
+ try:
66
+ token = client.login(email, password)
67
+ config = {"token": token, "email": email}
68
+
69
+ answer = input("Save password so expired tokens can be refreshed automatically? [y/N]: ")
70
+ if answer.strip().lower() in ("y", "yes"):
71
+ config["password"] = password
72
+ save_config(config)
73
+ print("Login successful. Token and password saved.")
74
+ else:
75
+ save_config(config)
76
+ print("Login successful. Token saved.")
77
+ except ScanyeError as e:
78
+ print(f"Login failed: {e}", file=sys.stderr)
79
+ sys.exit(1)
80
+
81
+
82
+ def build_month_filters(months: Optional[List[str]], raw_filter: Optional[str]) -> List[str]:
83
+ filters = ["dateAuthenticated?isNotNull"]
84
+ # Note: "unsent" isn't a real server-side field; callers apply it client-side
85
+ # after fetching, once ksef_status has been resolved from the raw invoice payload.
86
+
87
+ if raw_filter:
88
+ filters.append(raw_filter)
89
+ else:
90
+ now = datetime.now()
91
+ if not months:
92
+ # Default to a broad range for current year to show everything relevant
93
+ start_month = f"{now.year}-01"
94
+ end_month = f"{now.year + 1}-01"
95
+ filters.append(f"annotations.accountingMonth>={start_month}")
96
+ filters.append(f"annotations.accountingMonth<={end_month}")
97
+ else:
98
+ # Use specific range for requested months
99
+ # For simplicity, if multiple months are provided, we just use the range from min to max
100
+ sorted_months = sorted(months)
101
+ filters.append(f"annotations.accountingMonth>={sorted_months[0]}")
102
+ filters.append(f"annotations.accountingMonth<={sorted_months[-1]}")
103
+
104
+ return filters
105
+
106
+
107
+ def handle_invoices_list(args: argparse.Namespace) -> None:
108
+ config = load_config()
109
+ require_credentials(config)
110
+
111
+ is_sales = args.type == "sales"
112
+ client = build_client(config, args.debug)
113
+ filters = build_month_filters(args.month, args.filter)
114
+
115
+ try:
116
+ # Fetch more if we are filtering client-side
117
+ fetch_limit = args.limit * 5 if args.unsent else args.limit
118
+ invoices = client.fetch_invoices(
119
+ is_sales=is_sales,
120
+ limit=fetch_limit,
121
+ filters=",".join(filters) if filters else None,
122
+ )
123
+
124
+ if args.unsent:
125
+ invoices = [inv for inv in invoices if not inv.ksef_status or inv.ksef_status == "N/A"]
126
+ invoices = invoices[: args.limit]
127
+
128
+ if not invoices:
129
+ print("No invoices found.")
130
+ return
131
+
132
+ # "Client" for sales invoices (the buyer), "Seller" for purchase invoices (the vendor).
133
+ counterparty_label = "Client" if is_sales else "Seller"
134
+
135
+ # Header definition based on verbosity
136
+ if args.verbose:
137
+ h1 = f"{'ID':<38} | {'Date':<10} | {'Inv No':<15} | {'Gross':<10} | "
138
+ h2 = f"{'Paid':<10} | {'Tax No':<12} | {'Email':<25} | {counterparty_label}"
139
+ header = h1 + h2
140
+ else:
141
+ h1 = f"{'ID':<38} | {'Date':<10} | {'Inv No':<15} | {'Gross':<10} | "
142
+ h2 = f"{'Paid Date':<10} | {counterparty_label:<30} | {'KSeF'}"
143
+ header = h1 + h2
144
+
145
+ print(header)
146
+ print("-" * len(header))
147
+ for inv in invoices:
148
+ date = inv.issue_date or "N/A"
149
+ gross = inv.gross_amount or "0.00"
150
+ paid_date = inv.transfer_date or "N/A"
151
+
152
+ if args.verbose:
153
+ tax_no = inv.counterparty_tax_no or "N/A"
154
+ email = (inv.counterparty_email or "N/A")[:25]
155
+ counterparty = inv.counterparty_name or ""
156
+ p1 = f"{inv.id:<38} | {date:<10} | {inv.invoice_no:<15} | {gross:<10} | "
157
+ p2 = f"{paid_date:<10} | {tax_no:<12} | {email:<25} | {counterparty}"
158
+ print(p1 + p2)
159
+ else:
160
+ counterparty = (inv.counterparty_name or "")[:30]
161
+ ksef = inv.ksef_status or "N/A"
162
+ p1 = f"{inv.id:<38} | {date:<10} | {inv.invoice_no:<15} | {gross:<10} | "
163
+ p2 = f"{paid_date:<10} | {counterparty:<30} | {ksef}"
164
+ print(p1 + p2)
165
+
166
+ except ScanyeError as e:
167
+ print(f"Error: {e}", file=sys.stderr)
168
+ sys.exit(1)
169
+ finally:
170
+ persist_token(config, client)
171
+
172
+
173
+ def handle_invoices_mark_paid(args: argparse.Namespace) -> None:
174
+ config = load_config()
175
+ require_credentials(config)
176
+
177
+ client = build_client(config, args.debug)
178
+ try:
179
+ client.mark_as_paid(args.invoice_ids, transfer_date=args.date)
180
+ print(f"Successfully marked {len(args.invoice_ids)} invoices as paid.")
181
+ except ScanyeError as e:
182
+ print(f"Error: {e}", file=sys.stderr)
183
+ sys.exit(1)
184
+ finally:
185
+ persist_token(config, client)
186
+
187
+
188
+ def handle_invoices_mark_unpaid(args: argparse.Namespace) -> None:
189
+ config = load_config()
190
+ require_credentials(config)
191
+
192
+ client = build_client(config, args.debug)
193
+ try:
194
+ client.mark_as_unpaid(args.invoice_ids)
195
+ print(f"Successfully marked {len(args.invoice_ids)} invoices as unpaid.")
196
+ except ScanyeError as e:
197
+ print(f"Error: {e}", file=sys.stderr)
198
+ sys.exit(1)
199
+ finally:
200
+ persist_token(config, client)
201
+
202
+
203
+ def handle_invoices_send_ksef(args: argparse.Namespace) -> None:
204
+ config = load_config()
205
+ require_credentials(config)
206
+
207
+ client = build_client(config, args.debug)
208
+ invoice_ids = args.invoice_ids or []
209
+
210
+ try:
211
+ if args.all:
212
+ print("Searching for unsent sales invoices...")
213
+ # Fetch invoices from last few months to be safe
214
+ now = datetime.now()
215
+ start_month = f"{now.year if now.month > 1 else now.year - 1}-{max(1, (now.month - 2) % 12 or 12):02d}"
216
+ filters = [
217
+ "dateAuthenticated?isNotNull",
218
+ f"annotations.accountingMonth>={start_month}",
219
+ ]
220
+
221
+ invoices = client.fetch_invoices(
222
+ is_sales=True,
223
+ limit=100,
224
+ filters=",".join(filters),
225
+ )
226
+
227
+ # Filter for invoices that are not sent to KSeF
228
+ to_send = [inv.id for inv in invoices if not inv.ksef_status or inv.ksef_status == "N/A"]
229
+
230
+ if not to_send:
231
+ print("No unsent invoices found.")
232
+ return
233
+
234
+ print(f"Found {len(to_send)} unsent invoices.")
235
+ invoice_ids.extend(to_send)
236
+
237
+ if not invoice_ids:
238
+ print("No invoice IDs provided and --all not specified.", file=sys.stderr)
239
+ sys.exit(1)
240
+
241
+ print(f"Sending {len(invoice_ids)} invoices to KSeF...")
242
+ client.send_to_ksef(invoice_ids)
243
+ print("Successfully initiated sending to KSeF.")
244
+ except ScanyeError as e:
245
+ print(f"Error: {e}", file=sys.stderr)
246
+ sys.exit(1)
247
+ finally:
248
+ persist_token(config, client)
249
+
250
+
251
+ def handle_invoices_download(args: argparse.Namespace) -> None:
252
+ config = load_config()
253
+ require_credentials(config)
254
+
255
+ if args.invoice_ids and (args.month or args.filter):
256
+ print("Cannot combine specific invoice IDs with --month/--filter.", file=sys.stderr)
257
+ sys.exit(1)
258
+
259
+ is_sales = args.type == "sales"
260
+ client = build_client(config, args.debug)
261
+
262
+ try:
263
+ if args.invoice_ids:
264
+ invoice_ids = args.invoice_ids
265
+ else:
266
+ filters = build_month_filters(args.month, args.filter)
267
+ invoices = client.fetch_invoices(
268
+ is_sales=is_sales,
269
+ limit=args.limit,
270
+ filters=",".join(filters),
271
+ )
272
+ invoice_ids = [inv.id for inv in invoices]
273
+
274
+ if not invoice_ids:
275
+ print("No invoices found to download.")
276
+ return
277
+
278
+ output_dir = Path(args.output)
279
+ output_dir.mkdir(parents=True, exist_ok=True)
280
+
281
+ print(f"Downloading {len(invoice_ids)} invoice(s)...")
282
+ content, filename = client.fetch_printout(invoice_ids)
283
+
284
+ if zipfile.is_zipfile(io.BytesIO(content)):
285
+ with zipfile.ZipFile(io.BytesIO(content)) as zf:
286
+ names = zf.namelist()
287
+ zf.extractall(output_dir)
288
+ print(f"Saved {len(names)} file(s) to {output_dir}/")
289
+ else:
290
+ path = output_dir / Path(filename).name
291
+ path.write_bytes(content)
292
+ print(f"Saved {path}")
293
+ except ScanyeError as e:
294
+ print(f"Error: {e}", file=sys.stderr)
295
+ sys.exit(1)
296
+ finally:
297
+ persist_token(config, client)
298
+
299
+
300
+ def main() -> None:
301
+ parser = argparse.ArgumentParser(description="Scanye CLI tool")
302
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
303
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
304
+
305
+ # Login command
306
+ login_parser = subparsers.add_parser("login", help="Login to Scanye")
307
+ login_parser.add_argument("--email", required=True, help="Your Scanye email")
308
+
309
+ # Invoices command
310
+ invoice_parser = subparsers.add_parser("invoices", help="Invoice operations")
311
+ invoice_subparsers = invoice_parser.add_subparsers(dest="subcommand", help="Invoice subcommands")
312
+
313
+ list_parser = invoice_subparsers.add_parser("list", help="List invoices")
314
+ list_parser.add_argument("--type", choices=["sales", "purchase"], default="sales", help="Invoice type")
315
+ list_parser.add_argument("--limit", type=int, default=10, help="Limit number of invoices")
316
+ list_parser.add_argument("--unsent", action="store_true", help="List only unsent to KSeF")
317
+ list_parser.add_argument("--month", action="append", help="Month(s) to fetch (YYYY-MM), e.g. 2026-05")
318
+ list_parser.add_argument("--filter", help="Raw filter string for API")
319
+ list_parser.add_argument("-v", "--verbose", action="store_true", help="Show more details (NIP, email)")
320
+
321
+ paid_parser = invoice_subparsers.add_parser("mark-paid", help="Mark invoices as paid")
322
+ paid_parser.add_argument("invoice_ids", nargs="+", help="Invoice IDs to mark as paid")
323
+ paid_parser.add_argument("--date", help="Transfer order date (YYYY-MM-DD), defaults to today")
324
+
325
+ unpaid_parser = invoice_subparsers.add_parser("mark-unpaid", help="Mark invoices as unpaid")
326
+ unpaid_parser.add_argument("invoice_ids", nargs="+", help="Invoice IDs to mark as unpaid")
327
+
328
+ ksef_parser = invoice_subparsers.add_parser("send-ksef", help="Send invoices to KSeF")
329
+ ksef_parser.add_argument("invoice_ids", nargs="*", help="Specific invoice IDs to send")
330
+ ksef_parser.add_argument("--all", action="store_true", help="Automatically send all unsent sales invoices")
331
+
332
+ download_parser = invoice_subparsers.add_parser("download", help="Download invoices as PDF")
333
+ download_parser.add_argument(
334
+ "invoice_ids", nargs="*", help="Specific invoice IDs to download (omit to use --month/--filter instead)"
335
+ )
336
+ download_parser.add_argument("--type", choices=["sales", "purchase"], default="sales", help="Invoice type")
337
+ download_parser.add_argument("--month", action="append", help="Month(s) to fetch (YYYY-MM), e.g. 2026-07")
338
+ download_parser.add_argument("--filter", help="Raw filter string for API")
339
+ download_parser.add_argument("--limit", type=int, default=100, help="Max invoices to download when using filters")
340
+ download_parser.add_argument("-o", "--output", default=".", help="Output directory (default: current directory)")
341
+
342
+ args = parser.parse_args()
343
+
344
+ if args.command == "login":
345
+ handle_login(args)
346
+ elif args.command == "invoices":
347
+ if args.subcommand == "list":
348
+ handle_invoices_list(args)
349
+ elif args.subcommand == "mark-paid":
350
+ handle_invoices_mark_paid(args)
351
+ elif args.subcommand == "mark-unpaid":
352
+ handle_invoices_mark_unpaid(args)
353
+ elif args.subcommand == "send-ksef":
354
+ handle_invoices_send_ksef(args)
355
+ elif args.subcommand == "download":
356
+ handle_invoices_download(args)
357
+ else:
358
+ invoice_parser.print_help()
359
+ else:
360
+ parser.print_help()
361
+
362
+
363
+ if __name__ == "__main__":
364
+ main()