mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-21 15:25:09 +00:00
754 lines
28 KiB
Markdown
754 lines
28 KiB
Markdown
# МойСклад: синхронизация заказов покупателей — 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
|
||
```
|