Metadata-Version: 2.4
Name: ORD
Version: 1.12.1
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.1`
- **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.1
```

Зависимость от `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

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

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

### 5.2. Хардкод бизнес-данных — устранён в 1.9.0 и 1.10.0

Раньше в коде были захардкожены бизнес-значения:

- `Cash.cash_registration()` — ИНН/адрес/ОФД brandshop. **Параметризовано в 1.9.0** (`registration_data: dict`).
- `Receipt.receipt_registration()` для маркированных товаров — теги 1262/1263/1264 = `'030' / '21.11.2023' / '1944'`. **Параметризовано в 1.10.0** (поле `industry` в payload позиции).
- `Core.casher_info` class-level `"СИС. АДМИНИСТРАТОР"`. **Параметризовано в 1.10.0**: `Core(casher_name, casher_inn)` + `Core.set_casher(name, inn)`. Дефолт пустой → `_set_casher()` пропускает `operatorLogin`.

### 5.3. ~~`Setting` наследуется от `Core`~~ — исправлено в 1.10.0

Раньше `class Setting(Core)` + одновременно `self.core = core` — двойственная связь, бьётся с SingletonMeta. С 1.10.0 — чистая композиция: `class Setting:` с делегированием логирования через `self.core._error_log()`. См. §6.6.

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

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

### ~~5.5. `casher_info` class-level~~ — исправлено в 1.10.0

`Core.casher_info` теперь instance-level (`Core(casher_name=..., casher_inn=...)`
+ метод `set_casher(name, inn)`). См. §6.7.

### ~~5.6. Молчаливая обработка ошибок~~ — исправлено в 1.11.0

Опционально через `Core(raise_on_error=True)` методы поднимают
`OrdError(code, description, method)` вместо `return False`. Дефолт
`raise_on_error=False` — backward compatible. См. §6.8.

### ~~5.7. `_check_document_close` infinite loop~~ — исправлено в 1.11.0

Заменён на deadline-loop с параметром `timeout: float = 30.0` и
`OrdTimeoutError` при превышении. См. §6.9.

### ~~5.8. `get_version_ffd` баг ключа~~ — исправлено в 1.12.1

В `ord_fn.py:174` ключ `"document_number"` заменён на корректный
`"min_ffd_version"`. Теперь `Fn.get_version_ffd()` возвращает dict с
полным набором заявленных ключей.

---

## 6. Roadmap (закрыт)

Все задачи раздела закрыты, версии указаны рядом.
- ✅ **§6.1 paymentType маппинг** — в `1.8.26.1` (`Receipt.payment_type` dict)
- ✅ **§6.2 VAT mapping** — в `1.8.27.0` (`Receipt.vat_type` dict, 0/5/7/10/20/22 + расчётные + NO)
- ✅ **§6.3 docType коррекции и расход** — в `1.8.27.0` (`Receipt.receipt_type` dict с 8 типами)
- ✅ **§6.4 Cash.cash_registration параметризация** — в `1.9.0`. Принимает `registration_data: dict` (inn/address/org_name/email/place/reg_number/ofd_inn/ofd_name + tax_system/agent_type/ffd_version/tax_site/flags). Breaking change: дефолтов brandshop больше нет, ValueError при отсутствии обязательных полей.
- ✅ **§6.5 Параметризация отраслевого реквизита (тег 1260)** — в `1.10.0`. `Receipt.receipt_registration` принимает поле `industry` (`{mode, date, num}` для тегов 1262/1263/1264). Breaking change: при наличии `codeCheck` блок `industry` обязателен, иначе ValueError.
- ✅ **§6.6 `Setting(Core)` → `Setting`** — в `1.10.0`. Убрано наследование от Core, теперь composition (`self.core._error_log()`), убран `__del__` (Setting не владеет соединением).
- ✅ **§6.7 `casher_info` instance-level** — в `1.10.0`. `Core(path, casher_name='', casher_inn='')` + метод `Core.set_casher(name, inn='')`. Класс-атрибут с brandshop-хардкодом удалён. Backward-compat: при пустом `casher_name` метод `_set_casher` пропускает `operatorLogin` (фискальные операции работают без logon оператора).
- ✅ **§6.8 Опциональные исключения** — в `1.11.0`. `Core(..., raise_on_error=True)` поднимает `OrdError(code, description, method)` вместо тихого `return False`. Helper `Core._handle_error(method)` объединяет log + raise/return. Дефолт `raise_on_error=False` — backward compatible.
- ✅ **§6.9 Timeout в `_check_document_close`** — в `1.11.0`. Бесконечные `while ... < 0: continue` заменены на deadline-loop с параметром `timeout: float = 30.0`. При превышении — `OrdTimeoutError` (всегда, независимо от `raise_on_error`).
- ✅ **§6.10 Тесты** — в `1.11.0`. Структура `tests/unit/*.py` с 17 unit-тестами (Core/Receipt/Cash, ~70% покрытия публичного API). `libfptr10.IFptr` подменяется MagicMock'ом в conftest.py — тесты идут без реальной кассы. Dev-зависимости в `requirements-dev.txt`. Integration-тесты против реальной АТОЛ 30Ф — отдельным этапом.
- ✅ **§6.11 `pyproject.toml`** — в `1.12.0`. Перевели проект на PEP 517 build (`setuptools.build_meta`). `setup.py` + `setup.cfg` удалены, метаданные в `pyproject.toml`. Сборка через `python -m build`.
- ✅ **§6.12 Type hints (частично)** — в `1.12.0`. Добавлены `return`-аннотации публичным методам Core/Cash/Fn/Receipt/Shift/Setting и type hints параметров. `TypedDict` для CashInfo/FnDocInfo — не делали (низкий приоритет, оставляем `dict`).
- ✅ **§6.16 Контекст-менеджеры** — в `1.12.0`. `Core.__enter__`/`__exit__` — `with Core(...) as core:` гарантирует `close_connect()` даже при exception между open и close.
- ✅ **§6.17 Thread-safety** — в `1.12.0`. Документировано «один процесс, один поток» как требование. Добавлен `Core._operation_lock = threading.Lock()` для опционального оборачивания критической секции извне: `with core._operation_lock: ...`. Каждый публичный метод НЕ оборачивается мьютексом, чтобы не платить за однопоточные сценарии.
- ✅ **§6.18 AutoTimeSyncTime** — в `1.12.0`. `LIBFPTR_SETTING_AUTO_TIME_SYNC_TIME = 3600` (раз в час, было 15с). Компромисс между «реже трогаем ФН» и «успеваем поймать drift».
- ✅ **§6.19 Стандартный logging** — в `1.12.0`. Добавлен `logging.getLogger("ORD")`, ошибки и info-сообщения теперь пишутся **и** в журнал libfptr10, **и** в системный logger приложения.
- ✅ **§6.20 Документация кода (частично)** — в `1.12.0`. Docstrings главных методов Core/Receipt приведены к единому стилю (Russian-comment + :param/:return), новые публичные методы (`_handle_error`, `set_casher`, `__enter__`/`__exit__`) задокументированы. Полное прохождение по Fn/Cash — оставляем (низкий приоритет).
- ✅ **§6.21 Опечатка `cancel_reciept.py`** — файл не существует (был удалён ранее). Закрываем без изменений.
- ✅ **§6.22 .gitignore** — в `1.11.0` (`__pycache__/`, `*.pyc`, `*.egg-info/`, `dist/`, `build/`, `.pytest_cache/`, `venv/`).

---

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

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

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

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

1. Внести изменения в feature-ветке
2. Merge в `master` после ревью
3. Bump версии в `setup.py` (потом — в `pyproject.toml`)
4. Поставить tag: `git tag X.Y.Z && git push origin --tags`
5. Сборка: `python -m build`
6. Публикация: `twine upload dist/*`

**Текущая ситуация (на 2026-06-25):**

| Источник | Версия master |
|---|---|
| Локально (`IdeaProjects/online-receipt-driver`) | 1.8.26.0 |
| GitLab origin (master) | 1.8.26.0 |
| GitHub зеркало (master) | 1.8.26.0 |
| PyPI ORD | 1.8.26.0 |
| Production: `online-api/venv` на cashdev | 1.8.26.0 |

Все источники синхронизированы.

История версий:
- `1.8.x` — текущая линия (НДС 22%, AutoTimeSync, разрешительный режим 030)
- `1.6.x` (ветка `legacy-1.6.x` в GitLab) — заброшенная ветка с устаревшим
  API, оставлена для исторической справки
- `denis` — старая ветка экспериментов, не мерджена

---

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

До настоящего момента тесты для ORD делались **только в проекте-потребителе
`online-api`** (см. `online-api/tests/unit/test_piot_client_mock.py`,
`tests/e2e/test_fn_real.py`). Прогон против реальной АТОЛ 30Ф на cashdev:

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

После §6.10 тесты переедут в этот репо.

---

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

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