# batch_gxerp.py 批次统计计算规则

`apps/wpm/scripts/batch_gxerp.py` 用于按 **批次号 (batch)** 汇总该批次在各工序组下的生产/检验数据，写入 `BatchSt.data` (JSON)，同时维护 `first_time` / `last_time`。

---

## 一、入参与前置条件

| 参数 | 说明 |
| --- | --- |
| `batch` | 批次号 |
| `mgroup_obj` | 工序组对象（当前实现未直接使用，仅作为入口签名） |

- 必须存在 `BatchSt.objects.get(batch=batch, version=1)`，否则报错并退出。
- 遍历 `Mgroup.objects.all().order_by("sort")`，对每个工序组分别统计。
- 仅统计已提交且已出库的报工单：`mlog.submit_time__isnull=False` 且 `material_out__isnull=False`，并要求 `need_inout=True`。

---

## 二、字段命名约定

输出 JSON `data` 的 key 形式如下（`<G>` 代表工序组名）：

| Key 模式 | 含义 |
| --- | --- |
| `批次号` | 批次号 |
| `<G>_日期` | 该工序组所有报工日期，去重后 `;` 拼接 |
| `<G>_小日期` / `<G>_大日期` | 该工序组最早 / 最晚日期（`YYYY-MM-DD`） |
| `<G>_操作人` | 操作人姓名去重后 `;` 拼接 |
| `<G>_班次` | 班次名去重后 `;` 拼接 |
| `<G>_count_use` | 领用数（来自上游 `mlogb_from`） |
| `<G>_count_real` | 实际生产数 |
| `<G>_count_ok` | 合格数 |
| `<G>_count_notok` | 不合格数 |
| `<G>_count_ok_full` | 完全合格数 |
| `<G>_count_pn_jgqbl` | 加工前不良数（来自上游 `mlogb_from`） |
| `<G>_合格率` | 合格率（%） |
| `<G>_完全合格率` | 完全合格率（%） |
| `<G>_缺陷_<缺陷名>` | 该工序组的缺陷数量（按缺陷名汇总） |
| `<G>_缺陷_<缺陷名>_比例` | 缺陷数 / `count_real` × 100，单位 % |
| `<G>_加工前_缺陷_<缺陷名>` | 上游 `mlogb_from` 上的缺陷数量 |
| `<G>_加工前_缺陷_<缺陷名>_比例` | 加工前缺陷数 / `count_use` × 100，单位 % |

---

## 三、主流程：按工序组汇总（普通报工，`is_fix=False`）

数据源 `mlogb1_qs`：

```
Mlogb where mlog.submit_time IS NOT NULL
        and material_out IS NOT NULL
        and mlog.mgroup = <G>
        and mlog.is_fix = False
        and batch = <batch>
        and need_inout = True
```

### 1. 累加规则

对每条 `Mlogb`：

| 字段 | 累加来源 |
| --- | --- |
| `count_use` | `mlogb_from.count_use`（上游报工子项；若无 `mlogb_from` 则跳过） |
| `count_pn_jgqbl` | `mlogb_from.count_pn_jgqbl` |
| `count_real` | 当前 `item.count_real` |
| `count_ok` | 当前 `item.count_ok` |
| `count_ok_full` | 当前 `item.count_ok_full or 0` |
| `count_notok` | 当前 `item.count_notok or 0` |
| `操作人` | `mlog.handle_user`（最终去重，按 `name` 拼接） |
| `日期` | `mlog.handle_date`（最终去重排序） |
| `班次` | `mlog.shift.name` |

并记录所有上游 `mlogb_from.id` 到 `mlogb_q_ids`，用于"加工前缺陷"统计。

### 2. 合格率计算

```
完全合格率 = round(count_ok_full / count_real * 100, 2)
合格率     = round(count_ok      / count_real * 100, 2)
```

`decimal.InvalidOperation` 异常时（如分母为 0）记 `0`。

### 3. 缺陷统计

- 当前工序缺陷：`MlogbDefect.filter(mlogb__in=mlogb1_qs, count__gt=0).values('defect__name').annotate(total=Sum('count'))`
  - `<G>_缺陷_<name>` = total
  - `<G>_缺陷_<name>_比例` = `round(total / count_real * 100, 2)`
- 加工前（上游）缺陷：`MlogbDefect.filter(mlogb__id__in=mlogb_q_ids, count__gt=0)...`
  - `<G>_加工前_缺陷_<name>` = total
  - `<G>_加工前_缺陷_<name>_比例` = `round(total / count_use * 100, 2)`

### 4. 日期字段

```python
data[f"{G}_小日期"] = min(日期列表).strftime("%Y-%m-%d")  # 最早日期
data[f"{G}_大日期"] = max(日期列表).strftime("%Y-%m-%d")  # 最晚日期
```

最终 `<G>_日期` 被覆写为去重排序后的字符串（`;` 拼接）。

---

## 四、外观检验返修（`is_fix=True`）

数据源 `mlogb2_qs`：

```
Mlogb where mlog.submit_time IS NOT NULL
        and material_out IS NOT NULL
        and mlog.mgroup.name = '外观检验'
        and mlog.is_fix = True
        and batch = <batch>
        and need_inout = True
```

输出字段：

| Key | 计算 |
| --- | --- |
| `外观检验_返修_日期` | 报工日期，去重 `;` 拼接 |
| `外观检验_返修_操作人` | 操作人姓名，去重 `;` 拼接 |
| `外观检验_返修_count_real` | Σ `count_real` |
| `外观检验_返修_count_ok` | Σ `count_ok` |
| `外观检验_返修_count_ok_full` | Σ `count_ok_full or 0` |
| `外观检验_返修_缺陷_<name>` | `MlogbDefect` 按缺陷名汇总 |
| `外观检验_返修_缺陷_<name>_比例` | `round(total / 外观检验_返修_count_real * 100, 2)` |

---

## 五、外观检验车间库存抽检

数据源 `ft_qs`：

```
FtestWork where type2 = TYPE2_SOME
          and wm.mgroup.name = '外观检验'
          and batch = <batch>
          and submit_time IS NOT NULL
```

输出字段：

| Key | 计算 |
| --- | --- |
| `外观检验_车间库存抽检_日期` | `test_date` 去重 `;` 拼接 |
| `外观检验_车间库存抽检_操作人` | `test_user.name` 去重 `;` 拼接 |
| `外观检验_车间库存抽检_count_notok` | Σ `count_notok or 0` |
| `外观检验_车间库存抽检_缺陷_<name>` | `FtestworkDefect` 按缺陷名汇总（无比例字段） |

---

## 六、外观检验汇总指标（仅当存在 `外观检验_count_ok` 时计算）

| Key | 公式 |
| --- | --- |
| `外观检验_总合格数` | `外观检验_count_ok + 外观检验_返修_count_ok（默认0）` |
| `外观检验_总合格率` | `round(外观检验_总合格数 / 外观检验_count_real * 100, 2)` |
| `外观检验_完全总合格数` | `外观检验_count_ok_full + 外观检验_返修_count_ok_full（默认0）` |
| `外观检验_完全总合格率` | `round(外观检验_完全总合格数 / 外观检验_count_real * 100, 2)` |
| `外观检验_直通合格数` | `外观检验_总合格数 - 外观检验_车间库存抽检_count_notok（默认0）` |

### 直通合格率（依赖尺寸检验）

仅当 `尺寸检验_合格率` 存在时：

```
外观检验_直通合格率  = round(外观检验_总合格率 * 尺寸检验_合格率 / 100, 2)
外观检验_直通合格率2 = round(外观检验_直通合格数 / 尺寸检验_count_use * 100, 2)
```

仅当 `尺寸检验_完全合格率` 存在时：

```
外观检验_完全直通合格率 = round(外观检验_完全总合格率 * 尺寸检验_完全合格率 / 100, 2)
```

异常（`InvalidOperation` / `ZeroDivisionError`）回退为 `0`。

---

## 七、写回 BatchSt

1. `data` 经 `MyJSONEncoder` 序列化后回填 `batchst.data`。
2. 调 `get_f_l_date(data)`：扫描所有以 `_日期` 结尾的字段，按 `;` 拆开后逐个 `datetime.strptime("%Y-%m-%d")` 解析为 `date` 对象，再取最小/最大并转成上海时区的 `00:00:00` / `23:59:59`，得到 `first_time` / `last_time`。无法解析的片段会写日志并跳过。
3. 仅当现有 `first_time` 为空或新值更早时更新；`last_time` 反之。
4. 调用 `batchst.save()` 持久化。

---

## 八、关键依赖与字段含义速查

| 模型字段 | 中文释义 | 来源 |
| --- | --- | --- |
| `Mlogb.count_use` | 领用数 | 报工子项（来自 `mlogb_from`） |
| `Mlogb.count_real` | 实际生产数 | 报工子项 |
| `Mlogb.count_ok` | 合格数 | `count_real - count_notok` |
| `Mlogb.count_ok_full` | 完全合格数 | `count_real - count_notok_full` |
| `Mlogb.count_notok` | 不合格数 | 缺陷 `okcate=30` 求和 |
| `Mlogb.count_pn_jgqbl` | 加工前不良 | `MlogbDefect` 中加工前不良求和 |
| `Mlogb.need_inout` | 是否需出入库 | 仅 `True` 参与统计 |
| `Mlogb.mlogb_from` | 上游报工子项 | 用于追溯领用与加工前缺陷 |

---

## 九、已知潜在问题

1. **`mgroup_obj` 入参未使用**：当前实现忽略该参数，对所有 `Mgroup` 全量遍历。

## 十、修订记录

- 修复 `小日期 / 大日期` 命名与含义反向，`小日期` 取最早日期、`大日期` 取最晚日期；同步修正 `batch_bxerp.py`、`batch_gzerp.py` 中所有同类字段。
- `get_f_l_date` 改为先 `datetime.strptime` 解析为 `date` 对象再比较，移除对字符串字典序的隐式依赖；非法日期片段记录日志后跳过。
- 所有合格率/直通率计算的兜底 `except` 同时捕获 `decimal.InvalidOperation` 与 `ZeroDivisionError`，以兼容 `Decimal` 与原生数值的除零场景；`batch_bxerp.py` 与 `batch_gzerp.py` 中仅捕获 `decimal.InvalidOperation` 的合格率分支也同步扩展。
