Metadata-Version: 2.5
Name: lynara-erp
Version: 1.1.0
Summary: Python SDK for the Lynara ERP platform - Sales (invoices, quotes), Purchases, Banking, Inventory, Catalog, CRM, HR
Project-URL: Homepage, https://lynara.ai
Project-URL: Documentation, https://lynara.ai/api
Project-URL: Repository, https://github.com/lynara/lynara-python
Project-URL: Changelog, https://github.com/lynara/lynara-python/blob/main/CHANGELOG.md
Author-email: Lynara <dev@lynara.ai>
License: MIT
Keywords: api,banking,catalog,crm,erp,hr,inventory,lynara,sales,sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
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 :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: httpx>=0.24.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Provides-Extra: httpx
Requires-Dist: httpx>=0.24.0; extra == 'httpx'
Description-Content-Type: text/markdown

# Lynara Python SDK

Python SDK for the [Lynara](https://lynara.ai) ERP platform (version 1.1.0).

Typical use: a cloud platform measures what its customers consume, computes the usage on its side,
then uses the SDK to create the invoices and quotes in Lynara and to read them back for its own
customer portal.

## Installation

```bash
pip install lynara-erp
pip install lynara-erp[httpx]   # optional, faster HTTP client
```

## Connect

```python
from lynara import Lynara

client = Lynara(
    url="https://api.lynara.ai/api",       # a bare host gets /api appended automatically
    api_key="lnr_live_xxxxxxxxxxxxxxxx",
    company_id="your-company-uuid",         # must be the company of the key, otherwise 403
)

company = client.company()
print(company["country"], company["currency"], company["currency_decimals"])
rules = client.tax_rules()   # live VAT rates, stamps, identifiers, mentions of the company's country
```

**Country rules (what to send for Algeria, France, Tunisia, Kuwait...):** see
[docs/REGLES_PAYS.md](docs/REGLES_PAYS.md).

**API key** (Lynara > Settings > API Keys, created by an **owner**):

| Setting | For a billing integration |
|---|---|
| Permissions | `read`, `write` (add `delete` only if you delete draft documents) |
| Modules | `sales` and `catalog` |
| Rate limit | 30 to 1000 requests per minute |

The key acts on behalf of the user who created it. That user must stay an active owner or admin of
the company: if they lose that role, the key stops working (403) until a new key is created.

## Cloud billing flow

```python
run = "2026-10"                      # your billing run id

# 1. Customer and product (catalog)
customer = client.catalog.customers.create({
    "name": "Acme Hosting SARL",
    "contact_email": "billing@acme.example",
    "vat_number": "FR12345678901",
    "billing_country": "FR",
})
product = client.catalog.products.create({
    "name": "Object storage (GB-month)",
    "unit": "Go",
    "price": 0.0125,
    "vat_percentage": 20,
    "is_service": True,
})

# 2. Invoice computed on your side, created in Lynara
invoice = client.sales.invoices.create({
    "customer_id": customer["customer_id"],
    "issue_date": "2026-10-31",
    "due_date": "2026-11-30",
    "notes": "Usage October 2026",
    "items": [
        {"product_id": product["product_id"], "product_name": "Object storage (GB-month)",
         "quantity": 1250, "unit_price": 0.0125, "vat_rate": 20},
        {"product_name": "Support plan", "quantity": 1, "unit_price": 49, "vat_rate": 20},
    ],
    "total_ht": 64.63, "total_tax": 12.93, "total_ttc": 77.56,   # optional, compared with Lynara
}, idempotency_key=f"{run}-{customer['customer_id']}")

# 3. Issue it (same checks as the Lynara screen: seller VAT number, e-invoicing rules, locks)
client.sales.invoices.set_status(invoice["invoice_id"], "pending")

# 4. Read it back for your customer portal (customer, lines, totals, status, number)
document = client.sales.invoices.document(invoice["invoice_id"])

# 5. Follow payments
unpaid = client.sales.invoices.list(customer_id=customer["customer_id"],
                                    status__in=["unpaid", "partially_paid"])
```

**Quotes**

```python
quote = client.sales.quotes.create({
    "customer_id": customer["customer_id"],
    "valid_until": "2026-11-30",
    "items": [{"product_name": "Dedicated server (12 months)", "quantity": 12, "unit_price": 89, "vat_rate": 20}],
}, idempotency_key=f"quote-{run}-{customer['customer_id']}")
client.sales.quotes.set_status(quote["quote_id"], "sent")
invoice = client.sales.quotes.convert_to_invoice(quote["quote_id"], idempotency_key=f"convert-{quote['quote_id']}")
```

### What Lynara does for you

Invoices and quotes go through the same official rules as the Lynara screens:

- legal number (`FAC-2026-0000001`), never chosen by the API;
- line amounts recomputed from quantity, unit price, discount and VAT rate, rounded to the
  **decimals of the document currency** (2 or 3, see below), then header totals summed from lines;
- country rules of the company (stamp duty only where it exists, VAT regime, e-invoicing locks);
- stock and accounting entries when the company uses them.

`create_with_details()` returns, next to the document, `currency` (code, decimals, exchange rate)
and `totals_check`: if the totals you sent differ from Lynara's by more than half a unit of the last
decimal, `match` is `false` and `differences` lists them. Lynara's amounts are the legal ones.

Rules checked before anything is written:

- `vat_rate` is **mandatory on every line** and must be one of the rates of the company's country
  (`client.tax_rules()["vat_rates"]`); it is never guessed. A 0 % line in a VAT country also needs
  `vat_regime` (EXEMPT, EXPORT, SUSPENSION, AUTOLIQUIDATION...) and `vat_exemption_mention`, printed on
  the invoice (`vat_exemption_reference` for EXPORT). See [docs/REGLES_PAYS.md](docs/REGLES_PAYS.md);
- the customer, products, payment terms, depot and salesperson must belong to your company;
- a draft is created by default; issue it with `set_status`.

### Idempotency (no duplicate invoice)

Pass `idempotency_key` on every `create` and `convert_to_invoice` (8 to 128 characters: letters,
digits, `. _ : -`). Same key within 24 h = same response (`idempotent_replay: true`), never a
second document.

- A POST is retried automatically after a network or server error **only** when it has a key.
- `ConflictError` (409): the same key is still running, or the previous attempt ended in an
  uncertain state. Check `invoices.list(...)` before using a new key.
- A business refusal (422, 403...) releases the key: fix the data and send it again with the same key.

## Countries and currencies

The company's country and currency come from Lynara. A document is in the company currency unless
you send `currency`. For another currency, `exchange_rate` is **mandatory**: the value of 1 unit of
the document currency in the company currency (for a TND company invoicing in EUR:
`"currency": "EUR", "exchange_rate": 3.41`). No rate is ever invented.

| Country | Code | Currency | Decimals | Standard VAT configured in Lynara |
|---|---|---|---|---|
| Algeria | DZ | DZD | 2 | 19 % |
| Saudi Arabia | SA | SAR | 2 | 15 % |
| Belgium | BE | EUR | 2 | 21 % |
| Egypt | EG | EGP | 2 | 14 % |
| United Arab Emirates | AE | AED | 2 | 5 % |
| France | FR | EUR | 2 | 20 % |
| Jordan | JO | JOD | 3 | 16 % |
| Kuwait | KW | KWD | 3 | none configured |
| Libya | LY | LYD | 3 | none configured |
| Morocco | MA | MAD | 2 | 20 % (10 % reduced; 7 % and 14 % removed on 1 Jan 2026) |
| Oman | OM | OMR | 3 | 5 % |
| Qatar | QA | QAR | 2 | none configured |
| Switzerland | CH | CHF | 2 | 8.1 % |
| Tunisia | TN | TND | 3 | 19 % |

USD is also accepted as a document currency. Always read the live values instead of hard-coding them:

```python
client.reference.countries()    # country, currency, decimals, standard VAT, timezones
client.reference.currencies()   # code, decimals, symbol
```

The VAT rate of each line is your responsibility (reduced rates, exemptions, reverse charge): the
standard rate above is the one configured in Lynara, not tax advice.

## Modules and resources

| Module | Accessor | Write via API | Read only via API |
|---|---|---|---|
| Sales | `client.sales` | `invoices`, `quotes` (official rules) | quote_lines, orders, order_lines, invoice_lines, delivery_notes, delivery_note_lines, exit_vouchers, exit_voucher_lines, credit_notes, credit_note_lines, allocations, receipts |
| Catalog | `client.catalog` | customers, suppliers, products, product_categories, product_subcategories, price_lists, payment_terms | |
| Purchases | `client.purchases` | quotes, quote_lines | invoices, invoice_lines, payments, allocations |
| Banking | `client.banking` | statements, transactions | reconciliations |
| Inventory | `client.inventory` | storage_units, product_settings, stock_levels, lots, movements, serial_numbers, stock_counts | |
| CRM | `client.crm` | leads, opportunities, activities, pipeline_stages | |
| HR | `client.rh` | employees, contracts, leaves, trainings... (see `client.schema("rh")`) | |
| Treasury | `client.tresorerie` | same resources as sales, purchases and banking (`.invoices`, `.quotes`, `.receipts`...) | |

Read-only resources are managed by Lynara (numbering, stock, accounting): a write returns 405.

## Resource methods

| Method | Description |
|---|---|
| `.list(limit, offset, order_by, order, search, **filters)` | Paginated list (max 100 per page) |
| `.iterate(batch_size, **filters)` | Auto-paginated generator |
| `.get(id)` | Raw record |
| `.create(data, idempotency_key=None)` | Create |
| `.update(id, data)` | Update |
| `.delete(id)` | Delete (invoices and quotes: only when Lynara allows it, e.g. drafts) |
| `.bulk_create(items)` | Up to 500 records (not for invoices and quotes) |
| `.export(format, limit)` | JSON or CSV |
| `.count()` | Total count |
| `invoices/quotes.document(id)` | Full document with customer and lines |
| `invoices/quotes.set_status(id, status)` | Invoice: draft, pending, unpaid, partially_paid, paid, void. Quote: draft, sent, accepted, rejected, expired |
| `quotes.convert_to_invoice(id, idempotency_key)` | Official conversion |

**Filters** on `list` and `iterate`: `field=value` (equality), `field__gte` / `field__lte` (range),
`field__in=[...]` (list), `field=None` (empty). Only real columns of the resource are accepted.

```python
client.sales.invoices.list(customer_id="...", issue_date__gte="2026-10-01", status__in=["unpaid"])
client.catalog.customers.list(billing_country="FR", is_active=True)
```

## Errors

```python
from lynara import (LynaraError, AuthenticationError, LynaraPermissionError, NotFoundError,
                    ConflictError, RateLimitError, ValidationError, ServerError)

try:
    client.sales.invoices.create(data, idempotency_key=key)
except ValidationError as e:        # 422: currency, exchange rate, VAT rate, unknown product...
    print(e.message)
except LynaraPermissionError as e:  # 403: permission, module, other company, key creator no longer admin
    print(e.message)
except ConflictError:               # 409: same idempotency key running or uncertain
    ...
except RateLimitError as e:         # 429
    print(f"retry after {e.retry_after}s")
except ServerError:                 # 5xx: no technical detail is returned
    ...
```

## Limits

- Invoice PDFs are produced by the Lynara screens; the API returns the full document data
  (`document()`), which your portal can display or render.
- One key = one company. Use one key per Lynara company.

## License

MIT
