# Cetustek

Python SDK for [Cetustek](https://www.cetustek.com.tw/) Taiwan e-invoice API.

## Installation

```bash
pip install cetustek
```

## Quick Start

```python
from cetustek import Cetustek, CreateInvoiceInput, InvoiceItem

# Initialize the client
client = Cetustek(
    endpoint="https://invoice.cetustek.com.tw/InvoiceMultiWeb/InvoiceAPI",
    rent_id="YOUR_RENT_ID",
    site_code="YOUR_SITE_CODE",
    api_password="YOUR_API_PASSWORD",
)

# Create an invoice
result = client.createInvoice(CreateInvoiceInput(
    order_id="12345678",
    order_date="2026/01/15",
    buyer_identifier="12345678",
    buyer_name="Company Name",
    donate_mark="0",
    invoice_type="07",
    tax_type="1",
    pay_way="1",
    items=[
        InvoiceItem(
            production_code="PROD001",
            description="Product",
            quantity=1,
            unit_price=1000,
        )
    ],
))

# Access response as dataclass
print(f"Invoice Number: {result.invoice_number}")
print(f"Random Code: {result.random_code}")
print(f"Invoice Year: {result.invoice_year}")
```

## API Reference

### Cetustek Client

```python
from cetustek import Cetustek

client = Cetustek(
    endpoint="https://invoice.cetustek.com.tw/InvoiceMultiWeb/InvoiceAPI",
    rent_id="YOUR_RENT_ID",
    site_code="YOUR_SITE_CODE",
    api_password="YOUR_API_PASSWORD",
)
```

| Parameter | Description |
|-----------|-------------|
| `endpoint` | Cetustek API endpoint URL |
| `rent_id` | Your Cetustek rent ID (統一編號) |
| `site_code` | Your site code |
| `api_password` | Your API password |

### Create Invoice

Create a new e-invoice. Returns `CreateInvoiceResponse`.

```python
from cetustek import CreateInvoiceInput, InvoiceItem, CetustekAPIError

invoice_input = CreateInvoiceInput(
    order_id="12345678",              # Unique order ID
    order_date="2026/01/15",          # Format: yyyy/MM/dd
    donate_mark="0",                  # 0: No donation, 1: Donate, 2: Donate to specific org
    invoice_type="07",                # 07: B2B, 08: B2C
    tax_type="1",                     # 1: Taxable, 2: Zero-rated, 3: Tax-free, 9: Mixed
    pay_way="1",                      # Payment method code
    items=[
        InvoiceItem(
            production_code="PROD001",
            description="Product description",
            quantity=1,
            unit_price=1000,
            unit="件",                # Optional: unit name
        )
    ],
    # Optional fields
    buyer_identifier="12345678",      # Buyer's tax ID (統一編號)
    buyer_name="Company Name",
    buyer_address="Address",
    buyer_email="email@example.com",
    tax_rate=0.05,                    # Default: 0.05 (5%)
    carrier_type="3J0002",            # Carrier type code
    carrier_id1="/ABC1234",           # Carrier ID
    carrier_id2="/ABC1234",           # Carrier ID confirmation
    npoban="12345",                   # NPO code for donation
    remark="備註",                     # Remark
)

try:
    result = client.createInvoice(invoice_input)
    print(f"Invoice Number: {result.invoice_number}")  # e.g., "WP20260002"
    print(f"Random Code: {result.random_code}")        # e.g., "6827"
    print(f"Invoice Year: {result.invoice_year}")      # e.g., "2026"
except CetustekAPIError as e:
    print(f"Error: {e.code} - {e.message}")
```

### Query Invoice

Query an existing invoice by number and year. Returns `QueryInvoiceResponse`.

```python
from cetustek import QueryInvoiceInput

query_input = QueryInvoiceInput(
    invoice_number="AA12345678",      # 10-character invoice number
    invoice_year="2026",              # 4-digit year
)

result = client.queryInvoice(query_input)

# Access invoice details
print(f"Invoice Number: {result.invoice_number}")
print(f"Order ID: {result.order_id}")
print(f"Random Code: {result.random_code}")
print(f"Buyer Name: {result.buyer_name}")
print(f"Seller Name: {result.seller_name}")
print(f"Total Amount: {result.total_amount}")
print(f"Invoice Status: {result.invoice_status}")

# Access raw XML for additional parsing
print(result.raw_xml)
```

### Cancel Invoice

Cancel (void) an existing invoice. Returns `CancelInvoiceResponse`.

```python
from cetustek import CancelInvoiceInput

cancel_input = CancelInvoiceInput(
    invoice_number="AA12345678",
    invoice_year="2026",
    remark="Cancellation reason",
    return_tax_document_number=None,  # Optional: tax document number
)

# Standard cancellation (with validation)
result = client.cancelInvoice(cancel_input)

# Skip validation checks
result = client.cancelInvoice(cancel_input, no_check=True)

if result.success:
    print("Invoice cancelled successfully")
else:
    print(f"Failed with code: {result.code}")
```

## Response Models

### CreateInvoiceResponse

| Field | Type | Description |
|-------|------|-------------|
| `invoice_number` | str | 10-character invoice number (e.g., "WP20260002") |
| `random_code` | str | 4-digit random code (e.g., "6827") |
| `invoice_year` | str | Year extracted from invoice number (property) |

### QueryInvoiceResponse

| Field | Type | Description |
|-------|------|-------------|
| `invoice_number` | str | Invoice number |
| `invoice_date` | str | Invoice date |
| `invoice_time` | str | Invoice time |
| `order_id` | str | Order ID |
| `random_code` | str | Random code |
| `buyer_identifier` | str | Buyer tax ID |
| `buyer_name` | str | Buyer name |
| `seller_identifier` | str | Seller tax ID |
| `seller_name` | str | Seller name |
| `invoice_status` | str | Invoice status |
| `donate_mark` | str | Donation mark |
| `carrier_type` | str | Carrier type |
| `carrier_id` | str | Carrier ID |
| `npoban` | str | NPO code |
| `tax_type` | str | Tax type |
| `sales_amount` | float | Sales amount |
| `tax_amount` | float | Tax amount |
| `total_amount` | float | Total amount |
| `raw_xml` | str | Original XML response |

### CancelInvoiceResponse

| Field | Type | Description |
|-------|------|-------------|
| `success` | bool | Whether cancellation succeeded |
| `code` | str | Response code ("C0" for success) |
| `message` | str | Error message (if failed) |

## Input Models

### InvoiceItem

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `production_code` | str | Yes | Product code |
| `description` | str | Yes | Product description |
| `quantity` | float | Yes | Quantity |
| `unit_price` | float | Yes | Unit price |
| `unit` | str | No | Unit name (e.g., "件", "個") |

### CreateInvoiceInput

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `order_id` | str | Yes | Unique order ID |
| `order_date` | str | Yes | Order date (yyyy/MM/dd) |
| `donate_mark` | str | Yes | Donation mark: 0/1/2 |
| `invoice_type` | str | Yes | Invoice type: 07 (B2B) / 08 (B2C) |
| `pay_way` | str | Yes | Payment method code |
| `tax_type` | str | Yes | Tax type: 1/2/3/4/5/9 |
| `items` | List[InvoiceItem] | Yes | Invoice items |
| `buyer_identifier` | str | No | Buyer tax ID |
| `buyer_name` | str | No | Buyer name |
| `buyer_address` | str | No | Buyer address |
| `buyer_email` | str | No | Buyer email |
| `tax_rate` | float | No | Tax rate (default: 0.05) |
| `carrier_type` | str | No | Carrier type code |
| `carrier_id1` | str | No | Carrier ID |
| `carrier_id2` | str | No | Carrier ID confirmation |
| `npoban` | str | No | NPO code for donation |
| `remark` | str | No | Remark |

## Exception Handling

```python
from cetustek import CetustekError, CetustekAPIError

try:
    result = client.createInvoice(invoice_input)
except CetustekAPIError as e:
    # API returned an error response
    print(f"API Error Code: {e.code}")
    print(f"API Error Message: {e.message}")
except CetustekError as e:
    # General SDK error (e.g., invalid response format)
    print(f"SDK Error: {e}")
except Exception as e:
    # Network or other errors
    print(f"Error: {e}")
```

## Example

See [example.py](example.py) for a complete working example.

## Development

```bash
# Clone the repository
git clone https://github.com/vincelee888/cetustek.git
cd cetustek

# Install in development mode
pip install -e ".[dev]"

# Run tests
pytest
```

## License

MIT License - see [LICENSE](LICENSE) for details.
