mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-18 06:05:09 +00:00
docs: add MoySklad orders sync implementation plan
This commit is contained in:
parent
cc3e5f6e6a
commit
21a39e88e8
1 changed files with 754 additions and 0 deletions
754
docs/superpowers/plans/2026-03-20-moysklad-orders-sync.md
Normal file
754
docs/superpowers/plans/2026-03-20-moysklad-orders-sync.md
Normal file
|
|
@ -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
|
||||
<div id="ms-type-filter" style="display:flex;gap:6px;margin-bottom:8px;">
|
||||
<button class="btn btn-xs btn-default ms-type-btn active" data-type="">Все</button>
|
||||
<button class="btn btn-xs btn-default ms-type-btn" data-type="Move">Перемещения</button>
|
||||
<button class="btn btn-xs btn-default ms-type-btn" data-type="Order">Заказы</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **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('<div style="text-align:center;color:#888;padding:20px">Загрузка...</div>');
|
||||
|
||||
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('<div style="text-align:center;color:#888;padding:20px">Загрузка...</div>');
|
||||
|
||||
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'
|
||||
? '<span class="ms-badge" style="background:#cfe2ff;color:#084298">Заказ</span>'
|
||||
: '<span class="ms-badge" style="background:#e9ecef;color:#666">Перемещение</span>';
|
||||
```
|
||||
|
||||
Добавить `${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
|
||||
```
|
||||
Loading…
Reference in a new issue