Add project_id to read API responses and remove duplicate id fields.
Expose project_id across labor and calendar endpoints for unambiguous project matching. Use only project_id in /api/projects and /api/project-report; update docs, UI tables, and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5c21d25d45
commit
9fa9d5e3ed
@ -196,6 +196,10 @@ uvicorn app.main:app --host 0.0.0.0 --port 8090
|
|||||||
| `2` | Завершён |
|
| `2` | Завершён |
|
||||||
| `3` | Пауза |
|
| `3` | Пауза |
|
||||||
|
|
||||||
|
### Id проекта (`project_id`)
|
||||||
|
|
||||||
|
Во всех read-ответах с данными проекта обязательно есть **`project_id`**. Поле `id` в **`/api/work-report`** — это id **сотрудника**; id проекта — только **`project_id`**. В **`/api/projects`** и **`/api/project-report`** — только **`project_id`**.
|
||||||
|
|
||||||
> **Не путать:** поле `step` в **`tMerakomisDSection`** — фильтр разделов по стадии, это другая сущность.
|
> **Не путать:** поле `step` в **`tMerakomisDSection`** — фильтр разделов по стадии, это другая сущность.
|
||||||
|
|
||||||
### Переработки (`is_over`)
|
### Переработки (`is_over`)
|
||||||
@ -352,7 +356,7 @@ loop:
|
|||||||
| `include_removed` | `false` | Включать `removed = 1` |
|
| `include_removed` | `false` | Включать `removed = 1` |
|
||||||
| `include_archive` | `true` | Не фильтровать архивные |
|
| `include_archive` | `true` | Не фильтровать архивные |
|
||||||
|
|
||||||
**Поля `items`:** `id`, `code`, `name`, `director`, `step` (id стадии), `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`.
|
**Поля `items`:** **`project_id`**, `code`, `name`, `director`, `step` (id стадии), `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`.
|
||||||
|
|
||||||
Для человекочитаемой стадии и статуса используйте join через `/api/project-members` или агрегаты ниже (`step_name`, `status_name`).
|
Для человекочитаемой стадии и статуса используйте join через `/api/project-members` или агрегаты ниже (`step_name`, `status_name`).
|
||||||
|
|
||||||
@ -450,7 +454,7 @@ Upsert участника команды: если `(team, emp)` уже есть
|
|||||||
| `is_over` | `0` — рабочие, `1` — переработка |
|
| `is_over` | `0` — рабочие, `1` — переработка |
|
||||||
| `limit` | 1…2000 (по умолчанию 500) |
|
| `limit` | 1…2000 (по умолчанию 500) |
|
||||||
|
|
||||||
**Поля:** `id`, `emp`, `project`, `date`, `duration`, `is_over`.
|
**Поля:** `id`, `emp`, **`project_id`**, `project`, `date`, `duration`, `is_over`.
|
||||||
|
|
||||||
### `GET /api/work-report` — защищённый ⭐ основной контракт
|
### `GET /api/work-report` — защищённый ⭐ основной контракт
|
||||||
|
|
||||||
@ -474,6 +478,7 @@ Upsert участника команды: если `(team, emp)` уже есть
|
|||||||
| `department` | string \| null | Орготдел (коды через запятую) |
|
| `department` | string \| null | Орготдел (коды через запятую) |
|
||||||
| `staffing_title` | string \| null | Должность (Архитектор, ГИП…) |
|
| `staffing_title` | string \| null | Должность (Архитектор, ГИП…) |
|
||||||
| `section` | string \| null | Раздел в проекте |
|
| `section` | string \| null | Раздел в проекте |
|
||||||
|
| **`project_id`** | int | Id проекта |
|
||||||
| `project_code` | string | Код проекта |
|
| `project_code` | string | Код проекта |
|
||||||
| **`step_name`** | string | Стадия (ПД, РД…) |
|
| **`step_name`** | string | Стадия (ПД, РД…) |
|
||||||
| **`status`** | int \| null | Код статуса проекта |
|
| **`status`** | int \| null | Код статуса проекта |
|
||||||
@ -492,6 +497,7 @@ Upsert участника команды: если `(team, emp)` уже есть
|
|||||||
"department": "ОВ",
|
"department": "ОВ",
|
||||||
"staffing_title": "Инженер",
|
"staffing_title": "Инженер",
|
||||||
"section": "ОВ1",
|
"section": "ОВ1",
|
||||||
|
"project_id": 86,
|
||||||
"project_code": "2025-0016",
|
"project_code": "2025-0016",
|
||||||
"step_name": "РД",
|
"step_name": "РД",
|
||||||
"status": 1,
|
"status": 1,
|
||||||
@ -520,7 +526,7 @@ Upsert участника команды: если `(team, emp)` уже есть
|
|||||||
| `project_id` | Фильтр по проекту |
|
| `project_id` | Фильтр по проекту |
|
||||||
| `limit`, `offset` | Пагинация (1…2000) |
|
| `limit`, `offset` | Пагинация (1…2000) |
|
||||||
|
|
||||||
**Поля `items`:** `id` (project_id), `project_code`, **`step_name`**, **`status`**, **`status_name`**, **`archive`**, **`archive_date`**, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`.
|
**Поля `items`:** **`project_id`**, `project_code`, **`step_name`**, **`status`**, **`status_name`**, **`archive`**, **`archive_date`**, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`.
|
||||||
|
|
||||||
Одна строка = уникальная тройка **проект + department + section**.
|
Одна строка = уникальная тройка **проект + department + section**.
|
||||||
|
|
||||||
@ -648,7 +654,7 @@ Upsert участника команды: если `(team, emp)` уже есть
|
|||||||
|
|
||||||
### C. Список проектов сотрудника для UI
|
### C. Список проектов сотрудника для UI
|
||||||
|
|
||||||
`GET /api/project-members?emp_id={id}&active_only=true` — в ответе `project_code`, `step_name`, `status_name`, `archive`, `project_name`.
|
`GET /api/project-members?emp_id={id}&active_only=true` — в ответе **`project_id`**, `project_code`, `step_name`, `status_name`, `archive`, `project_name`.
|
||||||
|
|
||||||
### D. Запись часов из мобильного/бота
|
### D. Запись часов из мобильного/бота
|
||||||
|
|
||||||
|
|||||||
@ -207,6 +207,20 @@ loop:
|
|||||||
| `archive` | `/api/projects`, эндпоинты с `project_name` | `0` — активный, `1` — в архиве |
|
| `archive` | `/api/projects`, эндпоинты с `project_name` | `0` — активный, `1` — в архиве |
|
||||||
| `archive_date` | `/api/projects`, эндпоинты с `project_name` | Дата архивации (`YYYY-MM-DD`) или `null` |
|
| `archive_date` | `/api/projects`, эндпоинты с `project_name` | Дата архивации (`YYYY-MM-DD`) или `null` |
|
||||||
|
|
||||||
|
### Id проекта (`project_id`)
|
||||||
|
|
||||||
|
Во всех ответах, где есть **`project_code`**, **`project_name`** или поле **`project`** (сырой табель), дополнительно отдаётся **`project_id`** — числовой id из `tMerakomisProject`. Используйте его для однозначной идентификации проекта (код может повторяться в истории).
|
||||||
|
|
||||||
|
| Эндпоинт | Поле с id проекта |
|
||||||
|
|----------|-------------------|
|
||||||
|
| `GET /api/projects` | **`project_id`** |
|
||||||
|
| `GET /api/project-members`, `GET /api/labor-summary` | **`project_id`** |
|
||||||
|
| `GET /api/work-report` | **`project_id`** (`id` — это `emp_id`) |
|
||||||
|
| `GET /api/project-report` | **`project_id`** |
|
||||||
|
| `GET /api/time-entries` | **`project_id`** и `project` (оба — id проекта) |
|
||||||
|
| `GET /api/time-calendar` | **`project_id`** в корне ответа (если фильтр по проекту) |
|
||||||
|
| `GET /api/time-summary` | **`project_id`** в элементах массива `project[]` |
|
||||||
|
|
||||||
Коды **`status`** (порт `eStatus.php`):
|
Коды **`status`** (порт `eStatus.php`):
|
||||||
|
|
||||||
| `status` | `status_name` |
|
| `status` | `status_name` |
|
||||||
@ -370,7 +384,7 @@ since = сохранённый_у_себя_маркер # 0 при первом
|
|||||||
| `include_removed` | `false` | Включать удалённые |
|
| `include_removed` | `false` | Включать удалённые |
|
||||||
| `include_archive` | `true` | Не скрывать архивные |
|
| `include_archive` | `true` | Не скрывать архивные |
|
||||||
|
|
||||||
**Поля `items`:** `id`, `code`, `name`, `director`, `step`, `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`.
|
**Поля `items`:** **`project_id`**, `code`, `name`, `director`, `step`, `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -455,7 +469,7 @@ since = сохранённый_у_себя_маркер # 0 при первом
|
|||||||
| `limit` | `500` | 1…2000 |
|
| `limit` | `500` | 1…2000 |
|
||||||
| `offset` | `0` | |
|
| `offset` | `0` | |
|
||||||
|
|
||||||
**Поля `items`:** `id`, `emp`, `project`, `date`, `duration`, `is_over`.
|
**Поля `items`:** `id`, `emp`, **`project_id`**, `project`, `date`, `duration`, `is_over`.
|
||||||
|
|
||||||
Ошибка `400`, если `date_from > date_to`.
|
Ошибка `400`, если `date_from > date_to`.
|
||||||
|
|
||||||
@ -483,6 +497,7 @@ since = сохранённый_у_себя_маркер # 0 при первом
|
|||||||
| `department` | string \| null | Орготдел |
|
| `department` | string \| null | Орготдел |
|
||||||
| `staffing_title` | string \| null | Должность (Архитектор, ГИП…) |
|
| `staffing_title` | string \| null | Должность (Архитектор, ГИП…) |
|
||||||
| `section` | string \| null | Раздел в проекте |
|
| `section` | string \| null | Раздел в проекте |
|
||||||
|
| **`project_id`** | int | Id проекта Merakomis |
|
||||||
| `project_code` | string | |
|
| `project_code` | string | |
|
||||||
| `step_name` | string | Стадия |
|
| `step_name` | string | Стадия |
|
||||||
| `status` | int \| null | Код статуса проекта |
|
| `status` | int \| null | Код статуса проекта |
|
||||||
@ -510,7 +525,7 @@ since = сохранённый_у_себя_маркер # 0 при первом
|
|||||||
| `project_id` | | Фильтр |
|
| `project_id` | | Фильтр |
|
||||||
| `limit`, `offset` | | 1…2000 |
|
| `limit`, `offset` | | 1…2000 |
|
||||||
|
|
||||||
**Поля `items`:** `id` (project_id), `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`.
|
**Поля `items`:** **`project_id`**, `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,11 @@ from app.main import (
|
|||||||
)
|
)
|
||||||
from app.emp_departments import load_emp_departments
|
from app.emp_departments import load_emp_departments
|
||||||
from app.emp_staffing import load_emp_staffing_titles
|
from app.emp_staffing import load_emp_staffing_titles
|
||||||
|
from app.project_fields import (
|
||||||
|
ensure_project_id,
|
||||||
|
ensure_project_ids,
|
||||||
|
project_catalog_items,
|
||||||
|
)
|
||||||
from app.project_status import (
|
from app.project_status import (
|
||||||
enrich_project_status_fields,
|
enrich_project_status_fields,
|
||||||
project_status_name,
|
project_status_name,
|
||||||
@ -424,9 +429,9 @@ def projects(
|
|||||||
cur.execute(f"SELECT COUNT(*) AS n FROM {tq}{where_sql}")
|
cur.execute(f"SELECT COUNT(*) AS n FROM {tq}{where_sql}")
|
||||||
total = cur.fetchone()["n"]
|
total = cur.fetchone()["n"]
|
||||||
|
|
||||||
items = [
|
items = project_catalog_items([
|
||||||
enrich_project_status_fields(row) for row in json_rows(rows)
|
enrich_project_status_fields(row) for row in json_rows(rows)
|
||||||
]
|
])
|
||||||
return {
|
return {
|
||||||
"physical_table": table,
|
"physical_table": table,
|
||||||
"total": int(total),
|
"total": int(total),
|
||||||
@ -642,9 +647,9 @@ def project_members(
|
|||||||
"offset": resp_offset,
|
"offset": resp_offset,
|
||||||
"fetch_all": fetch_all,
|
"fetch_all": fetch_all,
|
||||||
"count": len(rows),
|
"count": len(rows),
|
||||||
"items": [
|
"items": ensure_project_ids([
|
||||||
enrich_project_status_fields(row) for row in json_rows(rows)
|
enrich_project_status_fields(row) for row in json_rows(rows)
|
||||||
],
|
]),
|
||||||
}
|
}
|
||||||
except pymysql.Error as e:
|
except pymysql.Error as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
@ -754,7 +759,7 @@ def time_entries(
|
|||||||
"date_from": d_from.isoformat() if d_from else None,
|
"date_from": d_from.isoformat() if d_from else None,
|
||||||
"date_to": d_to.isoformat() if d_to else None,
|
"date_to": d_to.isoformat() if d_to else None,
|
||||||
"skipped_unknown_in_db": skipped,
|
"skipped_unknown_in_db": skipped,
|
||||||
"items": json_rows(rows),
|
"items": ensure_project_ids(json_rows(rows)),
|
||||||
}
|
}
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@ -1024,6 +1029,7 @@ def work_report(
|
|||||||
"department": it.get("department"),
|
"department": it.get("department"),
|
||||||
"staffing_title": it.get("staffing_title"),
|
"staffing_title": it.get("staffing_title"),
|
||||||
"section": it.get("section_name"),
|
"section": it.get("section_name"),
|
||||||
|
"project_id": it["project_id"],
|
||||||
"project_code": it.get("project_code"),
|
"project_code": it.get("project_code"),
|
||||||
"project_name": it.get("project_name"),
|
"project_name": it.get("project_name"),
|
||||||
"step_name": it.get("step_name"),
|
"step_name": it.get("step_name"),
|
||||||
@ -1084,7 +1090,7 @@ def project_report(
|
|||||||
|
|
||||||
report = [
|
report = [
|
||||||
{
|
{
|
||||||
"id": it["project_id"],
|
"project_id": it["project_id"],
|
||||||
"project_code": it.get("project_code"),
|
"project_code": it.get("project_code"),
|
||||||
"project_name": it.get("project_name"),
|
"project_name": it.get("project_name"),
|
||||||
"step_name": it.get("step_name"),
|
"step_name": it.get("step_name"),
|
||||||
|
|||||||
@ -234,7 +234,8 @@ def time_summary(
|
|||||||
if all([t_emp, t_date, t_dur, t_over, t_proj, p_id]):
|
if all([t_emp, t_date, t_dur, t_over, t_proj, p_id]):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT p.{_quote_ident(p_code)} AS code,
|
SELECT t.{_quote_ident(t_proj)} AS project_id,
|
||||||
|
p.{_quote_ident(p_code)} AS code,
|
||||||
p.{_quote_ident(p_name)} AS name,
|
p.{_quote_ident(p_name)} AS name,
|
||||||
SUM(t.{_quote_ident(t_dur)}) AS dur,
|
SUM(t.{_quote_ident(t_dur)}) AS dur,
|
||||||
t.{_quote_ident(t_over)} AS is_over
|
t.{_quote_ident(t_over)} AS is_over
|
||||||
@ -246,21 +247,25 @@ def time_summary(
|
|||||||
""",
|
""",
|
||||||
(target, begin.isoformat(), end.isoformat()),
|
(target, begin.isoformat(), end.isoformat()),
|
||||||
)
|
)
|
||||||
proj_acc: dict[str, float] = {}
|
proj_acc: dict[int, dict[str, Any]] = {}
|
||||||
for row in cur.fetchall():
|
for row in cur.fetchall():
|
||||||
title = row.get("code") or row.get("name") or "?"
|
pid = int(row["project_id"] or 0)
|
||||||
|
title = row.get("code") or row.get("name") or str(pid or "?")
|
||||||
dur = float(row["dur"] or 0)
|
dur = float(row["dur"] or 0)
|
||||||
if int(row["is_over"] or 0):
|
if int(row["is_over"] or 0):
|
||||||
over += dur
|
over += dur
|
||||||
else:
|
else:
|
||||||
hours += dur
|
hours += dur
|
||||||
proj_acc[title] = proj_acc.get(title, 0) + dur
|
if pid not in proj_acc:
|
||||||
ci = 1
|
ci = len(proj_acc) + 1
|
||||||
for title, val in proj_acc.items():
|
proj_acc[pid] = {
|
||||||
block["project"].append(
|
"project_id": pid,
|
||||||
{"title": title, "value": val, "class": f"chart-color{ci}"}
|
"title": title,
|
||||||
)
|
"value": 0.0,
|
||||||
ci += 1
|
"class": f"chart-color{ci}",
|
||||||
|
}
|
||||||
|
proj_acc[pid]["value"] += dur
|
||||||
|
block["project"] = list(proj_acc.values())
|
||||||
block["total"] = [
|
block["total"] = [
|
||||||
{"title": "Раб.", "value": hours, "class": "color-green"},
|
{"title": "Раб.", "value": hours, "class": "color-green"},
|
||||||
{"title": "Сверх.", "value": over, "class": "color-brown"},
|
{"title": "Сверх.", "value": over, "class": "color-brown"},
|
||||||
@ -439,6 +444,7 @@ def time_calendar(
|
|||||||
"day_begin": day_begin.isoformat(),
|
"day_begin": day_begin.isoformat(),
|
||||||
"day_end": today.isoformat(),
|
"day_end": today.isoformat(),
|
||||||
"name": _emp_name(cur, db, target),
|
"name": _emp_name(cur, db, target),
|
||||||
|
"project_id": project_id or None,
|
||||||
"project": project_label,
|
"project": project_label,
|
||||||
"text": "",
|
"text": "",
|
||||||
"archive": archive,
|
"archive": archive,
|
||||||
|
|||||||
50
services/user-reader/app/project_fields.py
Normal file
50
services/user-reader/app/project_fields.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""Единообразные поля проекта в JSON API."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_project_id(item: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Добавляет project_id, если в записи уже есть данные проекта."""
|
||||||
|
if item.get("project_id") is not None:
|
||||||
|
return item
|
||||||
|
proj = item.get("project")
|
||||||
|
if proj is not None:
|
||||||
|
try:
|
||||||
|
item["project_id"] = int(proj)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return item
|
||||||
|
# id в work-report — emp_id; project_id задаётся явно в эндпоинте
|
||||||
|
if "employee" in item:
|
||||||
|
return item
|
||||||
|
pid = item.get("id")
|
||||||
|
if pid is None:
|
||||||
|
return item
|
||||||
|
if any(
|
||||||
|
k in item
|
||||||
|
for k in ("project_code", "project_name", "code", "director", "step", "team")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
item["project_id"] = int(pid)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_project_ids(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
return [ensure_project_id(dict(it)) for it in items]
|
||||||
|
|
||||||
|
|
||||||
|
def project_catalog_item(item: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Элемент GET /api/projects: только project_id, без id."""
|
||||||
|
row = dict(item)
|
||||||
|
pid = row.pop("id", None)
|
||||||
|
if pid is not None and row.get("project_id") is None:
|
||||||
|
row["project_id"] = int(pid)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def project_catalog_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
return [project_catalog_item(it) for it in items]
|
||||||
@ -7,6 +7,7 @@ from typing import Any
|
|||||||
from app.emp_schema import EMP_TABLE_CANONICAL
|
from app.emp_schema import EMP_TABLE_CANONICAL
|
||||||
from app.labor import _column_lookup, _prefixed_col, _resolve_table
|
from app.labor import _column_lookup, _prefixed_col, _resolve_table
|
||||||
from app.main import _quote_ident, _table_columns, resolve_emp_table
|
from app.main import _quote_ident, _table_columns, resolve_emp_table
|
||||||
|
from app.project_fields import ensure_project_id
|
||||||
from app.project_status import enrich_project_status_fields
|
from app.project_status import enrich_project_status_fields
|
||||||
from app.merakomis_schema import (
|
from app.merakomis_schema import (
|
||||||
PROJECT_SECTION_TABLE,
|
PROJECT_SECTION_TABLE,
|
||||||
@ -194,7 +195,7 @@ def fetch_member_item(
|
|||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return enrich_project_status_fields({k: row[k] for k in row})
|
return ensure_project_id(enrich_project_status_fields({k: row[k] for k in row}))
|
||||||
|
|
||||||
|
|
||||||
def emp_is_active_for_team(cur, db: str, emp_id: int) -> bool:
|
def emp_is_active_for_team(cur, db: str, emp_id: int) -> bool:
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
const STORE_KEY = 'meraproject_user_reader_api_key';
|
const STORE_KEY = 'meraproject_user_reader_api_key';
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
{ key: 'id', label: 'id' },
|
{ key: 'project_id', label: 'project_id', num: true },
|
||||||
{ key: 'project_code', label: 'project_code' },
|
{ key: 'project_code', label: 'project_code' },
|
||||||
{ key: 'step_name', label: 'Стадия' },
|
{ key: 'step_name', label: 'Стадия' },
|
||||||
{ key: 'status_name', label: 'Статус' },
|
{ key: 'status_name', label: 'Статус' },
|
||||||
|
|||||||
@ -59,6 +59,7 @@
|
|||||||
{ key: 'staffing_title', label: 'Должность' },
|
{ key: 'staffing_title', label: 'Должность' },
|
||||||
{ key: 'department', label: 'department' },
|
{ key: 'department', label: 'department' },
|
||||||
{ key: 'section', label: 'section' },
|
{ key: 'section', label: 'section' },
|
||||||
|
{ key: 'project_id', label: 'project_id', num: true },
|
||||||
{ key: 'project_code', label: 'project_code' },
|
{ key: 'project_code', label: 'project_code' },
|
||||||
{ key: 'step_name', label: 'Стадия' },
|
{ key: 'step_name', label: 'Стадия' },
|
||||||
{ key: 'status_name', label: 'Статус' },
|
{ key: 'status_name', label: 'Статус' },
|
||||||
|
|||||||
46
services/user-reader/tests/test_project_fields.py
Normal file
46
services/user-reader/tests/test_project_fields.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from app.project_fields import (
|
||||||
|
ensure_project_id,
|
||||||
|
ensure_project_ids,
|
||||||
|
project_catalog_item,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_id_from_project_column():
|
||||||
|
row = ensure_project_id({"project": 86, "date": "2026-01-01"})
|
||||||
|
assert row["project_id"] == 86
|
||||||
|
assert row["project"] == 86
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_id_from_catalog_id():
|
||||||
|
row = ensure_project_id({"id": 96, "code": "0000-0000", "name": "Test"})
|
||||||
|
assert row["project_id"] == 96
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_id_from_project_report_id():
|
||||||
|
row = ensure_project_id({"id": 86, "project_code": "2025-0016"})
|
||||||
|
assert row["project_id"] == 86
|
||||||
|
|
||||||
|
|
||||||
|
def test_work_report_does_not_confuse_emp_id():
|
||||||
|
row = ensure_project_id({"id": 46, "employee": "Иванов", "project_code": "X"})
|
||||||
|
assert "project_id" not in row
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_project_ids_list():
|
||||||
|
rows = ensure_project_ids([{"project": 1}, {"id": 2, "code": "A"}])
|
||||||
|
assert rows[0]["project_id"] == 1
|
||||||
|
assert rows[1]["project_id"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_catalog_item_uses_only_project_id():
|
||||||
|
row = project_catalog_item(
|
||||||
|
{"id": 96, "code": "0000-0000", "name": "Test", "project_id": 96}
|
||||||
|
)
|
||||||
|
assert row["project_id"] == 96
|
||||||
|
assert "id" not in row
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_catalog_item_from_id():
|
||||||
|
row = project_catalog_item({"id": 86, "code": "2025-0016"})
|
||||||
|
assert row["project_id"] == 86
|
||||||
|
assert "id" not in row
|
||||||
Loading…
Reference in New Issue
Block a user