Metadata-Version: 2.4
Name: cashio-client
Version: 0.3.0
Summary: Python client for the Cashio Client API (transactions and purchase payments)
Author: Cashio
License-Expression: MIT
Project-URL: Documentation, https://pypi.org/project/cashio-client/
Keywords: cashio,api,client,sdk,accounting,petty-cash,transactions,purchase
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Accounting
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=4.0; extra == "dev"
Dynamic: license-file

# cashio-client

Python client for the [Cashio](https://pypi.org/project/cashio-client/) Client API (transactions and purchase payments).

Install in other projects with `pip install cashio-client` (after publishing) or from a local path / git URL.

کلاینت پایتون برای API کلاینت Cashio (تراکنش‌ها و پرداخت‌های خرید). قابل استفاده در پروژه‌های دیگر و قابل انتشار روی PyPI.

## Install

```bash
pip install cashio-client
```

From this repo (editable):

```bash
pip install -e .
```

## Quick start

```python
from cashio_client import CashioClient

client = CashioClient("http://127.0.0.1:8000", token="YOUR_TOKEN")

result = client.transactions.create(
    amount=1500000,
    type="income",
    description="from rahkarmelk",
    date="1405-06-22",
)
print(result.ok, result.id)
```

Token comes from Cashio **Settings → Client API Config**. There is no session login.

If that config has a default fund (and category), you can omit `fund` / `category`.

## Authentication

The token is sent with **one** method (default: Bearer header):

| Method | How |
|---|---|
| `bearer` (default) | `Authorization: Bearer <token>` |
| `token` | `Authorization: Token <token>` |
| `x-api-token` | `X-Api-Token: <token>` |
| `body` | JSON/form field `"token": "<token>"` |

```python
from cashio_client import AuthMethod, CashioClient, CashioSettings

settings = CashioSettings(
    "http://127.0.0.1:8000",
    token="YOUR_TOKEN",
    auth_method=AuthMethod.X_API_TOKEN,
)
client = CashioClient(settings)
```

`base_url` is the Cashio host (`http://127.0.0.1:8000`). A trailing `/a/api` is stripped if present. Requests go to `{base_url}/a/api/...`.

## Create a transaction

`POST /a/api/transaction/create`

```python
result = client.transactions.create(
    amount=250000,
    transaction_type="expense",
    fund=15,
    category=3,
    description="rent",
    payment_type="full_payment",
)
```

Aliases from the API are accepted: `type` for `transaction_type`, `fund_id` for `fund`, `category_id` for `category`, `holder_id` for `holder`.

### With files (multipart)

```python
result = client.transactions.create(
    amount=1500000,
    type="income",
    attached_file=["/path/to/receipt.pdf", "/path/to/photo.jpg"],
)
```

`attached_file` may be a path, a file object, a `(filename, fileobj)` tuple, or a list of those.

### Fields

| Field | Type | Required |
|---|---|---|
| `amount` | number / Decimal | yes |
| `transaction_type` / `type` | `income` or `expense` | yes |
| `fund` / `fund_id` | int | if no default fund |
| `category` / `category_id` | int | no (client default) |
| `description` | str | no |
| `date` | str Jalali `YYYY-MM-DD` | no |
| `reference_number` | str | no |
| `payment_type` | `full_payment`, `part_payment`, `completed_parts` | no (API default `full_payment`) |
| `holder` / `holder_id` | int | no |
| `transaction_linked` | int | no |
| `fee` | number | no (creates a linked expense) |
| `attached_file` | file(s) | no |

Constants: `TransactionType.INCOME`, `TransactionType.EXPENSE`, `PaymentType.FULL_PAYMENT`, …

## Purchase payments

Two kinds: **direct** (already paid, created as `completed`) and **indirect** (accountant pays later, created as `unseen`). Documents are optional for both.

The payment `amount` is not sent: the server computes it as the sum of `quantity * unit_price` over `lines`. At least one line is required, and every line needs `description`, `quantity`, `unit_price`.

```python
from cashio_client import PurchaseKind, PurchasePaymentMethod, PurchasePaymentStatus

result = client.purchases.create_direct(
    lines=[
        {"description": "A4 paper", "quantity": 2, "unit": "box", "unit_price": 750000},
        {"description": "pen", "quantity": 10, "unit_price": 5000},
    ],
    documents=["/path/to/invoice.jpg"],   # optional
    description="stationery",
    payment_date="1405-06-22",   # optional Jalali date, defaults to today
    request_id="INV-1",          # optional, unique per client
)
print(result.id, result.status)  # 11 completed

result = client.purchases.create_indirect(
    lines=[{"description": "ride", "quantity": 1, "unit_price": 300000}],
    description="taxi",
    request_id="INV-2",
)

# payment methods: cash (default), installment, transfer_order
client.purchases.create_indirect(
    lines=[{"description": "laptop", "quantity": 1, "unit_price": 30000000}],
    payment_method=PurchasePaymentMethod.INSTALLMENT,
    installment_count=3,            # required, >= 2
    installment_amount=10000000,    # required
)
client.purchases.create_indirect(
    lines=[{"description": "laptop", "quantity": 1, "unit_price": 30000000}],
    payment_method=PurchasePaymentMethod.TRANSFER_ORDER,
    transfer_due_date="1405-09-01", # required, Jalali date
    guarantee_check=7,              # optional, fund check id with purpose "guarantee"
)

# read
payment = client.purchases.get(result.id)               # by id
payment = client.purchases.get(request_id="INV-2")      # by your own reference
payments = client.purchases.list(status=PurchasePaymentStatus.IN_REVIEW, kind=PurchaseKind.INDIRECT)
payment.status, payment.amount, payment.lines, payment.documents, payment.transaction_id
payment.payment_method, payment.installment_count, payment.installment_amount
payment.transfer_due_date, payment.guarantee_check_id

# lines, documents, history (adding lines recalculates the payment amount)
client.purchases.lines(payment.id)
client.purchases.add_lines(payment.id, {"description": "stapler", "quantity": 1, "unit_price": 90000})   # one
client.purchases.add_lines(payment.id, [{"description": "a", "quantity": 1, "unit_price": 10}, ...])     # many
client.purchases.documents(payment.id)
client.purchases.add_documents(payment.id, ["/path/to/receipt.pdf"])
client.purchases.statuses(payment.id)
```

| Method | Endpoint |
|---|---|
| `purchases.create_direct(...)` | `POST /a/api/purchase/direct/create` |
| `purchases.create_indirect(...)` | `POST /a/api/purchase/indirect/create` |
| `purchases.list(status=, kind=)` | `GET /a/api/purchase/list` |
| `purchases.get(id)` / `get(request_id=)` | `GET /a/api/purchase/<id>/status` / `purchase/status?request_id=` |
| `purchases.lines(id)` / `add_lines(id, ...)` | `GET` / `POST /a/api/purchase/<id>/lines` |
| `purchases.documents(id)` / `add_documents(id, files)` | `GET` / `POST /a/api/purchase/<id>/documents` |
| `purchases.statuses(id)` | `GET /a/api/purchase/<id>/statuses` |

- Statuses: `unseen`, `seen`, `in_review`, `completed` (`PurchasePaymentStatus`).
- Payment methods: `cash`, `installment`, `transfer_order` (`PurchasePaymentMethod`). Fields of other methods are ignored by the server.
- Line units: `piece` (default), `kilogram`, `gram`, `liter`, `meter`, `box`, `pack`, `set`, `other` (`PurchaseLineUnit`).
- Lines and documents of a `completed` payment cannot be changed (`CashioAPIError`).
- Money values are returned as `Decimal`; dates and timestamps stay strings as sent by the API.
- Files accept the same inputs as `attached_file` above.

## Errors

```python
from cashio_client import (
    CashioAuthError,
    CashioNotFoundError,
    CashioValidationError,
    CashioAPIError,
)

try:
    client.transactions.create(amount=10, type="income")
except CashioAuthError as exc:
    print(exc.status_code, exc.payload)   # 401
except CashioValidationError as exc:
    print(exc.errors)                     # {"fund": ["This field is required."]}
except CashioNotFoundError as exc:
    print(exc.status_code)                # 404 (e.g. unknown purchase payment); subclass of CashioAPIError
except CashioAPIError as exc:
    print(exc.status_code, exc.payload)   # 400 detail / 405 / other
```

## Publish to PyPI

```bash
pip install -e ".[dev]"
python -m build
twine upload dist/*
```

Use TestPyPI first: `twine upload --repository testpypi dist/*`.

## License

MIT
