Metadata-Version: 2.4
Name: libciss
Version: 0.1.0a6
Summary: Python client for CISS applications
Author: Redson
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0

# libciss

Python client for [CISS](https://www.ciss.com.br/) applications. Currently supports **CISS Control**, with helpers for authentication and **pré-carga / separação** (warehouse pre-load picking).

> **Status:** `0.1.0a3` (alpha) — the API surface may change.

## Requirements

- Python 3.10+ (uses `X | None` type syntax and `dataclass(slots=True)`)
- `requests>=2.31.0`

## Installation

```bash
pip install libciss
```

Everything is imported from the single top-level `libciss` package:

```python
from libciss import CISSClient
```

> **Upgrading from `0.1.0a1`:** that release was unusable — it shipped its
> modules under a top-level `services` package (which collided with any
> application that had its own `services/`), left `client` and `exceptions`
> out of the distribution entirely, and imported from a `src` package that
> was never packaged. `0.1.0a2` moves everything under `libciss`, so
> `from src import ...` becomes `from libciss import ...`.

## Quick start

```python
from datetime import datetime

from libciss import CISSClient
from libciss.services.cisscontrol.models import PreCargaSeparacaoSubmission

client = CISSClient("https://your-ciss-host.example.com")

# Authenticate — stores the bearer token on the shared session
user = client.cisscontrol.auth.login("username", "password")
print(user.id_empresa, user.razao_social)

# List pre-loads
preloads = client.cisscontrol.precarga_separacao.get_all(
    id_empresa=user.id_empresa,
    num_dias_sync=30,
)

# Fetch the line items of one pre-load
items = client.cisscontrol.precarga_separacao.items(
    id_empresa=user.id_empresa,
    id_precarga=preloads[0].id_pre_carga,
    id_usuario=user.id_user,
)

# Report a separated item back to CISS
item = items[0]
submission = PreCargaSeparacaoSubmission(
    id_usuario=user.id_user,
    id_empresa=item.id_empresa,
    id_documento=item.id_documento,
    id_pre_carga=item.id_pre_carga,
    id_produto=item.id_produto,
    id_subproduto=item.id_subproduto,
    id_local_retirada=item.id_local_retirada,
    ds_local_retirada=item.ds_local_retirada,
    num_sequencia=item.num_sequencia,
    qtd_produto=item.qtd_produto,
    fg_separado="S",
    qtd_contada=item.qtd_produto,
    dt_hr_inicio=datetime.now(),
)

response = client.cisscontrol.precarga_separacao.submit([submission])

client.cisscontrol.auth.logout()
```

## Architecture

One `CISSClient` owns a single `requests.Session`. Services never hold their own transport — they call back into `client.request(...)`, so the token set at login is automatically applied to every later call.

```text
CISSClient(base_url)
├── session          # requests.Session (shared headers / cookies)
├── request(...)     # low-level HTTP helper, raises on 4xx/5xx
└── cisscontrol      # CISSControl
      ├── auth                 # AuthenticationService
      │     ├── login / logout
      │     ├── token / authenticated
      │     └── user → LoginResponse | None
      ├── pre_carga            # PreCargaService — conferência
      │     ├── get_all(...)    → list[PreCarga]
      │     ├── items(...)      → list[PreCargaItem]
      │     ├── submit(...)     → requests.Response
      │     └── set_status(...) → requests.Response
      └── precarga_separacao   # PreCargaSeparacaoService — separação
            ├── get_all(...)  → list[PreCargaSeparacao]
            ├── items(...)    → list[PreCargaSeparacaoItem]
            └── submit(...)   → requests.Response
```

`pre_carga` and `precarga_separacao` are **different CISS modules**, not two views of one. `pre_carga` covers *conferência* — checking a picked order against its invoice. `precarga_separacao` covers *separação* — the warehouse picking itself. Their payloads share almost no fields.

## How conferência works

*Conferência* is the checking step: a **conferente** walks a pré-carga that has already been picked, counts what is physically there, and reports the counted quantities back so the order can be released.

Everything in this section is derived from real traffic captured off the reference Flutter app (`Dart/3.9`) against a CISS backend on `versao-backend: 8.14.2`. Where the captures do not settle a question, this says so rather than guessing.

### The flow

```text
1. get_all(id_empresa, id_conferente)
   └── the pré-cargas assigned to this conferente        → list[PreCarga]

2. items(id_empresa, id_usuario, id_precarga)
   └── the lines of one pré-carga, expected quantities   → list[PreCargaItem]
       every line comes back with qtd_contada == 0.0

3. ...the conferente counts, you set item.qtd_contada...

4. submit(pre_carga, itens)
   └── the header + every counted line, in one request   → 201 + persisted items

5. set_status(id_precarga, status_code, id_usuario_conferente)
   └── closes / moves the pré-carga                      → 200 + echo of the request

6. get_all(...) again
   └── refreshed queue
```

Steps 4 and 5 are **two separate requests**. Submitting the items does not change the status, and the library does not chain them for you — a crash between the two leaves the items recorded and the status untouched.

### Step by step

| # | Call | Sends | Gets back |
|---|------|-------|-----------|
| 1 | `get_all` | `idEmpresa`, `numDiasSync`, `dsPreCarga`, `dsPontoRetirada`, `idConferente` as query keys | Array of headers, each with `clientes[]` and `cliForList[]` |
| 2 | `items` | `idEmpresa`, `idUsuario`, `idPreCarga`, `dsPreCarga`, `dsPontoRetirada`, `isValidaEmpLog` as query keys | Array of lines — **9 fields**, expected quantities only |
| 4 | `submit` | `{"preCargaList": [header], "itens": [...]}` | `201` + the persisted lines, minus `idLote` / `corredor` / `gondola` / `prateleira` |
| 5 | `set_status` | `{"idPreCarga", "statusCode", "idUsuarioConferente"}` | `200` + the same three fields echoed verbatim |

### The counted quantity is yours to fill in

`items()` returns what the system *expects*. There is no `qtdContada` in the response — that value is the whole point of the conferência, so CISS has nothing to tell you about it yet.

The library models this by returning lines with **`qtd_contada == 0.0`**:

```python
itens = client.cisscontrol.pre_carga.items(
    id_empresa=user.id_empresa,
    id_usuario=user.id_user,
    id_precarga=pre_carga.id_pre_carga,
)

for item in itens:
    item.qtd_contada = count_physically(item)   # ← your job

client.cisscontrol.pre_carga.submit(pre_carga, itens)
```

**Submitting a line you never touched reports that you counted zero of it.** That is deliberate — the alternative default, seeding `qtd_contada` from `qtd_produto`, would silently claim a clean count for a pré-carga nobody checked.

Seven of the sixteen fields are also absent from the response and are filled in locally: `idEmpresa` and `idUsuario` come from the arguments you passed (CISS takes them as query parameters and does not echo them), while `idLote`, `corredor`, `gondola` and `prateleira` default to `None`. Where a real `idLote` is meant to come from is unresolved — see the open questions.

### How a line is identified

A conferência line is identified by **the sale it came from**, not by the product:

```text
idEmpresaVenda + idPlanilhaVenda + numSequenciaVenda
```

The capture proves this matters — `idProduto` `134640` appears **twice** in the same pré-carga, once under `idPlanilhaVenda` `21675444` and once under `21689087`, with different quantities (`90.0` and `30.0`). Deduplicating by product would silently merge two legitimate lines.

### Quantities

`qtdProduto` is what the system expects; `qtdContada` is what the conferente actually counted. Nothing is validated client-side, and a divergence is resolved server-side.

Worth knowing: in the captured session all seven lines matched exactly (`2/2`, `5/5`, `1/1`, `5/5`, `90/90`, `30/30`, `30/30`). **A divergent count was never observed**, so how the backend reacts to one is untested here.

### Statuses

This is the part the capture does *not* settle. What it shows, literally:

| Where | Field | Value |
|-------|-------|-------|
| Header sent in `preCargaList` | `idStatus` | `1` |
| Header sent in `preCargaList` | `idConferente` | `null` |
| `set_status` body | `statusCode` | `0` |
| A *different* pré-carga in the later listing | `idStatus` | `0` |
| That same pré-carga | `idConferente` | `617` |

Only `statusCode: 0` was ever sent, so the set of valid codes and their meanings is unknown. `idStatus` and `statusCode` are also not demonstrably the same scale — they are different field names in different payloads, and nothing in the capture links them. The library therefore passes status codes through untouched and neither validates nor enumerates them.

### Open questions

Genuinely unresolved — do not treat any of these as documented behaviour.

| Question | Why it is open |
|----------|----------------|
| What do the `statusCode` values mean? | Only `0` was ever sent |
| Are `idStatus` and `statusCode` the same scale? | Different fields, different payloads, no observed link |
| What does `isValidaEmpLog` do? | Only `false` was ever sent |
| Where do `idLote`, `corredor`, `gondola` and `prateleira` come from? | `items` never returns them, yet `submit` has a slot for each. Only `null` was ever sent |
| Why does `items` say `idUsuario` where `get_all` says `idConferente`? | Both were `617` in every capture, so nothing distinguishes them |
| Does submitting remove a pré-carga from the queue? | The submitted `43797` is absent from the later listing — but that listing filtered on `idConferente=617` and the submitted header carried `idConferente: null`, so the filter alone explains it |
| Does `preCargaList` accept more than one header? | Only ever sent with one |
| Is `submit` idempotent? | Never sent twice |
| Is `idPedidoPreCarga` ever non-`null`? | Only observed as `null` |

## API reference

### `CISSClient`

Entry point for all services.

```python
CISSClient(base_url: str)
```

| Attribute / method | Description |
|--------------------|-------------|
| `base_url` | API root URL; a trailing `/` is stripped |
| `session` | Shared `requests.Session` — holds the `Authorization` header after login |
| `cisscontrol` | `CISSControl` service namespace |
| `request(method, endpoint, **kwargs)` | Sends `method` to `base_url + endpoint`, calls `raise_for_status()`, returns the `Response` |

`endpoint` is concatenated onto `base_url` verbatim, so it must start with `/`. Any extra keyword argument is forwarded to `requests.Session.request` (`json=`, `params=`, `timeout=`, …).

Session-level configuration goes on `client.session`:

```python
client.session.verify = "/path/to/ca-bundle.pem"
client.session.proxies = {"https": "http://proxy.internal:3128"}
```

Every request is bounded by a default `(connect, read)` timeout of `(5.0, 30.0)` seconds, exposed as `libciss.client.DEFAULT_TIMEOUT`. The read half is short deliberately: CISS Control tends to stall rather than answer when a company has no pre-load.

Change it per client, or per call:

```python
client = CISSClient("https://ciss.example.com", timeout=(5, 60))
client.timeout = None                     # wait forever
client.request("GET", "/ciss-control/precarga_separacao", timeout=10)  # this call only
```

A request that exceeds it raises `requests.Timeout`. Note that setting `client.session.timeout` does **not** work — `requests.Session` has no such attribute; use the constructor argument above.

---

### Authentication — `client.cisscontrol.auth`

#### `login(username, password) -> LoginResponse`

`POST /ciss-control/login`

Sends `{"username": ..., "password": ...}` as JSON. On success it writes the returned `tokenBearer` into `session.headers["Authorization"]` and caches the parsed payload on `auth.user`.

The token is stored exactly as the API returns it — the library does not prepend a `Bearer` prefix. `razao_social` is right-stripped of trailing whitespace; all other fields are passed through unchanged.

#### `logout() -> None`

Removes the `Authorization` header and clears `auth.user`. **Client-side only** — no request is sent, so the token stays valid server-side until it expires.

#### Properties

| Property | Type | Description |
|----------|------|-------------|
| `token` | `str \| None` | Current `Authorization` header value, or `None` |
| `authenticated` | `bool` | `True` when a token is set |
| `user` | `LoginResponse \| None` | Payload of the last successful login |

`authenticated` only reports whether a token is *present* — it does not validate or check expiry. An expired token still reads as `True`, and the call then fails with `requests.HTTPError` (401).

#### `requires_login` decorator

`libciss.services.cisscontrol.auth.requires_login` guards service methods. It reads `self.client.cisscontrol.auth.authenticated` and raises `NotAuthenticatedError` when no token is set, so it can only decorate methods of a service that holds a `client` attribute.

```python
from libciss.services.cisscontrol.auth import requires_login

class MyService:
    def __init__(self, client):
        self.client = client

    @requires_login
    def do_something(self):
        return self.client.request("GET", "/ciss-control/something")
```

---

### Conferência — `client.cisscontrol.pre_carga`

Every method is decorated with `@requires_login` and raises `NotAuthenticatedError` when called before `login()`.

The flow is: `get_all` to find the pré-carga, `items` to fetch its lines, `submit` to send the counted quantities, `set_status` to close it.

```python
user = client.cisscontrol.auth.login("username", "password")

pre_cargas = client.cisscontrol.pre_carga.get_all(
    id_empresa=user.id_empresa,
    id_conferente=user.id_user,
)
pre_carga = pre_cargas[0]

itens = client.cisscontrol.pre_carga.items(
    id_empresa=user.id_empresa,
    id_usuario=user.id_user,
    id_precarga=pre_carga.id_pre_carga,
)

# Every line arrives with qtd_contada == 0.0 — record what you counted
for item in itens:
    item.qtd_contada = item.qtd_produto

client.cisscontrol.pre_carga.submit(pre_carga, itens)
client.cisscontrol.pre_carga.set_status(
    id_precarga=pre_carga.id_pre_carga,
    status_code=0,
    id_usuario_conferente=user.id_user,
)
```

#### `get_all(*, id_empresa, id_conferente, ...) -> list[PreCarga]`

`GET /ciss-control/pre_carga`

| Parameter | Query key | Default |
|-----------|-----------|---------|
| `id_empresa` | `idEmpresa` | required |
| `id_conferente` | `idConferente` | required |
| `num_dias_sync` | `numDiasSync` | `99` |
| `ds_precarga` | `dsPreCarga` | `""` |
| `ds_ponto_retirada` | `dsPontoRetirada` | `""` |

All keys are sent on every call, empty strings included. Returns `[]` on HTTP `204` or an empty/non-array body.

#### `items(*, id_empresa, id_usuario, id_precarga, ...) -> list[PreCargaItem]`

`GET /ciss-control/pre_carga/itens`

| Parameter | Query key | Default |
|-----------|-----------|---------|
| `id_empresa` | `idEmpresa` | required |
| `id_usuario` | `idUsuario` | required |
| `id_precarga` | `idPreCarga` | required |
| `ds_precarga` | `dsPreCarga` | `""` |
| `ds_ponto_retirada` | `dsPontoRetirada` | `""` |
| `is_valida_emp_log` | `isValidaEmpLog` | `False` |

The source of the `itens` you later hand to `submit`. Returns `[]` on HTTP `204` or an empty/non-array body.

`id_empresa` and `id_usuario` are copied onto every returned line — CISS takes them as query parameters and does not repeat them in the body. Note that this endpoint calls the user `idUsuario` while `get_all` calls the same person `idConferente`.

`is_valida_emp_log` is serialized as the **string** `"true"` / `"false"`. Letting `requests` stringify the bool would send `"False"`, which is not what the reference app sends. Its purpose is unknown and only `False` has been observed.

**Every returned line has `qtd_contada == 0.0`** — the response has no counted quantity, because producing one is the point of the conferência. Overwrite it before submitting, or you will report a count of zero. See [The counted quantity is yours to fill in](#the-counted-quantity-is-yours-to-fill-in).

#### `submit(pre_carga, itens) -> requests.Response`

`POST /ciss-control/pre_carga`

Posts `{"preCargaList": [pre_carga.to_dict()], "itens": [i.to_dict() for i in itens]}`. The wire format takes a list of headers, but only one pré-carga per call has ever been observed, so the method takes a single `PreCarga` and wraps it.

`itens` normally comes from `items()` with `qtd_contada` filled in.

Returns the raw `requests.Response` — `201` on success, carrying the persisted items as its body. The body is not parsed, so read `response.json()` yourself if you need it.

This does **not** close the pré-carga; call `set_status` afterwards.

#### `set_status(*, id_precarga, status_code, id_usuario_conferente) -> requests.Response`

`POST /ciss-control/pre_carga/status`

Posts `{"idPreCarga": ..., "statusCode": ..., "idUsuarioConferente": ...}`. Status codes are passed through as CISS defines them — the library neither validates nor enumerates them. CISS echoes the request body back, so there is nothing to parse.

---

### Pré-carga separação — `client.cisscontrol.precarga_separacao`

Every method is decorated with `@requires_login` and raises `NotAuthenticatedError` when called before `login()`.

#### `get_all(*, id_empresa, ...) -> list[PreCargaSeparacao]`

`GET /ciss-control/precarga_separacao`

Keyword-only. All filter parameters are always sent, empty strings included.

| Parameter | Type | Default | API query key |
|-----------|------|---------|---------------|
| `id_empresa` | `int` | *required* | `idEmpresa` |
| `num_dias_sync` | `int` | `99` | `numDiasSync` |
| `ds_separacao` | `str` | `""` | `dsSeparacao` |
| `ds_precarga` | `str` | `""` | `dsPreCarga` |
| `ds_ponto_retirada` | `str` | `""` | `dsPontoRetirada` |
| `dt_inicio` | `str` | `""` | `dtInicio` |
| `ds_pre_separacao` | `str` | `""` | `dsPreSeparacao` |
| `dt_fim` | `str` | `""` | `dtFim` |

Returns `[]` on HTTP `204` or an empty/non-array body, otherwise one `PreCargaSeparacao` per element of the JSON array.

Date filters (`dt_inicio` / `dt_fim`) are typed `str` and forwarded verbatim — the library does not format `date` objects for you.

> A large `num_dias_sync` can make this endpoint stall. It no longer hangs indefinitely — the default read timeout caps it — but a stalled call still costs you that timeout, so prefer a bounded window (e.g. `num_dias_sync=30`).

#### `items(*, id_empresa, id_precarga, id_usuario, ...) -> list[PreCargaSeparacaoItem]`

`GET /ciss-control/precarga_separacao/itens`

| Parameter | Type | Default | API query key |
|-----------|------|---------|---------------|
| `id_empresa` | `int` | *required* | `idEmpresa` |
| `id_precarga` | `int` | *required* | `idPreCarga` |
| `id_usuario` | `int` | *required* | `idUsuario` |
| `ds_separacao` | `str` | `""` | `dsSeparacao` |
| `ds_precarga` | `str` | `""` | `dsPreCarga` |
| `ds_ponto_retirada` | `str` | `""` | `dsPontoRetirada` |
| `dt_inicio` | `str` | `""` | `dtInicio` |
| `ds_pre_separacao` | `str` | `""` | `dsPreSeparacao` |
| `dt_fim` | `str` | `""` | `dtFim` |

Returns `[]` on HTTP `204` or an empty/non-array body, matching `get_all`.

#### `submit(items) -> requests.Response`

`POST /ciss-control/precarga_separacao`

Takes a positional `list[PreCargaSeparacaoSubmission]` and posts `[item.to_dict() for item in items]` as a JSON array. Returns the raw `requests.Response` — the body is not parsed, so inspect `response.status_code` / `response.json()` yourself.

---

## Data models

Models live in `libciss.services.cisscontrol.models`. They are `@dataclass(slots=True)`, use snake_case in Python, and map to the API's camelCase. Response models expose `from_dict`, request models expose `to_dict`, and `PreCarga` exposes both because CISS hands the same header back to you on submit.

Being slotted dataclasses, they take positional or keyword arguments, compare by value, and reject attributes that are not declared fields.

`from_dict` requires the keys that *identify* a record and tolerates the merely descriptive ones, because CISS omits those on some pre-loads and losing a description is not a reason to fail a whole batch. Each model below lists what it tolerates.

### `LoginResponse`

Built by `auth.login()`.

| Field | Type | API field |
|-------|------|-----------|
| `token_bearer` | `str` | `tokenBearer` |
| `id_user` | `int` | `idUser` |
| `id_grupo` | `int` | `idGrupo` |
| `username` | `str` | `username` |
| `enabled` | `bool` | `enabled` |
| `id_empresa` | `int` | `idEmpresa` |
| `cnpj_empresa` | `str` | `cnpjEmpresa` |
| `razao_social` | `str` | `razaoSocial` (right-stripped) |
| `permissions` | `list[str]` | `permissionsList` |

A missing or `null` `razaoSocial` becomes `""` and a missing `permissionsList` becomes `[]`. The remaining keys are required.

### `PreCarga`

Returned by `pre_carga.get_all()` and accepted back by `pre_carga.submit()`.

| Field | Type | API field |
|-------|------|-----------|
| `id_empresa` | `int` | `idEmpresa` |
| `id_pre_carga` | `int` | `idPreCarga` |
| `descr_pre_carga` | `str` | `descrPreCarga` (right-stripped) |
| `id_status` | `int` | `idStatus` |
| `dt_alteracao` | `datetime \| None` | `dtAlteracao` |
| `num_nota` | `int` | `numNota` |
| `id_orcamento` | `int` | `idOrcamento` |
| `id_pedido_pre_carga` | `int \| None` | `idPedidoPreCarga` |
| `id_conferente` | `int \| None` | `idConferente` |
| `emp_alias` | `str` | `empAlias` — **GET only** |
| `clientes` | `list[Cliente]` | `clientes` — **GET only** |
| `cli_for_list` | `list[CliFor]` | `cliForList` — **GET only** |

`idEmpresa`, `idPreCarga`, `idStatus`, `numNota` and `idOrcamento` are required — a missing one raises `KeyError`. A missing or `null` `descrPreCarga` / `empAlias` becomes `""`, the two nested arrays become `[]`, and the remaining keys become `None`.

`to_dict()` emits only the nine non-GET-only keys, which is exactly what CISS accepts in `preCargaList`. The intended use is to take an object from `get_all()`, adjust `id_status` / `id_conferente`, and pass it straight to `submit()`.

`dtAlteracao` keeps its fractional seconds rather than being truncated to a date, because it round-trips verbatim. CISS is inconsistent about the width — `"2026-08-11T17:14:40.161"` and `"2026-07-20T19:33:12.966555"` both occur — so parsing accepts either and serialization emits milliseconds, reproducing what the app sends.

### `Cliente`

Element of `PreCarga.clientes`.

| Field | Type | API field |
|-------|------|-----------|
| `id_clifor` | `int` | `idClifor` |
| `nome_cliente` | `str` | `nomeCliente` (right-stripped) |

`idClifor` is required; a missing or `null` `nomeCliente` becomes `""`.

### `CliFor`

Element of `PreCarga.cli_for_list`. Overlaps with `Cliente` — CISS returns the same party under both keys with different field names — but adds the activity flag and the last-change timestamp.

| Field | Type | API field |
|-------|------|-----------|
| `id_fornecedor` | `int` | `idFornecedor` |
| `nome_fornecedor` | `str` | `nomeFornecedor` (right-stripped) |
| `flag_inativo` | `str` | `flagInativo` (`"F"` / `"T"`), not coerced to `bool` |
| `dt_alteracao` | `datetime \| None` | `dtAlteracao` |

`idFornecedor` is required; the rest fall back to `""` / `None`.

### `PreCargaItem`

Returned by `pre_carga.items()` and accepted back by `pre_carga.submit()`. A line is identified by the sale it came from — `idEmpresaVenda` / `idPlanilhaVenda` / `numSequenciaVenda` — not by the product, so the same `idProduto` legitimately appears more than once in one pré-carga.

| Field | Type | API field | Default |
|-------|------|-----------|---------|
| `id_usuario` | `int` | `idUsuario` | required |
| `id_empresa` | `int` | `idEmpresa` | required |
| `id_pre_carga` | `int` | `idPreCarga` | required |
| `id_empresa_venda` | `int` | `idEmpresaVenda` | required |
| `id_planilha_venda` | `int` | `idPlanilhaVenda` | required |
| `num_sequencia_venda` | `int` | `numSequenciaVenda` | required |
| `id_produto` | `int` | `idProduto` | required |
| `id_subproduto` | `int` | `idSubproduto` | required |
| `qtd_produto` | `float` | `qtdProduto` | required |
| `qtd_contada` | `float` | `qtdContada` | required |
| `ds_local_retirada` | `str` | `dsLocalRetirada` | required |
| `id_lote` | `str \| None` | `idLote` | `None` |
| `visualiza_qtd_pre_carga` | `bool` | `visualizaQtdPreCarga` | `True` |
| `corredor` | `str \| None` | `corredor` | `None` |
| `gondola` | `str \| None` | `gondola` | `None` |
| `prateleira` | `str \| None` | `prateleira` | `None` |

Nothing is validated: a `qtd_contada` differing from `qtd_produto` is accepted and resolved server-side.

`visualizaQtdPreCarga` is serialized as the **string** `"true"` / `"false"`, not as a JSON boolean. That is what the app sends, even though CISS answers with a real boolean. Sending an actual boolean is untested.

#### `from_dict(data, *, id_empresa, id_usuario)`

Used by `pre_carga.items()`. The `/itens` response carries only nine of the sixteen fields, so `from_dict` takes the two it omits as keyword arguments and defaults the rest:

| Field | Where it comes from |
|-------|---------------------|
| `id_empresa`, `id_usuario` | The keyword arguments — CISS takes them as query parameters and does not echo them back |
| `qtd_contada` | `0.0` — nothing has been counted yet |
| `id_lote`, `corredor`, `gondola`, `prateleira` | `None` — absent from the response |

`idPreCarga`, `idEmpresaVenda`, `idPlanilhaVenda`, `numSequenciaVenda`, `idProduto`, `idSubproduto` and `qtdProduto` are required — a missing one raises `KeyError`. A missing or `null` `dsLocalRetirada` becomes `""`, and a missing `visualizaQtdPreCarga` becomes `True`.

Unlike `to_dict`, this reads `visualizaQtdPreCarga` as a real boolean, because that is how the endpoint returns it.

### `PreCargaSeparacao`

| Field | Type | API field |
|-------|------|-----------|
| `id_empresa` | `int` | `idEmpresa` |
| `id_pre_carga` | `int` | `idPreCarga` |
| `descricao` | `str` | `descricao` |
| `dt_movimento` | `date` | `dtMovimento`, coerced to a `date` |

`idEmpresa`, `idPreCarga` and `dtMovimento` are required — a missing one raises `KeyError`. A missing or `null` `descricao` becomes `""`, and it is right-stripped since CISS pads it to a fixed width.

`dtMovimento` accepts `"2026-08-05"`, a full `"2026-08-05T00:00:00"` timestamp, or an already-decoded `date`/`datetime`; CISS returns all of these for the same field. Anything else raises `ValueError`.

### `PreCargaSeparacaoItem`

| Field | Type | API field |
|-------|------|-----------|
| `id_pre_carga` | `int` | `idPreCarga` |
| `id_empresa` | `int` | `idEmpresa` |
| `id_documento` | `int` | `idDocumento` |
| `id_produto` | `int` | `idProduto` |
| `id_subproduto` | `int` | `idSubproduto` |
| `ds_local_retirada` | `str` | `dsLocalRetirada` |
| `num_sequencia` | `int` | `numSequencia` |
| `id_lote` | `str \| None` | `idLote` — optional, `None` when absent |
| `qtd_produto` | `float` | `qtdProduto` |
| `fg_separado` | `str` | `fgSeparado` |
| `qtd_contada_divergente` | `bool` | `qtdContadaDivergente` |
| `id_local_retirada` | `int` | `idLocalRetirada` |

Required: `idPreCarga`, `idEmpresa`, `idDocumento`, `idProduto`, `idSubproduto`, `numSequencia` and `qtdProduto` — these identify the line, so a missing one raises `KeyError`. The rest fall back when missing or `null`:

| Key | Fallback |
|-----|----------|
| `dsLocalRetirada` | `""` |
| `idLote` | `None` |
| `fgSeparado` | `""` |
| `qtdContadaDivergente` | `False` |
| `idLocalRetirada` | `0` |

Because of that fallback, **`id_local_retirada == 0` means "CISS did not say"**, not pick-up location number zero. `qtd_produto` is coerced with `float()` and `id_local_retirada` with `int()`, since CISS sometimes sends them as strings.

### `PreCargaSeparacaoSubmission`

Request DTO for `submit()`. Every field is required — there are no defaults, so all thirteen must be supplied.

| Field | Type | JSON key |
|-------|------|----------|
| `id_usuario` | `int` | `idUsuario` |
| `id_empresa` | `int` | `idEmpresa` |
| `id_documento` | `int` | `idDocumento` |
| `id_pre_carga` | `int` | `idPreCarga` |
| `id_produto` | `int` | `idProduto` |
| `id_subproduto` | `int` | `idSubproduto` |
| `id_local_retirada` | `int` | `idLocalRetirada` |
| `ds_local_retirada` | `str` | `dsLocalRetirada` |
| `num_sequencia` | `int` | `numSequencia` |
| `qtd_produto` | `float` | `qtdProduto` |
| `fg_separado` | `str` | `fgSeparado` |
| `qtd_contada` | `float` | `qtdContada` |
| `dt_hr_inicio` | `datetime.datetime` | `dtHrInicio`, serialized with `.isoformat()` |

`to_dict()` calls `.isoformat()` on `dt_hr_inicio`, so it must be a `datetime` — passing a string raises `AttributeError`. Nothing is validated beyond that: `fg_separado` is sent as-is (`"S"` / `"N"`), and a `qtd_contada` that differs from `qtd_produto` is accepted and resolved server-side.

---

## Exceptions

```text
Exception
└── CISSClientError
    └── AuthenticationError
        └── NotAuthenticatedError
```

Defined in `libciss.exceptions`.

| Exception | Raised when |
| --------- | ----------- |
| `CISSClientError` | Base class for library errors — catch this to catch them all |
| `AuthenticationError` | Auth-related failures |
| `NotAuthenticatedError` | A `@requires_login` method is called with no token set |

Transport and HTTP failures are **not** wrapped: `client.request()` calls `raise_for_status()`, so 4xx/5xx surface as `requests.HTTPError`, and connection/timeout problems surface as the corresponding `requests.RequestException` subclasses.

```python
import requests

from libciss.exceptions import NotAuthenticatedError

try:
    preloads = client.cisscontrol.precarga_separacao.get_all(id_empresa=1)
except NotAuthenticatedError:
    client.cisscontrol.auth.login("user", "pass")
except requests.HTTPError as e:
    print(e.response.status_code, e.response.text)
```

A 401 from an expired token arrives as `requests.HTTPError`, not `NotAuthenticatedError` — the local check only sees that a token exists. To recover, call `login()` again.

---

## Endpoint map

| HTTP | Path | Client method |
|------|------|---------------|
| `POST` | `/ciss-control/login` | `auth.login` |
| `GET` | `/ciss-control/pre_carga` | `pre_carga.get_all` |
| `GET` | `/ciss-control/pre_carga/itens` | `pre_carga.items` |
| `POST` | `/ciss-control/pre_carga` | `pre_carga.submit` |
| `POST` | `/ciss-control/pre_carga/status` | `pre_carga.set_status` |
| `GET` | `/ciss-control/precarga_separacao` | `precarga_separacao.get_all` |
| `GET` | `/ciss-control/precarga_separacao/itens` | `precarga_separacao.items` |
| `POST` | `/ciss-control/precarga_separacao` | `precarga_separacao.submit` |

---

## Known issues

These are current limitations of the code, not of the documentation.

| Issue | Impact |
| ----- | ------ |
| `get_all` can stall on wide sync windows | Bounded by the read timeout now, but a stalled call still costs you those seconds — keep `num_dias_sync` small |
| `logout()` is client-side only | The token remains valid on the server |
| Transport errors are not wrapped | Callers catch `requests` exceptions (`HTTPError`, `Timeout`) alongside `CISSClientError` |
| Empty query filters are sent as `key=` | The reference app sends bare `key` with no `=`. Both decode to an empty string server-side, but this is the one place the library is not byte-identical to the captured traffic |
| `pre_carga.submit` / `set_status` return unparsed responses | The `201` body (the persisted items) and the status echo are yours to read |
| `pre_carga.items` returns `qtd_contada == 0.0` | Submitting an untouched line reports a count of zero. Deliberate, but it is a footgun if you forget |
| `pre_carga.items` never returns `idLote` / `corredor` / `gondola` / `prateleira` | They default to `None` on submit; where real values come from is unknown |
| No test suite | Changes are unverified |

---

## Glossary

Portuguese terms used by the CISS API, kept untranslated in the model field names.

| Term | Meaning |
|------|---------|
| **Empresa** | Company / store (`id_empresa`, CNPJ, razão social) |
| **Pré-carga** | Pre-load batch for warehouse picking |
| **Separação** | Product picking / separation |
| **Conferência** | Checking a picked order against its invoice |
| **Conferente** | The person doing the conferência (`id_conferente`) |
| **Nota** | Invoice (`num_nota`) |
| **Orçamento** | Quote (`id_orcamento`, `0` when there is none) |
| **Planilha de venda** | Sale sheet a line item came from (`id_planilha_venda`) |
| **Clifor** | Cliente/fornecedor — customer or supplier, one record for both |
| **Corredor / gôndola / prateleira** | Aisle / gondola / shelf, the item's physical location |
| **Local de retirada** | Pick-up location |
| **Documento** | Related document ID |
| **Produto / Subproduto** | Product and subproduct IDs |
| **Lote** | Lot / batch code (optional) |
| **Qtd produto** | Expected quantity on the pre-load |
| **Qtd contada** | Quantity actually counted during separation |
| **fgSeparado** | Separation flag, an API string (`"S"` / `"N"`) |
| **numDiasSync** | Days of history to sync (default `99`) |

---

## License

MIT © Redson
