Metadata-Version: 2.4
Name: ORD
Version: 1.12.2
Summary: Python client for ATOL libfptr10 fiscal printer driver
Author-email: Vladimir Smirnov <volodya@brandshop.ru>
License: Proprietary
Project-URL: Repository, https://brandshop.gitlab.yandexcloud.net/online-receipt/online-receipt-driver
Project-URL: Mirror, https://github.com/brandshopru/online-receipt-driver
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=6.0; extra == "dev"
Requires-Dist: pytest-mock>=3.0; extra == "dev"

# ORD — Python-клиент драйвера АТОЛ libfptr10

Тонкая обёртка над `libfptr10` (драйвер ККТ АТОЛ v10.10.x) для типовых
кассовых сценариев: открытие/закрытие смены, регистрация чека продажи и
возврата, чтение состояния ФН и ОФД-обмена, регистрация ККТ, чтение и
запись device settings.

Используется в backend-проекте `online-receipt/online-api` (Flask + Celery)
для фискализации чеков на физических кассах АТОЛ 30Ф, ATOL Sigma и
совместимых моделях.

- **Текущая версия:** `1.12.2`
- **PyPI:** [`pip install ORD`](https://pypi.org/project/ORD/)
- **Источник истины:** GitLab `online-receipt/online-receipt-driver` (этот репо)
- **Зеркало:** GitHub `brandshopru/online-receipt-driver`
- **Стек:** Python 3.6+, `libfptr10` >=10.10.0
- **Лицензия:** проприетарная, ООО «БШ СТОР» (brandshop.ru)

---

## 1. Установка

```bash
pip install ORD==1.12.2
```

Зависимость от `libfptr10` (Python wrapper) ставится отдельно из дистрибутива
АТОЛ kit (`fptr10-rpc-server*.deb` + `wrappers/python/libfptr10.py`).

Минимальные системные требования:
- Python >=3.6 (3.6 в EOL, рекомендуется 3.8+)
- `libfptr10.so` v10.10.x в `/lib64/` или `LD_LIBRARY_PATH`
- USB-устройство ATOL (`Vendor:Product 2912:0005`) либо TCP/IP подключение
- При работе через `atol-grpc-service` — gRPC мост на `127.0.0.1:4041`/`:4042`

---

## 2. Архитектура

### 2.1. Структура

```
ORD/
├── __init__.py        # public-экспорт классов
├── ord_core.py        # Core (Singleton, IFptr wrapper)
├── ord_cash.py        # Cash (state ККТ, регистрация)
├── ord_fn.py          # Fn (ФН-операции, ОФД-обмен)
├── ord_receipt.py     # Receipt (чек: open/registration/payment/close)
├── ord_shift.py       # Shift (open/close смены)
├── ord_setting.py     # Setting (device settings R/W)
└── exceptions.py      # OrdError, OrdTimeoutError (§6.8 + §6.9, c 1.11.0)
```

### 2.2. Класс `Core` (Singleton)

`Core` — единая точка инициализации `IFptr`. Реализован как Singleton через
метакласс `SingletonMeta` с потокобезопасным `threading.Lock`. На процесс
гарантированно один экземпляр.

**Дефолтные settings** при `Core()`:

| Параметр libfptr10 | Значение |
|---|---|
| `LIBFPTR_SETTING_MODEL` | `LIBFPTR_MODEL_ATOL_AUTO` (автоопределение) |
| `LIBFPTR_SETTING_PORT` | `LIBFPTR_PORT_USB` |
| `LIBFPTR_SETTING_USB_DEVICE_PATH` | `"auto"` |
| `LIBFPTR_SETTING_OFD_CHANNEL` | `LIBFPTR_OFD_CHANNEL_AUTO` |
| `LIBFPTR_SETTING_AUTO_TIME_SYNC` | `True` |
| `LIBFPTR_SETTING_AUTO_TIME_SYNC_TIME` | `3600` (секунд, c 1.12.0; раньше было 15) |

**Instance-level state** `Core.casher_info` (c 1.10.0):
```python
core = Core(path="", casher_name="Иванов И.И.", casher_inn="500100732259")
# или сменить runtime:
core.set_casher("Петров П.П.", inn="...")
```
Используется в `_set_casher()` перед каждой фискальной операцией
(`setParam(1021, name)`, `setParam(1203, inn)`, `operatorLogin()`). Если
`casher_name` пуст — `operatorLogin` пропускается (logon оператора
не происходит, чек/смена пишутся без тегов 1021/1203).

**Жизненный цикл соединения:**
- `__init__` создаёт `IFptr` instance, читает версию, применяет settings
- `open_connect()` → `fptr.open()` — реальный коннект к ККТ
- `close_connect()` → `fptr.close()`
- `__del__` авто-закрывает если `is_opened() == 1`
- `__enter__` / `__exit__` (с 1.12.0) — `with Core() as core:` гарантирует
  `close_connect()` даже при exception между open и close

### 2.3. Доменные классы

`Cash`, `Fn`, `Receipt`, `Shift`, `Setting` — потребители Core. Все принимают
один Core-экземпляр в конструктор и сохраняют `self.fptr = core.fptr`. Если
соединение закрыто, конструктор автоматически вызывает `core.open_connect()`.

```python
from ORD.ord_core import Core
from ORD.ord_receipt import Receipt
from ORD.ord_fn import Fn

core = Core()  # Singleton
core.open_connect()

fn = Fn(core)
receipt = Receipt(core)
# ...
core.close_connect()
```

---

## 3. Типовой жизненный цикл чека

Псевдокод реального флоу из `online-api/jobs/handlers.py`:

```python
core = Core()
core.open_connect()

# 1. Открыть смену (необязательно — первая фискальная операция откроет автоматически)
Shift(core).open_shift()

# 2. Открыть чек
receipt = Receipt(core)
receipt.open_receipt({
    "docType":      "SALE",                     # или "RETURN"
    "printReceipt": False,                      # электронный
    "email":        "client@example.com",
    "taxMode":      "OSN",                      # OSN / USN_INCOME / ...
})

# 3. Регистрация каждой позиции
receipt.receipt_registration({
    "name":             "Кроссовки Nike",
    "price":            10000,
    "quantity":         "1",
    "vatTag":           1102,                   # 1102 — Сумма НДС по ставке 20%
    "vat":              22,                     # ставка 22 → LIBFPTR_TAX_VAT22
    "discSum":          0,
    "nomenclatureCode": "0104680001234567...",  # КМ Data Matrix, опционально
    "codeCheck":        "UUID=...&Time=...",    # результат ПИоТ-проверки, опционально
})

# 4. Оплата
receipt.receipt_payment({"paymentType": "CARD", "sum": 10000})
# paymentType резолвится через Receipt.payment_type → LIBFPTR_PT_* (с 1.8.26.1)

# 5. Закрытие чека → ФН → ОФД
receipt.receipt_close()

# 6. Получить фискальные реквизиты
last = Fn(core).get_info_last_doc()
# → {"document_number": 123, "fiscal_sign": "1234567890", "date_time": "..."}
```

Закрытие смены (Z-отчёт): `Shift(core).close_shift()`.

---

## 4. API Reference

### 4.1. `Core`

| Метод | Возврат | Назначение |
|---|---|---|
| `Core(path: str = '')` | — | Singleton-инициализация IFptr с дефолтными settings |
| `open_connect()` | `bool` | `fptr.open()` — коннект к ККТ |
| `close_connect()` | `bool` | `fptr.close()` |
| `is_opened()` | `int` | 0/1, см. §5.1 — не отражает реальное состояние |
| `get_version_driver()` | `str` | версия libfptr10 |
| `get_current_datetime()` | `str` | `"%Y-%m-%d %H:%M:%S"` от ККТ |
| `get_setting()` | `dict` | текущие device settings |
| `reboot()` | `bool` | `fptr.deviceReboot()` |
| `_set_casher()` | `bool` | оператор тег 1021 + 1203 + `operatorLogin()` (приватный) |
| `_check_document_close()` | `bool` | проверка `checkDocumentClosed`, допечатка (приватный) |
| `_error_log()` | — | пишет `errorCode + errorDescription` в журнал libfptr10 |
| `info_log(msg: str)` | — | пишет INFO в журнал libfptr10 |

### 4.2. `Cash`

| Метод | Возврат | Назначение |
|---|---|---|
| `Cash(core)` | — | Конструктор; авто-`open_connect` если закрыто |
| `get_cash_info()` | `dict` | 13 полей: модель, ФН, версия прошивки, состояние смены, ФФД |
| `get_cash_info_v2()` | `dict` | 31 поле: всё из v1 + флаги принтера/бумаги/ФН/блокировок |
| `get_uptime_cash()` | `int` | секунды непрерывной работы ККТ |
| `cash_registration()` | `bool` | Регистрация ККТ (МГМ). **Хардкод реквизитов brandshop**, см. §5.2 |

`shift_stage` mapping: `0→CLOSED`, `1→OPENED`, `2→EXPIRED`.

### 4.3. `Fn`

| Метод | Возврат | Назначение |
|---|---|---|
| `Fn(core)` | — | Конструктор |
| `get_status_fn()` | `str` | `FNS_INITIAL/CONFIGURED/FISCAL_MODE/POSTFISCAL_MODE/ACCESS_ARCHIVE` |
| `get_info_last_receipt()` | `dict` | номер, тип, сумма, ФПД, datetime последнего чека |
| `get_info_last_doc()` | `dict` | то же для последнего фискального документа |
| `get_version_ffd()` | `dict` | device/fn/min/max версии ФФД (есть баг ключа, см. §5.8) |
| `get_info_doc(number_doc)` | `dict` | тип, номер, ФПД, datetime, has_ofd_ticket для номера ФД |
| `get_ticket_ofd(number_doc)` | `dict` | номер, datetime, OFD ФПД (bytearray) квитанции от ОФД |
| `get_ofd_document_by_number(n)` | `list[dict]` | TLV-структуры ФД (`tag_number`, `tag_name`, `tag_type`, `tag_value`) |
| `get_fn_error()` | `dict` | `network` / `ofd` / `fn` ошибки и их тексты |
| `get_exchange_status()` | `dict` | статус ОФД-обмена, кол-во неотправленных |
| `get_registration_number()` | `str` | РНМ ККТ (тег 1037) |

### 4.4. `Receipt`

| Метод | Возврат | Назначение |
|---|---|---|
| `Receipt(core)` | — | Конструктор |
| `open_receipt(receipt_data)` | `bool` | Открытие чека (см. ниже) |
| `cancel_receipt()` | `bool` | `fptr.cancelReceipt()` — отмена незакрытого чека |
| `receipt_registration(product)` | `bool` | Регистрация одной позиции |
| `receipt_payment(money_position)` | `bool` | Оплата (paymentType через Receipt.payment_type) |
| `receipt_total(money_position)` | `bool` | Регистрация итога (необязательно) |
| `receipt_close()` | `bool` | `fptr.closeReceipt()` → запись в ФН |

**`open_receipt(receipt_data)` ожидает:**

| Ключ | Тип | Значения |
|---|---|---|
| `docType` | str | см. Receipt.receipt_type: SALE/RETURN, SALE_CORRECTION/RETURN_CORRECTION, BUY/BUY_RETURN, BUY_CORRECTION/BUY_RETURN_CORRECTION |
| `printReceipt` | bool | `False` → electronic чек (`LIBFPTR_PARAM_RECEIPT_ELECTRONICALLY=True`) |
| `email` | str | тег 1008 — email клиента |
| `taxMode` | str | `OSN`/`USN_INCOME`/`USN_INCOME_OUTCOME`/`ESN`/`PATENT` |

Жёстко устанавливается тег `1125=1` («признак расчёта в интернете») — это
hard-coded для интернет-магазина brandshop.

**`receipt_registration(product)` ожидает:**

| Ключ | Тип | Назначение |
|---|---|---|
| `name` | str | LIBFPTR_PARAM_COMMODITY_NAME |
| `price` | int/float | LIBFPTR_PARAM_PRICE |
| `quantity` | str | LIBFPTR_PARAM_QUANTITY |
| `vat` | int | см. Receipt.vat_type: 0/5/7/10/20/22 + расчётные 5\/105, 7\/107, 10\/110, 20\/120, 22\/122 + "NO" (без НДС) |
| `discSum` | int | LIBFPTR_PARAM_INFO_DISCOUNT_SUM (если > 0) |
| `nomenclatureCode` | str / None | КМ Data Matrix; запускает `beginMarkingCodeValidation` + polling + `acceptMarkingCode` |
| `codeCheck` | str / None | tag 1265 от ПИоТ; собирает TLV-тег 1260 (отраслевой реквизит предмета расчёта) |
| `vatTag` | int | поле в payload, но не используется в коде |

**Цепочка libfptr10 для маркированного товара** (упрощённо):

```
setParam(MARKING_CODE, km)
setParam(MARKING_CODE_STATUS, 2)
setParam(MARKING_PROCESSING_MODE, 0)
setParam(MEASUREMENT_UNIT, IU_PIECE)
beginMarkingCodeValidation()
while not getParamBool(MARKING_CODE_VALIDATION_READY):
    getMarkingCodeValidationStatus()
validation_result = getParamInt(MARKING_CODE_ONLINE_VALIDATION_RESULT)
acceptMarkingCode()

# Если есть codeCheck (tag 1265 от ПИоТ) — формируется TLV-тег 1260.
# С 1.10.0 значения 1262/1263/1264 параметризованы через product['industry']:
setParam(1262, industry['mode'])      # режим обработки (например, '030')
setParam(1263, industry['date'])      # дата НПА (например, '21.11.2023')
setParam(1264, industry['num'])       # номер НПА (например, '1944')
setParam(1265, codeCheck)
utilFormTlv() → tag 1260 TLV
setParam(1260, tlv)
setParam(1212, 33)                    # признак предмета расчёта: маркированный товар + услуга
```

### 4.5. `Shift`

| Метод | Возврат | Назначение |
|---|---|---|
| `Shift(core)` | — | Конструктор |
| `open_shift()` | `bool` | `_set_casher()` → `fptr.openShift()` → проверка закрытия |
| `close_shift()` | `bool` | `_set_casher()` → `report(LIBFPTR_RT_CLOSE_SHIFT)` → Z-отчёт |

### 4.6. `Setting`

| Метод | Возврат | Назначение |
|---|---|---|
| `Setting(core)` | — | Конструктор (composition, не наследуется от `Core`) |
| `init_setting()` | `bool` | `fptr.initSettings()` |
| `get_device_setting_by_id(id, type)` | `bool/int/str` | Чтение настройки ККТ (`type` ∈ `int`/`string`) |
| `set_device_setting_by_id(id, value)` | `bool` | Запись настройки |
| `commit_setting()` | `bool` | `fptr.commitSettings()` |

---

## 5. Известные ограничения и hidden behavior

Раздел оставлен только под **активные** ограничения by design. Все ранее
описанные баги и архитектурные дефекты закрыты в версиях 1.8.26.1 → 1.12.x
(полная история — в `git log`).

### 5.1. `Core.is_opened()` не отражает реальное состояние — by design libfptr10

Из docstring: «Результат метода не отражает текущее состояние подключения —
если с ККТ была разорвана связь, то метод всё также будет возвращать true».
Это особенность `libfptr10.isOpened()` — она проверяет только локальный флаг
сессии. Реально упало соединение или нет — узнаётся только на первом
вызове, который вернёт `LIBFPTR_ERROR_NO_CONNECTION`. ORD не может это
исправить — поведение задаётся нативной so-библиотекой.

### 5.2. `cash_registration` пишет в FN — by design (необратимо)

Метод реально вызывает `fptr.fnOperation()` с `LIBFPTR_FNOP_REGISTRATION` —
это **необратимая операция перерегистрации ФН**. Использовать только для
первоначальной активации новой ККТ или замены ФН, никогда не дёргать в
тестах. Это документированное поведение API libfptr10; защиту от
случайного вызова не добавляем — драйвер должен быть тонкой обёрткой.

---

## 6. Версионирование и релизы

Используется semver: `MAJOR.MINOR.PATCH`.

- `MAJOR` — несовместимое API изменение
- `MINOR` — новый функционал, обратно совместимый
- `PATCH` — багфиксы

**Процесс релиза:**

1. Внести изменения в feature-ветке (`feature-X.Y.Z` или тематическая)
2. Открыть MR в `master`, дождаться merge
3. Локально: `git checkout master && git pull`, bump `version` в
   `pyproject.toml`
4. Сборка: `python -m build` (артефакты в `dist/`, проверить через
   `twine check dist/*`)
5. Публикация: `twine upload dist/*` (PyPI-токен в `~/.pypirc` или через
   `TWINE_PASSWORD`)
6. (Опционально) Tag: `git tag vX.Y.Z && git push origin --tags`

**Текущая версия:** см. поле `version` в `pyproject.toml` или
`pip show ORD`. Источники синхронизированы:

- GitLab origin (master) — `https://brandshop.gitlab.yandexcloud.net/online-receipt/online-receipt-driver`
- PyPI — `https://pypi.org/project/ORD/`
- Потребитель — `online-api/requirements.txt` (ORD pinned)

**Линии версий:**

- `1.8.x` → `1.9.x` → `1.10.x` → `1.11.x` → `1.12.x` — основная линия,
  все задачи roadmap были закрыты в этом окне (см. `git log` для деталей).
- `1.6.x` (ветка `legacy-1.6.x` в GitLab) — устаревшая, оставлена для
  исторической справки.
- `denis` — старая ветка экспериментов, не мерджена.

---

## 7. Тестирование

Unit-тесты на моках `libfptr10.IFptr` живут в этом же репо в `tests/unit/`,
прогоняются на любой машине без реальной кассы:

```bash
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest tests/unit/ -v
```

E2e-тесты против реальной АТОЛ 30Ф находятся в проекте-потребителе
`online-api` (`tests/e2e/test_fn_real.py`) и запускаются на cashdev:

```bash
ssh volodya@cashdev 'cd /var/www/api/current && \
  ./venv/bin/pytest tests/ -m e2e -v'
```

В CI online-api (pipeline на `development`) e2e прогоняются автоматически
после каждого деплоя (`deploy_all:dev` step 9).

---

## 8. Контакты

- Автор: Vladimir Smirnov (volodya@brandshop.ru)
- Баги/MR: GitLab `online-receipt/online-receipt-driver`
