cetustek 0.1.0__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of cetustek might be problematic. Click here for more details.
- cetustek/__init__.py +16 -2
- cetustek/client.py +147 -9
- cetustek/models.py +52 -0
- cetustek-0.2.0.dist-info/METADATA +315 -0
- cetustek-0.2.0.dist-info/RECORD +8 -0
- cetustek-0.1.0.dist-info/METADATA +0 -111
- cetustek-0.1.0.dist-info/RECORD +0 -8
- {cetustek-0.1.0.dist-info → cetustek-0.2.0.dist-info}/WHEEL +0 -0
- {cetustek-0.1.0.dist-info → cetustek-0.2.0.dist-info}/licenses/LICENSE +0 -0
cetustek/__init__.py
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
"""Cetustek Taiwan e-invoice SDK."""
|
|
2
2
|
|
|
3
|
-
from cetustek.client import Cetustek
|
|
3
|
+
from cetustek.client import Cetustek, CetustekError, CetustekAPIError
|
|
4
4
|
from cetustek.models import (
|
|
5
|
+
# Input models
|
|
5
6
|
CancelInvoiceInput,
|
|
6
7
|
CreateInvoiceInput,
|
|
7
8
|
InvoiceItem,
|
|
8
9
|
QueryInvoiceInput,
|
|
10
|
+
# Response models
|
|
11
|
+
CancelInvoiceResponse,
|
|
12
|
+
CreateInvoiceResponse,
|
|
13
|
+
QueryInvoiceResponse,
|
|
9
14
|
)
|
|
10
15
|
|
|
11
16
|
__all__ = [
|
|
17
|
+
# Client
|
|
12
18
|
"Cetustek",
|
|
19
|
+
# Exceptions
|
|
20
|
+
"CetustekError",
|
|
21
|
+
"CetustekAPIError",
|
|
22
|
+
# Input models
|
|
13
23
|
"CreateInvoiceInput",
|
|
14
24
|
"CancelInvoiceInput",
|
|
15
25
|
"QueryInvoiceInput",
|
|
16
26
|
"InvoiceItem",
|
|
27
|
+
# Response models
|
|
28
|
+
"CreateInvoiceResponse",
|
|
29
|
+
"CancelInvoiceResponse",
|
|
30
|
+
"QueryInvoiceResponse",
|
|
17
31
|
]
|
|
18
32
|
|
|
19
|
-
__version__ = "0.
|
|
33
|
+
__version__ = "0.2.0"
|
cetustek/client.py
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
|
-
import
|
|
1
|
+
import html
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
2
4
|
from xml.sax.saxutils import escape
|
|
3
5
|
|
|
6
|
+
import requests
|
|
7
|
+
|
|
4
8
|
from cetustek.models import (
|
|
5
9
|
CreateInvoiceInput,
|
|
10
|
+
CreateInvoiceResponse,
|
|
6
11
|
CancelInvoiceInput,
|
|
12
|
+
CancelInvoiceResponse,
|
|
7
13
|
QueryInvoiceInput,
|
|
14
|
+
QueryInvoiceResponse,
|
|
8
15
|
)
|
|
9
16
|
|
|
10
17
|
|
|
18
|
+
class CetustekError(Exception):
|
|
19
|
+
"""Base exception for Cetustek API errors."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CetustekAPIError(CetustekError):
|
|
24
|
+
"""API returned an error response."""
|
|
25
|
+
def __init__(self, code: str, message: Optional[str] = None):
|
|
26
|
+
self.code = code
|
|
27
|
+
self.message = message
|
|
28
|
+
super().__init__(f"API Error: {code}" + (f" - {message}" if message else ""))
|
|
29
|
+
|
|
30
|
+
|
|
11
31
|
class Cetustek:
|
|
12
32
|
def __init__(self, *, endpoint: str, rent_id: str, api_password: str, site_code: str):
|
|
13
33
|
self.endpoint = endpoint
|
|
@@ -18,7 +38,16 @@ class Cetustek:
|
|
|
18
38
|
# API methods
|
|
19
39
|
# -------------------------
|
|
20
40
|
|
|
21
|
-
def createInvoice(self, data: CreateInvoiceInput) ->
|
|
41
|
+
def createInvoice(self, data: CreateInvoiceInput) -> CreateInvoiceResponse:
|
|
42
|
+
"""
|
|
43
|
+
Create a new e-invoice.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
CreateInvoiceResponse with invoice_number and random_code.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
CetustekAPIError: If the API returns an error.
|
|
50
|
+
"""
|
|
22
51
|
items_xml = ""
|
|
23
52
|
for item in data.items:
|
|
24
53
|
items_xml += f"""
|
|
@@ -61,10 +90,20 @@ class Cetustek:
|
|
|
61
90
|
<source>{self.source}</source>
|
|
62
91
|
"""
|
|
63
92
|
|
|
64
|
-
|
|
93
|
+
response_xml = self._post_soap(self._wrap_soap("CreateInvoiceV3", inner))
|
|
94
|
+
return self._parse_create_response(response_xml)
|
|
95
|
+
|
|
96
|
+
def cancelInvoice(self, data: CancelInvoiceInput, no_check: bool = False) -> CancelInvoiceResponse:
|
|
97
|
+
"""
|
|
98
|
+
Cancel (void) an existing invoice.
|
|
65
99
|
|
|
100
|
+
Args:
|
|
101
|
+
data: CancelInvoiceInput with invoice details.
|
|
102
|
+
no_check: If True, skip validation checks.
|
|
66
103
|
|
|
67
|
-
|
|
104
|
+
Returns:
|
|
105
|
+
CancelInvoiceResponse with success status and code.
|
|
106
|
+
"""
|
|
68
107
|
invoice_xml = f"""
|
|
69
108
|
<Invoice XSDVersion="2.8">
|
|
70
109
|
<InvoiceNumber>{data.invoice_number}</InvoiceNumber>
|
|
@@ -81,13 +120,18 @@ class Cetustek:
|
|
|
81
120
|
"""
|
|
82
121
|
|
|
83
122
|
action = "CancelInvoiceNoCheck" if no_check else "CancelInvoice"
|
|
123
|
+
response_xml = self._post_soap(self._wrap_soap(action, inner))
|
|
124
|
+
return self._parse_cancel_response(response_xml)
|
|
84
125
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def queryInvoice(self, data: QueryInvoiceInput) -> str:
|
|
126
|
+
def queryInvoice(self, data: QueryInvoiceInput) -> QueryInvoiceResponse:
|
|
88
127
|
"""
|
|
89
128
|
Query invoice by invoice number and year.
|
|
90
|
-
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
QueryInvoiceResponse with invoice details.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
CetustekAPIError: If the API returns an error.
|
|
91
135
|
"""
|
|
92
136
|
inner = f"""
|
|
93
137
|
<invoicenumber>{data.invoice_number}</invoicenumber>
|
|
@@ -95,12 +139,106 @@ class Cetustek:
|
|
|
95
139
|
<rentid>{self.rent_id}</rentid>
|
|
96
140
|
<source>{self.source}</source>
|
|
97
141
|
"""
|
|
98
|
-
|
|
142
|
+
response_xml = self._post_soap(self._wrap_soap("QueryInvoice", inner))
|
|
143
|
+
return self._parse_query_response(response_xml, data.invoice_number)
|
|
99
144
|
|
|
145
|
+
# -------------------------
|
|
146
|
+
# Response parsers
|
|
147
|
+
# -------------------------
|
|
148
|
+
|
|
149
|
+
def _parse_create_response(self, response_xml: str) -> CreateInvoiceResponse:
|
|
150
|
+
"""Parse createInvoice response XML."""
|
|
151
|
+
return_value = self._extract_return_value(response_xml)
|
|
152
|
+
|
|
153
|
+
if ";" not in return_value:
|
|
154
|
+
raise CetustekAPIError(return_value)
|
|
155
|
+
|
|
156
|
+
parts = return_value.split(";")
|
|
157
|
+
if len(parts) != 2:
|
|
158
|
+
raise CetustekAPIError(return_value, "Unexpected response format")
|
|
159
|
+
|
|
160
|
+
invoice_number, random_code = parts
|
|
161
|
+
return CreateInvoiceResponse(
|
|
162
|
+
invoice_number=invoice_number,
|
|
163
|
+
random_code=random_code,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _parse_cancel_response(self, response_xml: str) -> CancelInvoiceResponse:
|
|
167
|
+
"""Parse cancelInvoice response XML."""
|
|
168
|
+
return_value = self._extract_return_value(response_xml)
|
|
169
|
+
|
|
170
|
+
success = return_value == "C0"
|
|
171
|
+
return CancelInvoiceResponse(
|
|
172
|
+
success=success,
|
|
173
|
+
code=return_value,
|
|
174
|
+
message=None if success else return_value,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _parse_query_response(self, response_xml: str, invoice_number: str) -> QueryInvoiceResponse:
|
|
178
|
+
"""Parse queryInvoice response XML."""
|
|
179
|
+
return_value = self._extract_return_value(response_xml, dotall=True)
|
|
180
|
+
|
|
181
|
+
# Unescape HTML entities
|
|
182
|
+
invoice_xml = html.unescape(return_value)
|
|
183
|
+
|
|
184
|
+
# Check if response is an error (not XML)
|
|
185
|
+
if not invoice_xml.strip().startswith("<"):
|
|
186
|
+
raise CetustekAPIError(invoice_xml)
|
|
187
|
+
|
|
188
|
+
# Parse invoice fields from XML
|
|
189
|
+
return QueryInvoiceResponse(
|
|
190
|
+
invoice_number=invoice_number,
|
|
191
|
+
invoice_date=self._extract_xml_value(invoice_xml, "InvoiceDate"),
|
|
192
|
+
invoice_time=self._extract_xml_value(invoice_xml, "InvoiceTime"),
|
|
193
|
+
order_id=self._extract_xml_value(invoice_xml, "OrderID"),
|
|
194
|
+
random_code=self._extract_xml_value(invoice_xml, "RandomNumber"),
|
|
195
|
+
buyer_identifier=self._extract_xml_value(invoice_xml, "BuyerIdentifier"),
|
|
196
|
+
buyer_name=self._extract_xml_value(invoice_xml, "BuyerName"),
|
|
197
|
+
seller_identifier=self._extract_xml_value(invoice_xml, "SellerIdentifier"),
|
|
198
|
+
seller_name=self._extract_xml_value(invoice_xml, "SellerName"),
|
|
199
|
+
invoice_status=self._extract_xml_value(invoice_xml, "InvoiceStatus"),
|
|
200
|
+
donate_mark=self._extract_xml_value(invoice_xml, "DonateMark"),
|
|
201
|
+
carrier_type=self._extract_xml_value(invoice_xml, "CarrierType"),
|
|
202
|
+
carrier_id=self._extract_xml_value(invoice_xml, "CarrierId1"),
|
|
203
|
+
npoban=self._extract_xml_value(invoice_xml, "NPOBAN"),
|
|
204
|
+
tax_type=self._extract_xml_value(invoice_xml, "TaxType"),
|
|
205
|
+
sales_amount=self._extract_xml_float(invoice_xml, "SalesAmount"),
|
|
206
|
+
tax_amount=self._extract_xml_float(invoice_xml, "TaxAmount"),
|
|
207
|
+
total_amount=self._extract_xml_float(invoice_xml, "TotalAmount"),
|
|
208
|
+
raw_xml=invoice_xml,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def _extract_return_value(self, response_xml: str, dotall: bool = False) -> str:
|
|
212
|
+
"""Extract the <return> value from SOAP response."""
|
|
213
|
+
flags = re.DOTALL if dotall else 0
|
|
214
|
+
match = re.search(r"<return>(.*?)</return>", response_xml, flags)
|
|
215
|
+
if not match:
|
|
216
|
+
raise CetustekError(f"Invalid response: missing <return> tag")
|
|
217
|
+
return match.group(1).strip()
|
|
218
|
+
|
|
219
|
+
def _extract_xml_value(self, xml: str, tag: str) -> Optional[str]:
|
|
220
|
+
"""Extract value from XML tag (case-insensitive)."""
|
|
221
|
+
pattern = rf"<{tag}>(.*?)</{tag}>"
|
|
222
|
+
match = re.search(pattern, xml, re.IGNORECASE | re.DOTALL)
|
|
223
|
+
if match:
|
|
224
|
+
value = match.group(1).strip()
|
|
225
|
+
return value if value else None
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def _extract_xml_float(self, xml: str, tag: str) -> Optional[float]:
|
|
229
|
+
"""Extract float value from XML tag."""
|
|
230
|
+
value = self._extract_xml_value(xml, tag)
|
|
231
|
+
if value:
|
|
232
|
+
try:
|
|
233
|
+
return float(value)
|
|
234
|
+
except ValueError:
|
|
235
|
+
return None
|
|
236
|
+
return None
|
|
100
237
|
|
|
101
238
|
# -------------------------
|
|
102
239
|
# Internal helpers
|
|
103
240
|
# -------------------------
|
|
241
|
+
|
|
104
242
|
def _post_soap(self, body: str) -> str:
|
|
105
243
|
headers = {
|
|
106
244
|
"Content-Type": "text/xml; charset=utf-8",
|
cetustek/models.py
CHANGED
|
@@ -2,6 +2,10 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import Optional, List
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
# -------------------------
|
|
6
|
+
# Input Models
|
|
7
|
+
# -------------------------
|
|
8
|
+
|
|
5
9
|
@dataclass
|
|
6
10
|
class InvoiceItem:
|
|
7
11
|
production_code: str # ProductionCode
|
|
@@ -55,3 +59,51 @@ class CancelInvoiceInput:
|
|
|
55
59
|
class QueryInvoiceInput:
|
|
56
60
|
invoice_number: str
|
|
57
61
|
invoice_year: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# -------------------------
|
|
65
|
+
# Response Models
|
|
66
|
+
# -------------------------
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class CreateInvoiceResponse:
|
|
70
|
+
"""Response from createInvoice API."""
|
|
71
|
+
invoice_number: str # 10-character invoice number (e.g., "WP20260002")
|
|
72
|
+
random_code: str # 4-digit random code (e.g., "6827")
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def invoice_year(self) -> str:
|
|
76
|
+
"""Extract year from invoice number (positions 2-5)."""
|
|
77
|
+
return self.invoice_number[2:6]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class QueryInvoiceResponse:
|
|
82
|
+
"""Response from queryInvoice API."""
|
|
83
|
+
invoice_number: str
|
|
84
|
+
invoice_date: Optional[str] = None
|
|
85
|
+
invoice_time: Optional[str] = None
|
|
86
|
+
order_id: Optional[str] = None
|
|
87
|
+
random_code: Optional[str] = None
|
|
88
|
+
buyer_identifier: Optional[str] = None
|
|
89
|
+
buyer_name: Optional[str] = None
|
|
90
|
+
seller_identifier: Optional[str] = None
|
|
91
|
+
seller_name: Optional[str] = None
|
|
92
|
+
invoice_status: Optional[str] = None
|
|
93
|
+
donate_mark: Optional[str] = None
|
|
94
|
+
carrier_type: Optional[str] = None
|
|
95
|
+
carrier_id: Optional[str] = None
|
|
96
|
+
npoban: Optional[str] = None
|
|
97
|
+
tax_type: Optional[str] = None
|
|
98
|
+
sales_amount: Optional[float] = None
|
|
99
|
+
tax_amount: Optional[float] = None
|
|
100
|
+
total_amount: Optional[float] = None
|
|
101
|
+
raw_xml: Optional[str] = None # Original XML for additional parsing
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class CancelInvoiceResponse:
|
|
106
|
+
"""Response from cancelInvoice API."""
|
|
107
|
+
success: bool
|
|
108
|
+
code: str # "C0" for success, error code otherwise
|
|
109
|
+
message: Optional[str] = None
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cetustek
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for Cetustek Taiwan e-invoice API
|
|
5
|
+
Project-URL: Homepage, https://github.com/vincelee888/cetustek
|
|
6
|
+
Project-URL: Repository, https://github.com/vincelee888/cetustek
|
|
7
|
+
Project-URL: Issues, https://github.com/vincelee888/cetustek/issues
|
|
8
|
+
Author: Vince Lee
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cetustek,e-invoice,einvoice,invoice,taiwan
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: requests>=2.25.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Cetustek
|
|
32
|
+
|
|
33
|
+
Python SDK for [Cetustek](https://www.cetustek.com.tw/) Taiwan e-invoice API.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install cetustek
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from cetustek import Cetustek, CreateInvoiceInput, InvoiceItem
|
|
45
|
+
|
|
46
|
+
# Initialize the client
|
|
47
|
+
client = Cetustek(
|
|
48
|
+
endpoint="https://invoice.cetustek.com.tw/InvoiceMultiWeb/InvoiceAPI",
|
|
49
|
+
rent_id="YOUR_RENT_ID",
|
|
50
|
+
site_code="YOUR_SITE_CODE",
|
|
51
|
+
api_password="YOUR_API_PASSWORD",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Create an invoice
|
|
55
|
+
result = client.createInvoice(CreateInvoiceInput(
|
|
56
|
+
order_id="12345678",
|
|
57
|
+
order_date="2026/01/15",
|
|
58
|
+
buyer_identifier="12345678",
|
|
59
|
+
buyer_name="Company Name",
|
|
60
|
+
donate_mark="0",
|
|
61
|
+
invoice_type="07",
|
|
62
|
+
tax_type="1",
|
|
63
|
+
pay_way="1",
|
|
64
|
+
items=[
|
|
65
|
+
InvoiceItem(
|
|
66
|
+
production_code="PROD001",
|
|
67
|
+
description="Product",
|
|
68
|
+
quantity=1,
|
|
69
|
+
unit_price=1000,
|
|
70
|
+
)
|
|
71
|
+
],
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
# Access response as dataclass
|
|
75
|
+
print(f"Invoice Number: {result.invoice_number}")
|
|
76
|
+
print(f"Random Code: {result.random_code}")
|
|
77
|
+
print(f"Invoice Year: {result.invoice_year}")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### Cetustek Client
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from cetustek import Cetustek
|
|
86
|
+
|
|
87
|
+
client = Cetustek(
|
|
88
|
+
endpoint="https://invoice.cetustek.com.tw/InvoiceMultiWeb/InvoiceAPI",
|
|
89
|
+
rent_id="YOUR_RENT_ID",
|
|
90
|
+
site_code="YOUR_SITE_CODE",
|
|
91
|
+
api_password="YOUR_API_PASSWORD",
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Parameter | Description |
|
|
96
|
+
|-----------|-------------|
|
|
97
|
+
| `endpoint` | Cetustek API endpoint URL |
|
|
98
|
+
| `rent_id` | Your Cetustek rent ID (統一編號) |
|
|
99
|
+
| `site_code` | Your site code |
|
|
100
|
+
| `api_password` | Your API password |
|
|
101
|
+
|
|
102
|
+
### Create Invoice
|
|
103
|
+
|
|
104
|
+
Create a new e-invoice. Returns `CreateInvoiceResponse`.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from cetustek import CreateInvoiceInput, InvoiceItem, CetustekAPIError
|
|
108
|
+
|
|
109
|
+
invoice_input = CreateInvoiceInput(
|
|
110
|
+
order_id="12345678", # Unique order ID
|
|
111
|
+
order_date="2026/01/15", # Format: yyyy/MM/dd
|
|
112
|
+
donate_mark="0", # 0: No donation, 1: Donate, 2: Donate to specific org
|
|
113
|
+
invoice_type="07", # 07: B2B, 08: B2C
|
|
114
|
+
tax_type="1", # 1: Taxable, 2: Zero-rated, 3: Tax-free, 9: Mixed
|
|
115
|
+
pay_way="1", # Payment method code
|
|
116
|
+
items=[
|
|
117
|
+
InvoiceItem(
|
|
118
|
+
production_code="PROD001",
|
|
119
|
+
description="Product description",
|
|
120
|
+
quantity=1,
|
|
121
|
+
unit_price=1000,
|
|
122
|
+
unit="件", # Optional: unit name
|
|
123
|
+
)
|
|
124
|
+
],
|
|
125
|
+
# Optional fields
|
|
126
|
+
buyer_identifier="12345678", # Buyer's tax ID (統一編號)
|
|
127
|
+
buyer_name="Company Name",
|
|
128
|
+
buyer_address="Address",
|
|
129
|
+
buyer_email="email@example.com",
|
|
130
|
+
tax_rate=0.05, # Default: 0.05 (5%)
|
|
131
|
+
carrier_type="3J0002", # Carrier type code
|
|
132
|
+
carrier_id1="/ABC1234", # Carrier ID
|
|
133
|
+
carrier_id2="/ABC1234", # Carrier ID confirmation
|
|
134
|
+
npoban="12345", # NPO code for donation
|
|
135
|
+
remark="備註", # Remark
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
result = client.createInvoice(invoice_input)
|
|
140
|
+
print(f"Invoice Number: {result.invoice_number}") # e.g., "WP20260002"
|
|
141
|
+
print(f"Random Code: {result.random_code}") # e.g., "6827"
|
|
142
|
+
print(f"Invoice Year: {result.invoice_year}") # e.g., "2026"
|
|
143
|
+
except CetustekAPIError as e:
|
|
144
|
+
print(f"Error: {e.code} - {e.message}")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Query Invoice
|
|
148
|
+
|
|
149
|
+
Query an existing invoice by number and year. Returns `QueryInvoiceResponse`.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from cetustek import QueryInvoiceInput
|
|
153
|
+
|
|
154
|
+
query_input = QueryInvoiceInput(
|
|
155
|
+
invoice_number="AA12345678", # 10-character invoice number
|
|
156
|
+
invoice_year="2026", # 4-digit year
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
result = client.queryInvoice(query_input)
|
|
160
|
+
|
|
161
|
+
# Access invoice details
|
|
162
|
+
print(f"Invoice Number: {result.invoice_number}")
|
|
163
|
+
print(f"Order ID: {result.order_id}")
|
|
164
|
+
print(f"Random Code: {result.random_code}")
|
|
165
|
+
print(f"Buyer Name: {result.buyer_name}")
|
|
166
|
+
print(f"Seller Name: {result.seller_name}")
|
|
167
|
+
print(f"Total Amount: {result.total_amount}")
|
|
168
|
+
print(f"Invoice Status: {result.invoice_status}")
|
|
169
|
+
|
|
170
|
+
# Access raw XML for additional parsing
|
|
171
|
+
print(result.raw_xml)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Cancel Invoice
|
|
175
|
+
|
|
176
|
+
Cancel (void) an existing invoice. Returns `CancelInvoiceResponse`.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from cetustek import CancelInvoiceInput
|
|
180
|
+
|
|
181
|
+
cancel_input = CancelInvoiceInput(
|
|
182
|
+
invoice_number="AA12345678",
|
|
183
|
+
invoice_year="2026",
|
|
184
|
+
remark="Cancellation reason",
|
|
185
|
+
return_tax_document_number=None, # Optional: tax document number
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Standard cancellation (with validation)
|
|
189
|
+
result = client.cancelInvoice(cancel_input)
|
|
190
|
+
|
|
191
|
+
# Skip validation checks
|
|
192
|
+
result = client.cancelInvoice(cancel_input, no_check=True)
|
|
193
|
+
|
|
194
|
+
if result.success:
|
|
195
|
+
print("Invoice cancelled successfully")
|
|
196
|
+
else:
|
|
197
|
+
print(f"Failed with code: {result.code}")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Response Models
|
|
201
|
+
|
|
202
|
+
### CreateInvoiceResponse
|
|
203
|
+
|
|
204
|
+
| Field | Type | Description |
|
|
205
|
+
|-------|------|-------------|
|
|
206
|
+
| `invoice_number` | str | 10-character invoice number (e.g., "WP20260002") |
|
|
207
|
+
| `random_code` | str | 4-digit random code (e.g., "6827") |
|
|
208
|
+
| `invoice_year` | str | Year extracted from invoice number (property) |
|
|
209
|
+
|
|
210
|
+
### QueryInvoiceResponse
|
|
211
|
+
|
|
212
|
+
| Field | Type | Description |
|
|
213
|
+
|-------|------|-------------|
|
|
214
|
+
| `invoice_number` | str | Invoice number |
|
|
215
|
+
| `invoice_date` | str | Invoice date |
|
|
216
|
+
| `invoice_time` | str | Invoice time |
|
|
217
|
+
| `order_id` | str | Order ID |
|
|
218
|
+
| `random_code` | str | Random code |
|
|
219
|
+
| `buyer_identifier` | str | Buyer tax ID |
|
|
220
|
+
| `buyer_name` | str | Buyer name |
|
|
221
|
+
| `seller_identifier` | str | Seller tax ID |
|
|
222
|
+
| `seller_name` | str | Seller name |
|
|
223
|
+
| `invoice_status` | str | Invoice status |
|
|
224
|
+
| `donate_mark` | str | Donation mark |
|
|
225
|
+
| `carrier_type` | str | Carrier type |
|
|
226
|
+
| `carrier_id` | str | Carrier ID |
|
|
227
|
+
| `npoban` | str | NPO code |
|
|
228
|
+
| `tax_type` | str | Tax type |
|
|
229
|
+
| `sales_amount` | float | Sales amount |
|
|
230
|
+
| `tax_amount` | float | Tax amount |
|
|
231
|
+
| `total_amount` | float | Total amount |
|
|
232
|
+
| `raw_xml` | str | Original XML response |
|
|
233
|
+
|
|
234
|
+
### CancelInvoiceResponse
|
|
235
|
+
|
|
236
|
+
| Field | Type | Description |
|
|
237
|
+
|-------|------|-------------|
|
|
238
|
+
| `success` | bool | Whether cancellation succeeded |
|
|
239
|
+
| `code` | str | Response code ("C0" for success) |
|
|
240
|
+
| `message` | str | Error message (if failed) |
|
|
241
|
+
|
|
242
|
+
## Input Models
|
|
243
|
+
|
|
244
|
+
### InvoiceItem
|
|
245
|
+
|
|
246
|
+
| Field | Type | Required | Description |
|
|
247
|
+
|-------|------|----------|-------------|
|
|
248
|
+
| `production_code` | str | Yes | Product code |
|
|
249
|
+
| `description` | str | Yes | Product description |
|
|
250
|
+
| `quantity` | float | Yes | Quantity |
|
|
251
|
+
| `unit_price` | float | Yes | Unit price |
|
|
252
|
+
| `unit` | str | No | Unit name (e.g., "件", "個") |
|
|
253
|
+
|
|
254
|
+
### CreateInvoiceInput
|
|
255
|
+
|
|
256
|
+
| Field | Type | Required | Description |
|
|
257
|
+
|-------|------|----------|-------------|
|
|
258
|
+
| `order_id` | str | Yes | Unique order ID |
|
|
259
|
+
| `order_date` | str | Yes | Order date (yyyy/MM/dd) |
|
|
260
|
+
| `donate_mark` | str | Yes | Donation mark: 0/1/2 |
|
|
261
|
+
| `invoice_type` | str | Yes | Invoice type: 07 (B2B) / 08 (B2C) |
|
|
262
|
+
| `pay_way` | str | Yes | Payment method code |
|
|
263
|
+
| `tax_type` | str | Yes | Tax type: 1/2/3/4/5/9 |
|
|
264
|
+
| `items` | List[InvoiceItem] | Yes | Invoice items |
|
|
265
|
+
| `buyer_identifier` | str | No | Buyer tax ID |
|
|
266
|
+
| `buyer_name` | str | No | Buyer name |
|
|
267
|
+
| `buyer_address` | str | No | Buyer address |
|
|
268
|
+
| `buyer_email` | str | No | Buyer email |
|
|
269
|
+
| `tax_rate` | float | No | Tax rate (default: 0.05) |
|
|
270
|
+
| `carrier_type` | str | No | Carrier type code |
|
|
271
|
+
| `carrier_id1` | str | No | Carrier ID |
|
|
272
|
+
| `carrier_id2` | str | No | Carrier ID confirmation |
|
|
273
|
+
| `npoban` | str | No | NPO code for donation |
|
|
274
|
+
| `remark` | str | No | Remark |
|
|
275
|
+
|
|
276
|
+
## Exception Handling
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from cetustek import CetustekError, CetustekAPIError
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
result = client.createInvoice(invoice_input)
|
|
283
|
+
except CetustekAPIError as e:
|
|
284
|
+
# API returned an error response
|
|
285
|
+
print(f"API Error Code: {e.code}")
|
|
286
|
+
print(f"API Error Message: {e.message}")
|
|
287
|
+
except CetustekError as e:
|
|
288
|
+
# General SDK error (e.g., invalid response format)
|
|
289
|
+
print(f"SDK Error: {e}")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
# Network or other errors
|
|
292
|
+
print(f"Error: {e}")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Example
|
|
296
|
+
|
|
297
|
+
See [example.py](example.py) for a complete working example.
|
|
298
|
+
|
|
299
|
+
## Development
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Clone the repository
|
|
303
|
+
git clone https://github.com/vincelee888/cetustek.git
|
|
304
|
+
cd cetustek
|
|
305
|
+
|
|
306
|
+
# Install in development mode
|
|
307
|
+
pip install -e ".[dev]"
|
|
308
|
+
|
|
309
|
+
# Run tests
|
|
310
|
+
pytest
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cetustek/__init__.py,sha256=IWKld6QIm0OW-Dx4MTuY8H4n3hvjnuXMS2521x0d1hE,701
|
|
2
|
+
cetustek/client.py,sha256=S56MgkTvXJV0yfRDF98m9Q5hinhLtwenFdqNlv8MWUw,9399
|
|
3
|
+
cetustek/models.py,sha256=ZXm8-EonV2bB2R7M3FHVXRbl6bg0si7a7JlL1JKXuIo,3119
|
|
4
|
+
cetustek/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
cetustek-0.2.0.dist-info/METADATA,sha256=GzAAKfR72vNCdlymyvtYpCf_gKwCBniri6_9tKmLMAw,9426
|
|
6
|
+
cetustek-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
cetustek-0.2.0.dist-info/licenses/LICENSE,sha256=3Zyxxlid2vbpfK2L93sfxat307kl3auQ32KXigeRGiE,1066
|
|
8
|
+
cetustek-0.2.0.dist-info/RECORD,,
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: cetustek
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Python SDK for Cetustek Taiwan e-invoice API
|
|
5
|
-
Project-URL: Homepage, https://github.com/vincelee888/cetustek
|
|
6
|
-
Project-URL: Repository, https://github.com/vincelee888/cetustek
|
|
7
|
-
Project-URL: Issues, https://github.com/vincelee888/cetustek/issues
|
|
8
|
-
Author: Vince Lee
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: cetustek,e-invoice,einvoice,invoice,taiwan
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
-
Classifier: Topic :: Office/Business :: Financial
|
|
23
|
-
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.9
|
|
25
|
-
Requires-Dist: requests>=2.25.0
|
|
26
|
-
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
28
|
-
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
30
|
-
|
|
31
|
-
# Cetustek
|
|
32
|
-
|
|
33
|
-
Python SDK for Cetustek Taiwan e-invoice API.
|
|
34
|
-
|
|
35
|
-
## Installation
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
pip install cetustek
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Usage
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
from cetustek import Cetustek, CreateInvoiceInput, InvoiceItem
|
|
45
|
-
|
|
46
|
-
# Initialize the client
|
|
47
|
-
client = Cetustek(
|
|
48
|
-
endpoint="https://invoice.cetustek.com.tw/InvoiceMultiWeb/InvoiceAPI",
|
|
49
|
-
rent_id="YOUR_RENT_ID",
|
|
50
|
-
site_code="YOUR_SITE_CODE",
|
|
51
|
-
api_password="YOUR_API_PASSWORD",
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
# Create an invoice
|
|
55
|
-
invoice_input = CreateInvoiceInput(
|
|
56
|
-
order_id="12345678",
|
|
57
|
-
order_date="2026/01/15",
|
|
58
|
-
buyer_identifier="12345678",
|
|
59
|
-
buyer_name="Company Name",
|
|
60
|
-
buyer_email="email@example.com",
|
|
61
|
-
donate_mark="0",
|
|
62
|
-
invoice_type="07",
|
|
63
|
-
tax_type="1",
|
|
64
|
-
tax_rate=0.05,
|
|
65
|
-
pay_way="1",
|
|
66
|
-
items=[
|
|
67
|
-
InvoiceItem(
|
|
68
|
-
production_code="PROD001",
|
|
69
|
-
description="Product description",
|
|
70
|
-
quantity=1,
|
|
71
|
-
unit_price=1000,
|
|
72
|
-
unit="件",
|
|
73
|
-
)
|
|
74
|
-
],
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
result = client.createInvoice(invoice_input)
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Query an Invoice
|
|
81
|
-
|
|
82
|
-
```python
|
|
83
|
-
from cetustek import QueryInvoiceInput
|
|
84
|
-
|
|
85
|
-
query_input = QueryInvoiceInput(
|
|
86
|
-
invoice_number="AA12345678",
|
|
87
|
-
invoice_year="2026",
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
result = client.queryInvoice(query_input)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Cancel an Invoice
|
|
94
|
-
|
|
95
|
-
```python
|
|
96
|
-
from cetustek import CancelInvoiceInput
|
|
97
|
-
|
|
98
|
-
cancel_input = CancelInvoiceInput(
|
|
99
|
-
invoice_number="AA12345678",
|
|
100
|
-
invoice_year="2026",
|
|
101
|
-
remark="Cancellation reason",
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
result = client.cancelInvoice(cancel_input)
|
|
105
|
-
# Or use no_check=True to skip validation
|
|
106
|
-
result = client.cancelInvoice(cancel_input, no_check=True)
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## License
|
|
110
|
-
|
|
111
|
-
MIT
|
cetustek-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
cetustek/__init__.py,sha256=IDmudJOsB1z-P6kdq3AXlAldzq5ycUrjiFViZmpZnYM,345
|
|
2
|
-
cetustek/client.py,sha256=dpY7SO4PudPe3O8hgHXvEk9iD5eExc260W2wrkeOxqw,3911
|
|
3
|
-
cetustek/models.py,sha256=Z0Pd9ujiF7fmkMMLHZgnxekvDZYdxE3IbxAiWjVrM3s,1565
|
|
4
|
-
cetustek/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
cetustek-0.1.0.dist-info/METADATA,sha256=5S3FSlwCAuxHiv493w1R7mJcm-ez8hdk5-fo8rG_VoU,2765
|
|
6
|
-
cetustek-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
cetustek-0.1.0.dist-info/licenses/LICENSE,sha256=3Zyxxlid2vbpfK2L93sfxat307kl3auQ32KXigeRGiE,1066
|
|
8
|
-
cetustek-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|