diff --git a/docs/superpowers/plans/2026-03-20-moysklad-orders-sync.md b/docs/superpowers/plans/2026-03-20-moysklad-orders-sync.md new file mode 100644 index 00000000..5917559d --- /dev/null +++ b/docs/superpowers/plans/2026-03-20-moysklad-orders-sync.md @@ -0,0 +1,754 @@ +# МойСклад: синхронизация заказов покупателей — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Добавить синхронизацию заказов покупателей (customerorder) из МойСклада в очередь комплектации наравне с перемещениями. + +**Architecture:** Расширяем существующий `Picking List` полем `source_type`, добавляем префикс `move:`/`order:` к `ms_id` (снимаем unique-констрейнт), добавляем `sync_picking_orders()` и `sync_all()` в `api.py`, обновляем UI попапа. + +**Tech Stack:** Python (Frappe), JSON DocType definitions, vanilla JS (jQuery/Frappe dialogs) + +--- + +## Файловая структура + +| Файл | Действие | Что меняем | +|------|----------|------------| +| `/home/mkr/picking_app/picking/doctype/picking_settings/picking_settings.json` | Modify | Добавить поле `ms_order_state` | +| `/home/mkr/picking_app/picking/doctype/picking_list/picking_list.json` | Modify | Добавить `source_type`, убрать `unique` с `ms_id` | +| `/home/mkr/picking_app/picking/api.py` | Modify | `sync_picking_list()` backfill, новые `sync_picking_orders()` + `sync_all()`, фикс бага `added_rows`, обновить `get_picking_list_items()` | +| `/home/mkr/picking_app/picking/page/picking_doc/picking_doc.js` | Modify | UI: кнопка → `sync_all`, колонка Тип, фильтр-табы | + +> **Важно:** `/home/mkr/picking_app/` смонтирован как read-only том в контейнер `frappe-project-backend-1` в `/home/frappe/frappe-bench/apps/picking_app/picking_app/`. Все изменения делаются в файлах на хосте. После изменений JS нужно пересобрать: `docker exec frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local migrate"`. После изменений JSON-схем нужен `bench migrate`. После изменений JS — `bench build --app picking_app` или перезагрузка страницы в dev-режиме. + +--- + +## Task 1: Добавить поле `ms_order_state` в Picking Settings + +**Files:** +- Modify: `/home/mkr/picking_app/picking/doctype/picking_settings/picking_settings.json` + +- [ ] **Step 1: Открыть файл и найти конец массива `fields`** + +```bash +cat /home/mkr/picking_app/picking/doctype/picking_settings/picking_settings.json +``` + +- [ ] **Step 2: Добавить поле после `ms_account_id`** + +В массив `fields` добавить перед закрывающей `]`: + +```json + ,{ + "fieldname": "ms_order_state", + "fieldtype": "Data", + "label": "Статус заказа для синхронизации", + "description": "Название статуса в МоёмСкладе для фильтрации заказов покупателей", + "default": "Подтверждён", + "reqd": 1 + } +``` + +- [ ] **Step 3: Применить миграцию** + +```bash +docker exec frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local migrate" +``` + +Ожидаемый вывод: `Migrating erp.local` … `Done` + +- [ ] **Step 4: Проверить что поле появилось** + +```bash +echo " +result = frappe.db.get_value('Picking Settings', 'Default', 'ms_order_state') +print('ms_order_state:', repr(result)) +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep "ms_order_state:" +``` + +Ожидаемый вывод: `ms_order_state: 'Подтверждён'` + +- [ ] **Step 5: Commit** + +```bash +cd /home/mkr/picking_app +git add picking/doctype/picking_settings/picking_settings.json +git commit -m "feat: add ms_order_state field to Picking Settings" +``` + +--- + +## Task 2: Обновить Picking List — добавить `source_type`, убрать `unique` с `ms_id` + +**Files:** +- Modify: `/home/mkr/picking_app/picking/doctype/picking_list/picking_list.json` + +- [ ] **Step 1: Убрать `"unique": 1` из поля `ms_id`** + +В `picking_list.json` найти блок поля `ms_id`: +```json +{ + "fieldname": "ms_id", + "fieldtype": "Data", + "label": "МойСклад ID", + "unique": 1, + "reqd": 1 +} +``` +Изменить на: +```json +{ + "fieldname": "ms_id", + "fieldtype": "Data", + "label": "МойСклад ID", + "reqd": 1 +} +``` + +- [ ] **Step 2: Добавить поле `source_type` после поля `synced_at`** + +После блока `synced_at`, перед `items`: +```json + ,{ + "fieldname": "source_type", + "fieldtype": "Select", + "label": "Тип источника", + "options": "\nMove\nOrder", + "default": "Move" + } +``` + +- [ ] **Step 3: Применить миграцию** + +```bash +docker exec frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local migrate" +``` + +- [ ] **Step 4: Проверить что поле создалось и unique снят** + +```bash +echo " +exec(''' +import frappe +# Проверить что source_type существует +cols = frappe.db.get_table_columns(\"tabPicking List\") +print(\"source_type in columns:\", \"source_type\" in cols) + +# Проверить что unique снят — должна пройти вставка двух записей с одинаковым ms_id (НЕ делаем, просто проверим мета) +meta = frappe.get_meta(\"Picking List\") +ms_id_field = next((f for f in meta.fields if f.fieldname == \"ms_id\"), None) +print(\"ms_id unique:\", getattr(ms_id_field, \"unique\", None)) +''') +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep -E "source_type|ms_id unique" +``` + +Ожидаемый вывод: +``` +source_type in columns: True +ms_id unique: 0 +``` + +- [ ] **Step 5: Commit** + +```bash +cd /home/mkr/picking_app +git add picking/doctype/picking_list/picking_list.json +git commit -m "feat: add source_type to Picking List, remove unique from ms_id" +``` + +--- + +## Task 3: Миграция существующих записей Picking List — добавить префикс `move:` к ms_id + +**Files:** +- Нет изменений в файлах — только разовая миграция данных в БД + +> **Выполнить до Task 4.** Без этого шага после обновления `sync_picking_list()` существующие записи с чистыми UUID не будут найдены при поиске по `move:{uuid}` — возникнут дубликаты. + +- [ ] **Step 1: Проверить количество записей для миграции** + +```bash +echo " +exec(''' +import frappe +total = frappe.db.count(\"Picking List\") +already_prefixed = frappe.db.sql(\"SELECT COUNT(*) FROM \`tabPicking List\` WHERE ms_id LIKE \\\"move:%\\\" OR ms_id LIKE \\\"order:%\\\"\")[0][0] +needs_migration = total - already_prefixed +print(f\"Всего: {total}, уже с префиксом: {already_prefixed}, требуют миграции: {needs_migration}\") +''', globals()) +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep "Всего:" +``` + +- [ ] **Step 2: Выполнить миграцию** + +```bash +echo " +exec(''' +import frappe +rows = frappe.db.sql( + \"SELECT name, ms_id FROM \`tabPicking List\` WHERE ms_id NOT LIKE \\\"move:%\\\" AND ms_id NOT LIKE \\\"order:%\\\"\", + as_dict=True +) +updated = 0 +for row in rows: + new_ms_id = f\"move:{row.ms_id}\" + frappe.db.set_value(\"Picking List\", row.name, \"ms_id\", new_ms_id) + updated += 1 +frappe.db.commit() +print(f\"Обновлено записей: {updated}\") +''', globals()) +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep "Обновлено" +``` + +Ожидаемый вывод: `Обновлено записей: N` (N ≥ 0, не ошибка) + +- [ ] **Step 3: Верифицировать — не должно остаться записей без префикса** + +```bash +echo " +exec(''' +import frappe +orphans = frappe.db.sql( + \"SELECT COUNT(*) FROM \`tabPicking List\` WHERE ms_id NOT LIKE \\\"move:%\\\" AND ms_id NOT LIKE \\\"order:%\\\"\", +)[0][0] +print(\"Без префикса:\", orphans) +''', globals()) +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep "Без префикса:" +``` + +Ожидаемый вывод: `Без префикса: 0` + +--- + +## Task 4: Обновить `sync_picking_list()` — prefixed ms_id + backfill source_type + +**Files:** +- Modify: `/home/mkr/picking_app/picking/api.py` + +Текущая логика `sync_picking_list()` ищет записи через `frappe.db.get_value("Picking List", {"ms_id": ms_id}, "name")` где `ms_id` — чистый UUID. После этого изменения поиск и хранение будут через `move:{uuid}`. + +- [ ] **Step 1: Найти строку с `ms_id = move.get("id")`** + +```bash +grep -n "ms_id\|existing\|source_type" /home/mkr/picking_app/picking/api.py | head -30 +``` + +- [ ] **Step 2: Обновить функцию `sync_picking_list()`** + +Найти блок (примерно строки 298–365 в api.py): +```python + ms_id = move.get("id") + if not ms_id: + continue + ... + existing = frappe.db.get_value("Picking List", {"ms_id": ms_id}, "name") + if existing: + pl = frappe.get_doc("Picking List", existing) + pl.ms_number = ms_number + ... + pl.save(ignore_permissions=True) + updated += 1 + else: + frappe.get_doc({ + "doctype": "Picking List", + "ms_id": ms_id, + ... + }).insert(ignore_permissions=True) + created += 1 +``` + +Изменить на: +```python + raw_id = move.get("id") + if not raw_id: + continue + ms_id = f"move:{raw_id}" + ... + existing = frappe.db.get_value("Picking List", {"ms_id": ms_id}, "name") + if existing: + pl = frappe.get_doc("Picking List", existing) + pl.ms_number = ms_number + pl.ms_date = ms_date + pl.from_warehouse = from_wh + pl.to_warehouse = to_wh + pl.synced_at = synced_at + pl.source_type = "Move" # backfill + if pl.status != "Added": + _update_pl_items(pl, positions) + pl.save(ignore_permissions=True) + updated += 1 + else: + frappe.get_doc({ + "doctype": "Picking List", + "ms_id": ms_id, + "ms_number": ms_number, + "ms_date": ms_date, + "from_warehouse": from_wh, + "to_warehouse": to_wh, + "status": "Draft", + "source_type": "Move", + "synced_at": synced_at, + "items": [_item_from_position(p) for p in positions], + }).insert(ignore_permissions=True) + created += 1 +``` + +- [ ] **Step 3: Проверить синтаксис** + +```bash +python3 -c "import ast; ast.parse(open('/home/mkr/picking_app/picking/api.py').read()); print('OK')" +``` + +- [ ] **Step 4: Commit** + +```bash +cd /home/mkr/picking_app +git add picking/api.py +git commit -m "feat: prefix ms_id with move:, backfill source_type in sync_picking_list" +``` + +--- + +## Task 5: Добавить `sync_picking_orders()` и `sync_all()` + +**Files:** +- Modify: `/home/mkr/picking_app/picking/api.py` + +> **Порядок шагов важен:** сначала обновляем `_ms_fetch_all()` (Step 1), затем добавляем `sync_picking_orders()` которая его использует (Step 2). + +- [ ] **Step 1: Обновить `_ms_fetch_all()` — добавить параметр `max_pages`** + +Найти функцию `_ms_fetch_all`: +```python +def _ms_fetch_all(endpoint, params): + """Получить все строки с автопагинацией через nextHref.""" + headers = _ms_headers() + rows = [] + url = f"{MS_BASE}/{endpoint}" + while url: + data = _ms_get(url, params=params, headers=headers) + rows.extend(data.get("rows", [])) + url = data.get("meta", {}).get("nextHref") + params = None + return rows +``` + +Изменить на: +```python +def _ms_fetch_all(endpoint, params, max_pages=None): + """Получить все строки с автопагинацией через nextHref. + + max_pages: ограничить количество страниц (None = без ограничения). + """ + headers = _ms_headers() + rows = [] + url = f"{MS_BASE}/{endpoint}" + page = 0 + while url: + if max_pages and page >= max_pages: + break + data = _ms_get(url, params=params, headers=headers) + rows.extend(data.get("rows", [])) + url = data.get("meta", {}).get("nextHref") + params = None + page += 1 + return rows +``` + +- [ ] **Step 2: Добавить функцию `sync_picking_orders()` после `sync_picking_list()`** + +Вставить после функции `sync_picking_list()` (перед `def _update_pl_items`): + +```python +@frappe.whitelist() +def sync_picking_orders(): + """Синхронизировать заказы покупателей из МоёгоСклада в Picking List. + + Фильтрация по state.name == ms_order_state (из Picking Settings). + Лимит: 10 страниц × 100 заказов = 1000 за синхронизацию. + """ + settings_name = frappe.db.get_value("Picking Settings", {}, "name") + order_state = ( + frappe.db.get_value("Picking Settings", settings_name, "ms_order_state") + if settings_name else None + ) or "Подтверждён" + + orders = _ms_fetch_all("entity/customerorder", { + "limit": 100, + "order": "moment,desc", + "expand": "state,store,positions.assortment,positions.assortment.uom", + }, max_pages=10) + + wh_cache = _build_wh_cache() + synced_at = now_datetime() + created = updated = 0 + + for order in orders: + state_obj = order.get("state") or {} + if state_obj.get("name") != order_state: + continue + + raw_id = order.get("id") + if not raw_id: + continue + ms_id = f"order:{raw_id}" + + ms_number = order.get("name", "") + ms_date = _parse_ms_datetime(order.get("moment", "")) + + store_id, store_name = _extract_store_id(order.get("store")) + to_wh = wh_cache.get(store_id) or wh_cache.get(store_name) + + positions = _positions_from_move(order) + + existing = frappe.db.get_value("Picking List", {"ms_id": ms_id}, "name") + if existing: + pl = frappe.get_doc("Picking List", existing) + pl.ms_number = ms_number + pl.ms_date = ms_date + pl.to_warehouse = to_wh + pl.synced_at = synced_at + pl.source_type = "Order" + if pl.status != "Added": + _update_pl_items(pl, positions) + pl.save(ignore_permissions=True) + updated += 1 + else: + frappe.get_doc({ + "doctype": "Picking List", + "ms_id": ms_id, + "ms_number": ms_number, + "ms_date": ms_date, + "to_warehouse": to_wh, + "status": "Draft", + "source_type": "Order", + "synced_at": synced_at, + "items": [_item_from_position(p) for p in positions], + }).insert(ignore_permissions=True) + created += 1 + + frappe.db.commit() + return {"created": created, "updated": updated, "total": len(orders)} + + +@frappe.whitelist() +def sync_all(): + """Синхронизировать перемещения и заказы покупателей из МоёгоСклада.""" + moves = sync_picking_list() + orders = sync_picking_orders() + return { + "status": "ok", + "moves": {"created": moves["created"], "updated": moves["updated"]}, + "orders": {"created": orders["created"], "updated": orders["updated"]}, + } +``` + +- [ ] **Step 3: Проверить синтаксис** + +```bash +python3 -c "import ast; ast.parse(open('/home/mkr/picking_app/picking/api.py').read()); print('OK')" +``` + +- [ ] **Step 4: Проверить что функции доступны через bench** + +```bash +echo " +exec(''' +import picking_app.picking.api as api +print(\"sync_picking_orders:\", callable(api.sync_picking_orders)) +print(\"sync_all:\", callable(api.sync_all)) +''') +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep -E "sync_picking|sync_all" +``` + +Ожидаемый вывод: +``` +sync_picking_orders: True +sync_all: True +``` + +- [ ] **Step 5: Commit** + +```bash +cd /home/mkr/picking_app +git add picking/api.py +git commit -m "feat: add sync_picking_orders() and sync_all()" +``` + +--- + +## Task 6: Исправить баг `added_rows` и обновить `get_picking_list_items()` + +**Files:** +- Modify: `/home/mkr/picking_app/picking/api.py` + +- [ ] **Step 1: Исправить `added_rows` → `added_row_names` в `add_items_from_picking_list()`** + +Найти строку (примерно строка 462): +```python + return {"status": "ok", "added": len(added_rows), "items": updated_items} +``` +Изменить на: +```python + return {"status": "ok", "added": len(added_row_names), "items": updated_items} +``` + +- [ ] **Step 2: Обновить `get_picking_list_items()` — добавить параметр `source_type` и поле в ответ** + +Найти функцию: +```python +@frappe.whitelist() +def get_picking_list_items(from_warehouse=None, to_warehouse=None): + """Вернуть Picking List для попап-диалога (только не полностью добавленные).""" + filters = {"status": ["!=", "Added"]} + if from_warehouse: + filters["from_warehouse"] = from_warehouse + if to_warehouse: + filters["to_warehouse"] = to_warehouse + + pls = frappe.get_all( + "Picking List", + filters=filters, + fields=["name", "ms_id", "ms_number", "ms_date", "from_warehouse", "to_warehouse", "status", "synced_at"], + order_by="ms_date desc", + limit=200 + ) +``` + +Изменить на: +```python +@frappe.whitelist() +def get_picking_list_items(from_warehouse=None, to_warehouse=None, source_type=None): + """Вернуть Picking List для попап-диалога (только не полностью добавленные). + + source_type=None — все записи (включая legacy без source_type). + source_type="Move" — Move + NULL (обратная совместимость). + source_type="Order" — только Order. + """ + filters = {"status": ["!=", "Added"]} + if from_warehouse: + filters["from_warehouse"] = from_warehouse + if to_warehouse: + filters["to_warehouse"] = to_warehouse + if source_type == "Order": + filters["source_type"] = "Order" + elif source_type == "Move": + # Frappe хранит пустой Select как пустую строку, не NULL + # Пустая строка = legacy-запись до добавления source_type (считается Move) + filters["source_type"] = ["in", ["Move", ""]] + + pls = frappe.get_all( + "Picking List", + filters=filters, + fields=["name", "ms_id", "ms_number", "ms_date", "from_warehouse", + "to_warehouse", "status", "synced_at", "source_type"], + order_by="ms_date desc", + limit=200 + ) +``` + +- [ ] **Step 3: Проверить синтаксис** + +```bash +python3 -c "import ast; ast.parse(open('/home/mkr/picking_app/picking/api.py').read()); print('OK')" +``` + +- [ ] **Step 4: Commit** + +```bash +cd /home/mkr/picking_app +git add picking/api.py +git commit -m "fix: added_rows NameError in add_items_from_picking_list; feat: source_type filter in get_picking_list_items" +``` + +--- + +## Task 7: Обновить UI попапа — фильтр-табы и колонка Тип + +**Files:** +- Modify: `/home/mkr/picking_app/picking/page/picking_doc/picking_doc.js` + +Все изменения в методах `_showMsDialog()` и `_loadMsList()`. + +- [ ] **Step 1: Добавить таб-фильтр в HTML диалога** + +В `_showMsDialog()` найти HTML фильтров (строки с `ms-filter-from`, `ms-filter-to`) и добавить после них строку с табами: + +```html +
+ + + +
+``` + +- [ ] **Step 2: Добавить обработчик клика по табам** + +В `_showMsDialog()` добавить обработчик после других `dialog.$body.on(...)`: + +```javascript +dialog.$body.on('click', '.ms-type-btn', function() { + dialog.$body.find('.ms-type-btn').removeClass('active btn-primary').addClass('btn-default'); + $(this).removeClass('btn-default').addClass('btn-primary active'); + self._loadMsList(dialog); +}); +``` + +- [ ] **Step 3: Обновить кнопку синхронизации — вызывать `sync_all` вместо `sync_picking_list`** + +Найти: +```javascript +method: 'picking_app.picking.api.sync_picking_list' +``` +Заменить на: +```javascript +method: 'picking_app.picking.api.sync_all' +``` + +Найти строку с отображением результата: +```javascript +$status.text(`Готово: создано ${d.created || 0}, обновлено ${d.updated || 0} (всего ${d.total || 0})`); +``` +Заменить на: +```javascript +const m = d.moves || {}; +const o = d.orders || {}; +$status.text( + `Перемещения: +${m.created || 0} / ~${m.updated || 0} | Заказы: +${o.created || 0} / ~${o.updated || 0}` +); +``` + +- [ ] **Step 4: Передавать `source_type` при вызове `get_picking_list_items`** + +В методе `_loadMsList(dialog)` найти весь блок: +```javascript + async _loadMsList(dialog) { + const from_wh = dialog.$body.find('#ms-filter-from').val().trim(); + const to_wh = dialog.$body.find('#ms-filter-to').val().trim(); + const $cont = dialog.$body.find('#ms-list-container'); + $cont.html('
Загрузка...
'); + + try { + const r = await frappe.call({ + method: 'picking_app.picking.api.get_picking_list_items', + args: { from_warehouse: from_wh || null, to_warehouse: to_wh || null } + }); +``` + +Заменить на: +```javascript + async _loadMsList(dialog) { + const from_wh = dialog.$body.find('#ms-filter-from').val().trim(); + const to_wh = dialog.$body.find('#ms-filter-to').val().trim(); + const activeType = dialog.$body.find('.ms-type-btn.active').data('type') || null; + const $cont = dialog.$body.find('#ms-list-container'); + $cont.html('
Загрузка...
'); + + try { + const r = await frappe.call({ + method: 'picking_app.picking.api.get_picking_list_items', + args: { from_warehouse: from_wh || null, to_warehouse: to_wh || null, source_type: activeType } + }); +``` + +- [ ] **Step 5: Добавить колонку "Тип" в заголовок каждой строки Picking List** + +В методе `_loadMsList()` в функции рендеринга строки (`pls.map(pl => {...})`) найти строку с `from` и `to`: +```javascript +const from = pl.from_warehouse || '?'; +const to = pl.to_warehouse || '?'; +``` +Добавить после: +```javascript +const typeLabel = pl.source_type === 'Order' + ? 'Заказ' + : 'Перемещение'; +``` + +Добавить `${typeLabel}` в шаблон строки `.ms-pl-head` после `${statusBadge}`: +```javascript +${statusBadge} +${typeLabel} +``` + +- [ ] **Step 6: Пересобрать фронтенд и проверить** + +```bash +docker exec frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench build --app picking_app" 2>&1 | tail -5 +``` + +Открыть страницу Picking Document в браузере, нажать кнопку добавления из МС — проверить: +- Появились табы Все / Перемещения / Заказы +- В строках видна метка типа +- Синхронизация показывает статистику по двум типам + +- [ ] **Step 7: Commit** + +```bash +cd /home/mkr/picking_app +git add picking/page/picking_doc/picking_doc.js +git commit -m "feat: add source_type filter tabs and type badge to MoySklad popup" +``` + +--- + +## Task 8: Финальная проверка end-to-end + +- [ ] **Step 1: Убедиться что токен заполнен** + +Открыть в ERPNext: **Picking Settings → Default**, секция МойСклад, поле API-токен должно быть заполнено. + +- [ ] **Step 2: Запустить `sync_all` вручную** + +```bash +echo " +exec(''' +import picking_app.picking.api as api +result = api.sync_all() +print(result) +''', globals()) +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep -E "moves|orders|status" +``` + +Ожидаемый вывод вида: +``` +{'status': 'ok', 'moves': {'created': N, 'updated': N}, 'orders': {'created': N, 'updated': N}} +``` + +- [ ] **Step 3: Проверить записи в БД** + +```bash +echo " +exec(''' +import frappe +moves = frappe.db.count(\"Picking List\", {\"source_type\": \"Move\"}) +orders = frappe.db.count(\"Picking List\", {\"source_type\": \"Order\"}) +nulls = frappe.db.count(\"Picking List\", {\"source_type\": [\"\", None]}) +print(f\"Move: {moves}, Order: {orders}, NULL/legacy: {nulls}\") +''', globals()) +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep -E "Move:|Order:" +``` + +- [ ] **Step 4: Проверить `get_picking_list_items` с фильтром** + +```bash +echo " +exec(''' +import picking_app.picking.api as api +all_items = api.get_picking_list_items() +order_items = api.get_picking_list_items(source_type=\"Order\") +move_items = api.get_picking_list_items(source_type=\"Move\") +print(f\"All: {len(all_items)}, Orders: {len(order_items)}, Moves: {len(move_items)}\") +# Проверяем что source_type возвращается +if all_items: + print(\"source_type in response:\", \"source_type\" in all_items[0]) +''', globals()) +" | docker exec -i frappe-project-backend-1 bash -c "cd /home/frappe/frappe-bench && bench --site erp.local console" 2>&1 | grep -E "All:|source_type" +``` + +- [ ] **Step 5: Final commit tag** + +```bash +cd /home/mkr/picking_app +git log --oneline -7 +```