diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java index 6205362e..955f2fdb 100644 --- a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java @@ -12,6 +12,8 @@ public interface ErrorCodeConstants { // ================ 出入库表相关错误码 ================= ErrorCode IOSTOR_INV_NOT_EXISTS = new ErrorCode(4, "出入库单主表不存在"); + ErrorCode IOSTOR_INV_CODE_GENERATE_FAILED = new ErrorCode(7, "单据号生成失败"); + ErrorCode IOSTORINV_DTL_NOT_EXISTS = new ErrorCode(5, "出入库单明细不存在"); ErrorCode IOSTORINV_DIS_NOT_EXISTS = new ErrorCode(6, "出入库单分配不存在"); diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java index 919f9a4d..82f8c078 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java @@ -38,6 +38,13 @@ public class IostorInvController { @Resource private IostorInvService iostorInvService; + @PostMapping("/createOutbound") + @Operation(summary = "创建出库单及明细") + @PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')") + public CommonResult createOutbound(@Valid @RequestBody IostorInvCreateReqVO reqVO) { + return success(iostorInvService.createOutbound(reqVO)); + } + @GetMapping("/availableInventoryPage") @Operation(summary = "获得可用库存分页") @PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')") diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java index 17623860..89623dbf 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java @@ -1,11 +1,17 @@ package cn.code.nl.module.wms.service.iostorinv; import cn.hutool.core.collection.CollUtil; +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.module.base.api.codegen.CodeGenApi; +import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; +import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.util.*; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.*; import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO; @@ -28,6 +34,7 @@ import static cn.code.nl.module.wms.enums.ErrorCodeConstants.*; */ @Service @Validated +@Slf4j public class IostorInvServiceImpl implements IostorInvService { @Resource @@ -36,9 +43,59 @@ public class IostorInvServiceImpl implements IostorInvService { @Resource private IostorinvDtlMapper iostorinvDtlMapper; + @Resource + private CodeGenApi codeGenApi; + + /** + * 创建出库单及其明细。 + */ @Override + @Transactional(rollbackFor = Exception.class) public String createOutbound(IostorInvCreateReqVO reqVO) { - throw new UnsupportedOperationException("出库单聚合保存功能尚未实现"); + CodeGenerateReqDTO codeGenerateReqDTO = new CodeGenerateReqDTO(); + codeGenerateReqDTO.setRuleCode("IO_CODE"); + CommonResult codeResult = codeGenApi.generate(codeGenerateReqDTO); + if (codeResult == null || !codeResult.isSuccess() || codeResult.getData() == null) { + String remoteMessage = codeResult == null ? "返回结果为空" : codeResult.getMsg(); + log.error("[createOutbound][生成出库单号失败,远程信息:{}]", remoteMessage); + throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); + } + + BigDecimal totalWeight = reqVO.getDetails().stream() + .map(IostorInvCreateReqVO.Detail::getPlanQty) + .reduce(BigDecimal.ZERO, BigDecimal::add); + IostorInvDO iostorInv = new IostorInvDO(); + iostorInv.setBillCode(codeResult.getData()); + iostorInv.setIoType("OUT"); + iostorInv.setBillType(reqVO.getBillType()); + iostorInv.setBizDate(reqVO.getBizDate()); + iostorInv.setStorId(reqVO.getStorId()); + iostorInv.setBillStatus("生成"); + iostorInv.setRemark(reqVO.getRemark()); + iostorInv.setDetailCount(reqVO.getDetails().size()); + iostorInv.setTotalWeight(totalWeight); + iostorInvMapper.insert(iostorInv); + + for (int index = 0; index < reqVO.getDetails().size(); index++) { + IostorInvCreateReqVO.Detail detail = reqVO.getDetails().get(index); + IostorinvDtlDO detailDO = new IostorinvDtlDO(); + detailDO.setIostorinvId(iostorInv.getIostorinvId()); + detailDO.setSeqNo(index + 1); + detailDO.setMaterialCode(detail.getMaterialCode()); + detailDO.setMaterialId(detail.getMaterialId()); + detailDO.setPcsn(detail.getPcsn()); + detailDO.setPlanQty(detail.getPlanQty()); + detailDO.setUnassignQty(detail.getPlanQty()); + detailDO.setAssignQty(BigDecimal.ZERO); + detailDO.setQtyUnitId(detail.getQtyUnitId()); + detailDO.setQtyUnitName(detail.getQtyUnitName()); + detailDO.setSourceBillCode(detail.getSourceBillCode()); + detailDO.setSourceBillType(detail.getSourceBillType()); + detailDO.setSourceBilldtlId(detail.getSourceBilldtlId()); + detailDO.setRemark(detail.getRemark()); + iostorinvDtlMapper.insert(detailDO); + } + return iostorInv.getIostorinvId(); } @Override diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index b6615b70..0632d5b5 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -1,38 +1,200 @@ package cn.code.nl.module.wms.service.iostorinv; +import cn.code.nl.framework.common.exception.ServiceException; +import cn.code.nl.framework.common.pojo.CommonResult; +import cn.code.nl.module.base.api.codegen.CodeGenApi; +import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.incrementer.H2KeyGenerator; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import jakarta.annotation.Resource; -import jakarta.validation.ConstraintViolationException; +import org.apache.ibatis.session.SqlSessionFactory; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import javax.sql.DataSource; +import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.Collections; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -@SpringBootTest -@ActiveProfiles("local") +@SpringJUnitConfig +@ContextConfiguration(classes = IostorInvServiceLocalSpringTest.TestConfiguration.class) +@Sql("/sql/iostorinv/create-outbound.sql") class IostorInvServiceLocalSpringTest { @Resource private IostorInvService iostorInvService; + @Resource + private JdbcTemplate jdbcTemplate; + @Resource + private TestCodeGenApi codeGenApi; - @Test - void createOutboundShouldRejectEmptyDetails() { - IostorInvCreateReqVO reqVO = new IostorInvCreateReqVO(); - reqVO.setBillType("OUTBOUND"); - reqVO.setStorId("stor-1"); - reqVO.setBizDate(LocalDateTime.now()); - reqVO.setDetails(Collections.emptyList()); - - ConstraintViolationException exception = assertThrows(ConstraintViolationException.class, - () -> iostorInvService.createOutbound(reqVO)); - - assertTrue(exception.getConstraintViolations().stream() - .anyMatch(violation -> violation.getPropertyPath().toString().contains("details"))); + @BeforeEach + void resetCodeGenApi() { + codeGenApi.setResult(CommonResult.success("OUT-TEST-0001")); } + @Test + void shouldCreateOutboundHeaderAndDetailsInOneTransaction() { + String id = iostorInvService.createOutbound(buildRequest()); + + assertNotNull(id); + jdbcTemplate.queryForObject("SELECT * FROM wms_iostorinv WHERE iostorinv_id = ?", (rs, rowNum) -> { + assertEquals("OUT-TEST-0001", rs.getString("bill_code")); + assertEquals("OUT", rs.getString("io_type")); + assertEquals("销售出库", rs.getString("bill_type")); + assertEquals("STOR-01", rs.getString("stor_id")); + assertEquals("生成", rs.getString("bill_status")); + assertEquals("主表备注", rs.getString("remark")); + assertEquals(2, rs.getInt("detail_count")); + assertEquals(new BigDecimal("19.750"), rs.getBigDecimal("total_weight")); + return null; + }, id); + + List details = jdbcTemplate.query( + "SELECT * FROM wms_iostorinvdtl WHERE iostorinv_id = ? ORDER BY seq_no", + (rs, rowNum) -> new DetailRow(rs.getInt("seq_no"), rs.getString("material_code"), + rs.getString("material_id"), rs.getString("pcsn"), rs.getBigDecimal("plan_qty"), + rs.getBigDecimal("assign_qty"), rs.getBigDecimal("unassign_qty"), + rs.getString("qty_unit_id"), rs.getString("qty_unit_name"), + rs.getString("source_bill_code"), rs.getString("source_bill_type"), + rs.getString("source_billdtl_id"), rs.getString("remark")), id); + assertEquals(List.of( + new DetailRow(1, "MAT-01", "MID-01", "PCSN-01", new BigDecimal("12.500"), + BigDecimal.ZERO.setScale(3), new BigDecimal("12.500"), "KG", "千克", + "SRC-01", "ORDER", "SRC-DTL-01", "明细一"), + new DetailRow(2, "MAT-02", "MID-02", "PCSN-02", new BigDecimal("7.250"), + BigDecimal.ZERO.setScale(3), new BigDecimal("7.250"), "KG", "千克", + "SRC-02", "ORDER", "SRC-DTL-02", "明细二")), details); + } + + @Test + void shouldNotInsertAnythingWhenCodeGenerationFails() { + codeGenApi.setResult(CommonResult.error(500, "编码服务暂不可用")); + + ServiceException exception = assertThrows(ServiceException.class, + () -> iostorInvService.createOutbound(buildRequest())); + + assertEquals("单据号生成失败", exception.getMessage()); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + + private IostorInvCreateReqVO buildRequest() { + IostorInvCreateReqVO request = new IostorInvCreateReqVO(); + request.setBillType("销售出库"); + request.setStorId("STOR-01"); + request.setBizDate(LocalDateTime.of(2026, 7, 22, 0, 0)); + request.setRemark("主表备注"); + request.setDetails(List.of(buildDetail("01", "12.500", "明细一"), + buildDetail("02", "7.250", "明细二"))); + return request; + } + + private IostorInvCreateReqVO.Detail buildDetail(String suffix, String planQty, String remark) { + IostorInvCreateReqVO.Detail detail = new IostorInvCreateReqVO.Detail(); + detail.setMaterialCode("MAT-" + suffix); + detail.setMaterialId("MID-" + suffix); + detail.setPcsn("PCSN-" + suffix); + detail.setPlanQty(new BigDecimal(planQty)); + detail.setQtyUnitId("KG"); + detail.setQtyUnitName("千克"); + detail.setSourceBillCode("SRC-" + suffix); + detail.setSourceBillType("ORDER"); + detail.setSourceBilldtlId("SRC-DTL-" + suffix); + detail.setRemark(remark); + return detail; + } + + private record DetailRow(Integer seqNo, String materialCode, String materialId, String pcsn, + BigDecimal planQty, BigDecimal assignQty, BigDecimal unassignQty, + String qtyUnitId, String qtyUnitName, String sourceBillCode, + String sourceBillType, String sourceBilldtlId, String remark) { + } + + @Configuration(proxyBeanMethods = false) + @EnableTransactionManagement + @MapperScan(basePackages = {"cn.code.nl.module.wms.dal.mysql.iostorinv", + "cn.code.nl.module.wms.dal.mysql.iostorinvdtl"}) + static class TestConfiguration { + + @Bean + DataSource dataSource() { + return new DriverManagerDataSource("jdbc:h2:mem:create_outbound;MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + } + + @Bean + SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(dataSource); + GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig(); + dbConfig.setKeyGenerators(List.of(new H2KeyGenerator())); + factory.setGlobalConfig(new GlobalConfig().setDbConfig(dbConfig)); + org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); + configuration.setMapUnderscoreToCamelCase(true); + factory.setConfiguration(configuration); + return factory.getObject(); + } + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Bean + JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + static MethodValidationPostProcessor methodValidationPostProcessor() { + return new MethodValidationPostProcessor(); + } + + @Bean + IostorInvService iostorInvService() { + return new IostorInvServiceImpl(); + } + + @Bean + TestCodeGenApi codeGenApi() { + return new TestCodeGenApi(); + } + } + + static class TestCodeGenApi implements CodeGenApi { + private CommonResult result; + + void setResult(CommonResult result) { + this.result = result; + } + + @Override + public CommonResult generate(CodeGenerateReqDTO reqDTO) { + assertEquals("IO_CODE", reqDTO.getRuleCode()); + return result; + } + + @Override + public CommonResult preview(String ruleCode) { + throw new UnsupportedOperationException(); + } + } } diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql new file mode 100644 index 00000000..8d3f1e65 --- /dev/null +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql @@ -0,0 +1,30 @@ +DROP TABLE IF EXISTS wms_iostorinvdtl; +DROP TABLE IF EXISTS wms_iostorinv; +DROP SEQUENCE IF EXISTS wms_iostorinvdtl_seq; +DROP SEQUENCE IF EXISTS wms_iostorinv_seq; + +CREATE SEQUENCE wms_iostorinv_seq START WITH 1; +CREATE SEQUENCE wms_iostorinvdtl_seq START WITH 1; + +CREATE TABLE wms_iostorinv ( + iostorinv_id VARCHAR(64) PRIMARY KEY, + bill_code VARCHAR(64), io_type VARCHAR(32), bill_type VARCHAR(64), biz_date TIMESTAMP, + stor_id VARCHAR(64), stor_code VARCHAR(64), stor_name VARCHAR(128), source_id VARCHAR(64), + source_name VARCHAR(128), source_type VARCHAR(64), total_qty DECIMAL(18, 3), + total_weight DECIMAL(18, 3), detail_count INT, bill_status VARCHAR(32), remark VARCHAR(255), + create_mode VARCHAR(32), dis_optid VARCHAR(64), dis_time TIMESTAMP, confirm_optid VARCHAR(64), + confirm_time TIMESTAMP, sysdeptid VARCHAR(64), syscompanyid VARCHAR(64), is_upload VARCHAR(16), + upload_optid VARCHAR(64), upload_time VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP, + creator VARCHAR(64), updater VARCHAR(64), deleted BOOLEAN DEFAULT FALSE +); + +CREATE TABLE wms_iostorinvdtl ( + iostorinvdtl_id VARCHAR(64) PRIMARY KEY, iostorinv_id VARCHAR(64), seq_no INT, + pcsn VARCHAR(128), bill_status VARCHAR(32), qty_unit_id VARCHAR(64), qty_unit_name VARCHAR(64), + plan_qty DECIMAL(18, 3), real_qty DECIMAL(18, 3), source_billdtl_id VARCHAR(64), + source_bill_type VARCHAR(64), source_bill_code VARCHAR(64), source_bill_table VARCHAR(128), + remark VARCHAR(255), assign_qty DECIMAL(18, 3), unassign_qty DECIMAL(18, 3), + material_code VARCHAR(64), source_load_port VARCHAR(64), callback_strategy VARCHAR(255), + material_id VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP, + creator VARCHAR(64), updater VARCHAR(64), deleted BOOLEAN DEFAULT FALSE +);