Metadata-Version: 2.4
Name: ORD
Version: 1.8.27.0
Summary: Module for working with the ATOL cash register driver
Home-page: https://github.com/brandshopru
Author: Vladimir Smirnov
Author-email: volodya@brandshop.ru
Description-Content-Type: text/markdown
Dynamic: author
Dynamic: author-email
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: summary

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

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

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

- **Текущая версия:** `1.8.27.0`
- **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.8.27.0
```

Зависимость от `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)
└── test/              # пока пустой шаблон, см. §6.10
```

### 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` | `15` (секунд) |

**Class-level state** `Core.casher_info`:
```python
casher_info = {"name": "СИС. АДМИНИСТРАТОР", "inn": ""}
```
Используется в `_set_casher()` перед каждой фискальной операцией
(`setParam(1021, name)`, `setParam(1203, inn)`, `operatorLogin()`). См.
§5.5 — это известное ограничение.

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

### 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:
setParam(1262, '030')                 # хардкод режима, см. §6.5
setParam(1263, '21.11.2023')          # хардкод даты, см. §6.5
setParam(1264, '1944')                # хардкод номера, см. §6.5
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)` | — | Конструктор (наследуется от `Core`, см. §5.3) |
| `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()` не отражает реальное состояние

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

### 5.2. Хардкод бизнес-данных brandshop

Несколько мест в коде содержат **жёстко зашитые реквизиты ООО «БШ СТОР»**:

- `Cash.cash_registration()` — ИНН `9705112246`, адрес «127051, г. Москва,
  Петровский б-р, 21», email `volodya@brandshop.ru`, ОФД `nalog.ru`,
  ОФД-ИНН `9715260691`, ОФД-имя «ООО ПС СТ», тег 1037 = `0000000001030584`
- `Receipt.receipt_registration()` для маркированных товаров:
  тег 1262 = `'030'`, тег 1263 = `'21.11.2023'`, тег 1264 = `'1944'`
- `Core.casher_info` class-level: `"СИС. АДМИНИСТРАТОР"`

Эти константы не должны быть в opensource-обёртке. См. §6.4, §6.5.

### 5.3. `Setting` наследуется от `Core`

```python
class Setting(Core):
    def __init__(self, core):
        self.core = core
        ...
```

Странная конструкция: `Setting` одновременно наследник `Core`
(`class Setting(Core):`) и хранит ссылку на core-экземпляр (`self.core =`).
Из-за SingletonMeta `Setting(core)` создаст другой Singleton-ключ, и
`Setting.is_opened()` будет работать с собственным `self.fptr`
(унаследованным от `Core.__init__` во время «первого» вызова класса).
Архитектурный дефект. См. §6.6.

### 5.4. `cash_registration` пишет в FN

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

### 5.5. `casher_info` class-level

Реквизиты кассира (`name`, `inn`) — это class-level переменная `Core`,
а не instance. Это значит, что изменение из одного места приложения
повлияет на все будущие вызовы `_set_casher()`. Для multi-tenant сценариев
(несколько касс в одном процессе) — баг.

### 5.6. Молчаливая обработка ошибок

Все методы возвращают `False` при ошибке `libfptr10` и пишут описание через
`logWrite`. Никаких исключений. Это удобно для batch-обработки чеков, но
затрудняет отладку — приходится парсить логи libfptr10 (`/var/log/...`) и
коррелировать с приложением. См. §6.8.

### 5.7. `_check_document_close` infinite loop

Цикл `while self.fptr.checkDocumentClosed() < 0: ...` без timeout. Если ККТ
отвалилась (`LIBFPTR_ERROR_NO_CONNECTION`) — поток зависнет навсегда.
Аналогично для `while self.fptr.continuePrint() < 0`. См. §6.9.

### 5.8. `get_version_ffd` баг ключа

В строке 334 `ord_fn.py`:
```python
version_ffd.update({"document_number": self.ffd_version.get(min_ffd_version)})
```
Ключ должен быть `"min_ffd_version"`, а не `"document_number"`. Опечатка
скрывает значение `min_ffd_version` в возвращаемом dict.

---

## 6. Задачи на доработку (TODO)

Сделанные задачи:
- ✅ **§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 типами)

Также соответствующие пункты в §5 (Hidden behavior) больше не актуальны — см.
актуальное поведение в `Receipt.payment_type`/`vat_type`/`receipt_type`.

### 6.4. Вынести бизнес-реквизиты brandshop в конфигурацию

**Приоритет:** P1.

`Cash.cash_registration()` хардкодит ИНН/адрес/ОФД конкретной организации.
Принимать параметром `registration_data: dict` либо читать из ENV
(`ORD_KKT_INN`, `ORD_KKT_ADDRESS`, `ORD_OFD_*`).

### 6.5. Параметризовать отраслевой реквизит (тег 1260)

**Приоритет:** P1.

В `receipt_registration` теги 1262/1263/1264 хардкод (`'030'`,
`'21.11.2023'`, `'1944'`). Это **режим обработки 030** = разрешительный
режим, дата документа НПА = 21 ноября 2023, номер = 1944. Это конкретный
НПА. Должно приходить из payload:

```python
"industry": {
    "mode":  "030",
    "date":  "2023-11-21",
    "num":   "1944",
    # tag 1265 — это codeCheck от ПИоТ, остаётся как есть
}
```

### 6.6. Рефакторинг `Setting(Core)` → `Setting`

**Приоритет:** P2.

Убрать наследование от `Core`, использовать composition (как у остальных
доменных классов: `Cash(core)`, `Fn(core)`, `Receipt(core)`).

### 6.7. `casher_info` — instance-level

**Приоритет:** P1.

Перевести `Core.casher_info` из class-level в instance + принимать в
конструкторе. Альтернатива — передавать в `open_receipt`/`open_shift` и
ставить только локально в рамках конкретной операции.

### 6.8. Опциональные исключения вместо `False`

**Приоритет:** P2.

Добавить параметр `Core(raise_on_error=True)`. При активации все методы
поднимают `OrdError(error_code, error_description)` вместо `return False`.
Сохранить дефолт `False` для обратной совместимости. Это даст:
- Stacktrace в логах приложения
- Возможность `except OrdError` для конкретных кодов libfptr10
- Прекращение «тихих фейлов» когда `False` игнорируется вызывающим кодом

### 6.9. Timeout в `_check_document_close` / `continuePrint`

**Приоритет:** P1.

Бесконечные `while ... < 0: continue` без выхода. Заменить на:

```python
import time
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
    if self.fptr.checkDocumentClosed() >= 0:
        break
    self._error_log()
    time.sleep(0.5)
else:
    raise OrdTimeoutError(...)
```

### 6.10. Тесты

**Приоритет:** P0.

`ORD/test/ORD_test.py` сейчас 207 байт (пустой шаблон). Добавить:
- **Unit-тесты** с моком `IFptr` — каждый метод проверяет правильность
  setParam-цепочки (без реальной кассы). Минимум 30 тестов.
- **Integration-тесты** против реальной АТОЛ 30Ф (cashdev,
  `172.16.0.7`) — реальные пробития чеков, чтение ФН. Маркер
  `@pytest.mark.real_kkt`, skip-by-default.
- Покрытие минимум 70%.

Использовать `pytest` + `pytest-mock`. Сейчас вообще нет покрытия.

### 6.11. `pyproject.toml` вместо `setup.py`

**Приоритет:** P2.

Перейти на PEP 517 build (`pyproject.toml` + `setuptools.build_meta`).
Убрать `setup.py`+`setup.cfg`, перенести метаданные:

```toml
[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "ORD"
version = "1.9.0"
description = "Python client for ATOL libfptr10 fiscal printer driver"
authors = [{name = "Vladimir Smirnov", email = "volodya@brandshop.ru"}]
readme = "README.md"
license = {text = "Proprietary"}
requires-python = ">=3.8"
dependencies = []  # libfptr10 ставится отдельно

[project.urls]
Repository = "https://brandshop.gitlab.yandexcloud.net/online-receipt/online-receipt-driver"
Mirror = "https://github.com/brandshopru/online-receipt-driver"
```

### 6.12. Type hints

**Приоритет:** P3.

Сейчас код без аннотаций. Добавить:
- Возвращаемые типы у всех публичных методов
- `TypedDict` или `dataclass` для словарей-результатов
  (`CashInfo`, `FnDocInfo`, `OfdExchangeStatus`, ...)
- Конкретные перечисления для `docType`, `paymentType`, `taxMode` через
  `enum.StrEnum` (Python 3.11) или `Literal[...]`

### 6.13. Обновить минимальную версию Python: 3.6 → 3.8+

**Приоритет:** P2.

Python 3.6 в EOL с 23 декабря 2021. На cashdev сейчас стоит 3.6.8.
Согласовать миграцию: 3.8 или 3.10 как минимум. Это снимет ограничения
mock-3.0.5 / pytest-6.2.5 в проекте-потребителе `online-api`.

### 6.14. CI/CD в GitLab

**Приоритет:** P1.

Добавить `.gitlab-ci.yml`:
- **lint** — `ruff check ORD/` + `mypy ORD/`
- **test** — `pytest tests/` (без `real_kkt` маркера)
- **build** — `python -m build` → артефакты `dist/*.whl + *.tar.gz`
- **publish** — на tag запускать `twine upload dist/* -u __token__ -p $PYPI_TOKEN`
- **mirror** — push на GitHub после успешного релиза в master

### 6.15. CHANGELOG.md

**Приоритет:** P2.

Сейчас отсутствует. Восстановить ретроспективно (по тэгам 1.6.x — 1.8.x)
и продолжать в формате [Keep a Changelog](https://keepachangelog.com/).

### 6.16. Контекст-менеджеры

**Приоритет:** P3.

Поддержать `with Core() as core:` (`__enter__`/`__exit__`). Гарантирует
закрытие коннекта даже при exception между open и close. Сейчас полагается
на `__del__`, который не вызывается при `kill -9`.

### 6.17. Thread-safety guarantee

**Приоритет:** P2.

`SingletonMeta` потокобезопасен на создание. Но **операции** над одним
`fptr` — нет: setParam → queryData → getParam — это «stateful цепочка», и
если две задачи параллельно зовут разные методы, состояние перемешается.

Опции:
- Документировать «один процесс, один поток» как требование
- Добавить `Core._operation_lock = threading.Lock()` и оборачивать им
  каждый публичный метод
- Сделать `Core` per-thread (`threading.local`)

### 6.18. `AutoTimeSyncTime`: 15 секунд избыточно

**Приоритет:** P3.

`LIBFPTR_SETTING_AUTO_TIME_SYNC_TIME = 15` означает синк раз в 15 секунд —
это нагружает ФН без необходимости. ФНС требует синхронизацию **раз в
сутки**. Поставить `3600` (раз в час) или `86400` (раз в сутки).

### 6.19. Логирование в стандартный logging

**Приоритет:** P2.

Сейчас `_error_log`/`info_log` пишут через `fptr.logWrite` — внутренний
журнал libfptr10. Добавить параллельно вывод в стандартный
`logging.getLogger("ORD")` чтобы интегрироваться с системным логированием
приложения.

### 6.20. Документация кода

**Приоритет:** P3.

Docstrings есть, но в смешанном стиле (русский комментарий с английским
именем параметров, иногда отсутствие `:param:`/`:return:`). Привести к
единому формату (Google или Numpy style).

### 6.21. Исправить опечатку `cancel_reciept.py`

**Приоритет:** P3.

Файл в корне репо называется `cancel_reciept.py` (reciept → receipt).
Переименовать.

### 6.22. Удалить артефакты из `.gitignore`

**Приоритет:** P3.

`ORD.egg-info/`, `build/`, `dist/`, `__pycache__/`, `venv/` сейчас
неотслеживаемые, но не добавлены в `.gitignore`. Добавить.

---

## 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/*`
7. После §6.14 — CI делает всё начиная с tag автоматически

**Текущая ситуация (на 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`
