ht 6 dagar sedan
förälder
incheckning
290d0606e8

+ 103 - 1
README.md

@@ -301,6 +301,8 @@ Content-Type: application/json
 | `axmt500` | `ApproveOrderFormPushHandler` | 客户订单审批 | `FORM-8454FA17732746058A2D90B1C559CC19G5SR` | `TPROC--9VD66QA18AJ8317KK3NXM54W1HIT30C7N28TM7` |
 | `axmt140` | `CustomerReleaseFormPushHandler` | 客户放行审批 | `FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C` | `TPROC--6TG66381N8G8Y7L9GBEEY7QVXTUY2LBBGZ7TMU` |
 | `cint301` | `MiscReceiptIssueFormPushHandler` | 杂收杂发审批 | `FORM-4B08ADABCDAA45788C043729A855E03ED76G` | `TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0` |
+| `asft800` | `Asft800FormPushHandler` | 包装工单变更审批流程 | `FORM-40D6FBBBF4A94864A2AF9535AAC7924CRBH2` | `TPROC--U2I66F719HF854X6PIGK5A2AP9VC2LODB7YSM1` |
+| `ampt100` | `Ampt100FormPushHandler` | 供应商导入流程 | `FORM-9EE14CA14D8D4AEE8102944E669E6B0CFUKS` | `TPROC--75G66PD15YG82766OFSWD8BS4ZJ33YPRFOZSM2` |
 
 ### 5.2 `mainData` 规则
 
@@ -334,6 +336,8 @@ Content-Type: application/json
 | `axmt500` | `detailData1`、`detailData2`,兼容历史 `detailData` |
 | `axmt140` | 优先 `detailData1`,缺失时回退到 `detailData` |
 | `cint301` | `detailData1` |
+| `asft800` | `detailData1` |
+| `ampt100` | 无明细,传入空对象 `{}` |
 
 明细对象中可以直接传入宜搭 `fieldId`。服务端会保留包含 `Field_` 的字段值,用于补充默认字段映射。
 
@@ -345,6 +349,104 @@ Content-Type: application/json
 4. 匹配到 `YidaFormPushHandler` 时,提交人工号允许为空,具体处理器可以使用默认发起人兜底。
 5. 未匹配到表单处理器并走通用 `YidaProcessService` 时,提交人工号不能为空。
 
+### 5.5 `asft800` 包装工单变更审批流程
+
+调用 `POST /api/erp/yida/push` 时传入 `type: "asft800"`。表头使用 ERP 原始字段,明细必须放在 `detailData.detailData1`。
+
+```json
+{
+  "type": "asft800",
+  "bm": "123456789",
+  "gs": "87",
+  "tjr": "EMP0001",
+  "mainData": {
+    "rq": "2026-09-16",
+    "bm": "123456789",
+    "gs": "87",
+    "tjr": "EMP0001",
+    "t100id": "EMP0001",
+    "ent": "87",
+    "t100bm": "PACK",
+    "sfkaent": "87",
+    "sfkasite": "ML",
+    "sfkadocno": "WO-20260916-001",
+    "sfka900": "2",
+    "sfka902": "2026/09/16",
+    "sfka057": "包装工单",
+    "sfka010": "ITEM-001",
+    "imaal003": "锂电池包装组件",
+    "imaal004": "标准规格",
+    "sfka012": 100.5,
+    "sfka006": "SOURCE-001",
+    "sfkaud009": "MO-001",
+    "sfka906": "变更物料及数量",
+    "ooagud004": "dingtalk-user-id"
+  },
+  "detailData": {
+    "detailData1": [
+      {
+        "sfkgent": "87",
+        "sfkgdocno": "WO-20260916-001",
+        "sfkg900": "2",
+        "sfkgseq": "10",
+        "sfkg901": "变更",
+        "sfkg006": "MAT-002",
+        "imaal003": "新物料名称",
+        "imaal004": "新物料规格",
+        "sfkg013": 101.5,
+        "sfkg014": "PCS",
+        "sfba006": "MAT-001",
+        "o_imaal003": "原物料名称",
+        "o_imaal004": "原物料规格",
+        "sfba013": 100.0
+      }
+    ]
+  }
+}
+```
+
+`tjr` 是员工工号,服务端会从员工花名册解析对应钉钉用户作为发起人;未传时,处理器使用 `mainData.ooagud004` 作为钉钉用户 ID。`bm` 为钉钉部门 ID,`sfka902` 支持 `yyyy-MM-dd` 或 `yyyy/MM/dd` 格式。
+
+### 5.6 `ampt100` 供应商导入流程
+
+调用 `POST /api/erp/yida/push` 时传入 `type: "ampt100"`。该流程无明细,`detailData` 必须为 `{}`;客户文档中的 `apmt100` 为旧标识,不作为路由值。
+
+```json
+{
+  "type": "ampt100",
+  "bm": "123456789",
+  "gs": "ML",
+  "tjr": "EMP0001",
+  "mainData": {
+    "rq": "2026-09-16",
+    "sqlx": "NEW",
+    "gysmc": "供应商名称",
+    "gysbm": "SUP-001",
+    "sh": "91330100TEST",
+    "gczczj": "1000000.50",
+    "clsj": "2026/09/16",
+    "zysccp": "锂电池包装组件",
+    "gcgm": "120",
+    "lcbh": "PM-001",
+    "smzq": "ACTIVE",
+    "pmbb033all": "CNY",
+    "pmbb034all": "VAT",
+    "pmbb053all": "NET30",
+    "pmbb033": "CNY",
+    "pmbb034": "VAT",
+    "pmbb053": "NET30",
+    "pmbb037": "NET30",
+    "gst100": "ML",
+    "lxr": "张三",
+    "gsdz": "浙江省杭州市",
+    "lxfs": "13800000000"
+  },
+  "detailData": {}
+}
+```
+
+`lcbh` 用于生成流程标题;若其为空,可使用 `docno`。`clsj` 支持 `yyyy-MM-dd` 或 `yyyy/MM/dd`。所有 `pmbb***all` 字段映射到集团惯用数据,未带 `all` 后缀的 `pmbb***` 映射到据点惯用数据。
+
 ## 6. 日志和异常
 
 ### 6.1 日志
@@ -421,7 +523,7 @@ mvn clean test
 当前验证结果:
 
 ```text
-Tests run: 30, Failures: 0, Errors: 0, Skipped: 1
+Tests run: 36, Failures: 0, Errors: 0, Skipped: 1
 ```
 
 ## 9. 扩展规范

+ 7 - 2
src/main/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImpl.java

@@ -75,7 +75,7 @@ public class ErpYidaPushServiceImpl implements ErpYidaPushService {
      */
     @Override
     public McR push(ErpYidaPushRequest request) {
-        validateDetailDataShape(request.getDetailData());
+        validateDetailDataShape(request);
         applyTopLevelCommonFields(request);
         YidaFormPushHandler formPushHandler = formPushHandlers.get(normalizeType(request.getType()));
         String originatorUserId = resolveOriginatorUserId(request, formPushHandler != null);
@@ -103,10 +103,15 @@ public class ErpYidaPushServiceImpl implements ErpYidaPushService {
         return response;
     }
 
-    private void validateDetailDataShape(Object detailData) {
+    private void validateDetailDataShape(ErpYidaPushRequest request) {
+        Object detailData = request.getDetailData();
         if (!(detailData instanceof Map)) {
             throw new IllegalArgumentException("ERP Yida push detailData must be an object containing detailData array");
         }
+        if (normalizeType(Ampt100FormPushHandler.TYPE).equals(normalizeType(request.getType()))
+                && ((Map<?, ?>) detailData).isEmpty()) {
+            return;
+        }
         if (!hasDetailDataRows((Map<?, ?>) detailData)) {
             throw new IllegalArgumentException("ERP Yida push detailData.detailData must be an array");
         }

+ 32 - 0
src/test/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImplTest.java

@@ -197,6 +197,38 @@ class ErpYidaPushServiceImplTest {
         verify(yidaProcessService, never()).startProcess(any(YidaProcessStartRequest.class));
     }
 
+    @Test
+    void acceptsEmptyDetailDataForSupplierImportFormPushHandler() {
+        Logger logger = mock(Logger.class);
+        YidaProcessService yidaProcessService = mock(YidaProcessService.class);
+        EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
+        YidaFormPushHandler formPushHandler = mock(YidaFormPushHandler.class);
+        when(formPushHandler.getTypes()).thenReturn(Collections.singletonList(Ampt100FormPushHandler.TYPE));
+        when(employeeRosterService.findByEmployeeNo("user-001")).thenReturn(employee("dingtalk-user-001"));
+
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(Ampt100FormPushHandler.TYPE);
+        ErpYidaPushMainData mainData = new ErpYidaPushMainData();
+        mainData.setTjr("user-001");
+        request.setMainData(mainData);
+        request.setDetailData(Collections.emptyMap());
+        when(formPushHandler.push(request)).thenReturn("form-instance-1");
+
+        ErpYidaPushServiceImpl service = new ErpYidaPushServiceImpl(
+                logger,
+                yidaProcessService,
+                Collections.singletonList(formPushHandler),
+                employeeRosterService);
+
+        McR result = service.push(request);
+
+        assertNotNull(result);
+        assertEquals(Collections.emptyMap(), request.getDetailData());
+        assertEquals("dingtalk-user-001", request.getOriginatorUserId());
+        verify(formPushHandler).push(request);
+        verify(yidaProcessService, never()).startProcess(any(YidaProcessStartRequest.class));
+    }
+
     @Test
     void rejectsMainDataTjrWhenEmployeeNoCannotBeResolved() {
         Logger logger = mock(Logger.class);