From 63cae8bc311bc9c3956cdf8d0bb18c530d536ee1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 06:29:06 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=88wms?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 1 + .../src/main/resources/application.yaml | 12 +- yudao-module-wms/pom.xml | 24 + yudao-module-wms/yudao-module-wms-api/pom.xml | 33 + .../yudao/module/wms/enums/ApiConstants.java | 23 + .../module/wms/enums/DictTypeConstants.java | 16 + .../module/wms/enums/ErrorCodeConstants.java | 91 +++ .../wms/enums/md/WmsMerchantTypeEnum.java | 38 ++ .../wms/enums/order/WmsOrderStatusEnum.java | 39 ++ .../enums/order/WmsOrderTypeConstants.java | 28 + .../wms/enums/order/WmsOrderTypeEnum.java | 39 ++ .../enums/order/WmsReceiptOrderTypeEnum.java | 39 ++ .../enums/order/WmsShipmentOrderTypeEnum.java | 38 ++ .../yudao-module-wms-server/Dockerfile | 19 + .../yudao-module-wms-server/pom.xml | 119 ++++ .../module/wms/WmsServerApplication.java | 30 + .../home/WmsHomeStatisticsController.java | 64 ++ .../vo/WmsHomeInventorySummaryRespVO.java | 52 ++ .../home/vo/WmsHomeOrderStatusRespVO.java | 16 + .../home/vo/WmsHomeOrderSummaryRespVO.java | 21 + .../home/vo/WmsHomeOrderTrendRespVO.java | 27 + .../inventory/WmsInventoryController.java | 87 +++ .../WmsInventoryHistoryController.java | 79 +++ .../inventory/vo/WmsInventoryListReqVO.java | 15 + .../inventory/vo/WmsInventoryPageReqVO.java | 56 ++ .../inventory/vo/WmsInventoryRespVO.java | 44 ++ .../history/WmsInventoryHistoryPageReqVO.java | 48 ++ .../vo/history/WmsInventoryHistoryRespVO.java | 59 ++ .../admin/md/item/WmsItemBrandController.java | 100 +++ .../md/item/WmsItemCategoryController.java | 81 +++ .../admin/md/item/WmsItemController.java | 150 +++++ .../admin/md/item/WmsItemSkuController.java | 89 +++ .../item/vo/brand/WmsItemBrandPageReqVO.java | 21 + .../md/item/vo/brand/WmsItemBrandRespVO.java | 31 + .../item/vo/brand/WmsItemBrandSaveReqVO.java | 25 + .../vo/category/WmsItemCategoryListReqVO.java | 25 + .../vo/category/WmsItemCategoryRespVO.java | 33 + .../vo/category/WmsItemCategorySaveReqVO.java | 41 ++ .../md/item/vo/item/WmsItemListReqVO.java | 31 + .../md/item/vo/item/WmsItemPageReqVO.java | 36 + .../admin/md/item/vo/item/WmsItemRespVO.java | 58 ++ .../md/item/vo/item/WmsItemSaveReqVO.java | 51 ++ .../md/item/vo/sku/WmsItemSkuPageReqVO.java | 36 + .../md/item/vo/sku/WmsItemSkuRespVO.java | 73 ++ .../md/item/vo/sku/WmsItemSkuSaveReqVO.java | 62 ++ .../md/merchant/WmsMerchantController.java | 101 +++ .../md/merchant/vo/WmsMerchantListReqVO.java | 28 + .../md/merchant/vo/WmsMerchantPageReqVO.java | 33 + .../md/merchant/vo/WmsMerchantRespVO.java | 68 ++ .../md/merchant/vo/WmsMerchantSaveReqVO.java | 69 ++ .../md/warehouse/WmsWarehouseController.java | 100 +++ .../warehouse/vo/WmsWarehousePageReqVO.java | 21 + .../md/warehouse/vo/WmsWarehouseRespVO.java | 39 ++ .../warehouse/vo/WmsWarehouseSaveReqVO.java | 32 + .../order/check/WmsCheckOrderController.java | 198 ++++++ .../check/WmsCheckOrderDetailController.java | 81 +++ .../vo/detail/WmsCheckOrderDetailRespVO.java | 64 ++ .../detail/WmsCheckOrderDetailSaveReqVO.java | 42 ++ .../vo/order/WmsCheckOrderPageReqVO.java | 69 ++ .../check/vo/order/WmsCheckOrderRespVO.java | 79 +++ .../vo/order/WmsCheckOrderSaveReqVO.java | 42 ++ .../movement/WmsMovementOrderController.java | 206 ++++++ .../WmsMovementOrderDetailController.java | 86 +++ .../detail/WmsMovementOrderDetailRespVO.java | 64 ++ .../WmsMovementOrderDetailSaveReqVO.java | 34 + .../vo/order/WmsMovementOrderPageReqVO.java | 66 ++ .../vo/order/WmsMovementOrderRespVO.java | 82 +++ .../vo/order/WmsMovementOrderSaveReqVO.java | 46 ++ .../receipt/WmsReceiptOrderController.java | 204 ++++++ .../WmsReceiptOrderDetailController.java | 81 +++ .../detail/WmsReceiptOrderDetailRespVO.java | 58 ++ .../WmsReceiptOrderDetailSaveReqVO.java | 34 + .../vo/order/WmsReceiptOrderPageReqVO.java | 74 ++ .../vo/order/WmsReceiptOrderRespVO.java | 91 +++ .../vo/order/WmsReceiptOrderSaveReqVO.java | 56 ++ .../shipment/WmsShipmentOrderController.java | 204 ++++++ .../WmsShipmentOrderDetailController.java | 81 +++ .../detail/WmsShipmentOrderDetailRespVO.java | 58 ++ .../WmsShipmentOrderDetailSaveReqVO.java | 34 + .../vo/order/WmsShipmentOrderPageReqVO.java | 74 ++ .../vo/order/WmsShipmentOrderRespVO.java | 91 +++ .../vo/order/WmsShipmentOrderSaveReqVO.java | 56 ++ .../wms/controller/admin/package-info.java | 4 + .../module/wms/controller/package-info.java | 6 + .../dataobject/inventory/WmsInventoryDO.java | 54 ++ .../inventory/WmsInventoryHistoryDO.java | 94 +++ .../dataobject/md/item/WmsItemBrandDO.java | 38 ++ .../dataobject/md/item/WmsItemCategoryDO.java | 60 ++ .../wms/dal/dataobject/md/item/WmsItemDO.java | 58 ++ .../dal/dataobject/md/item/WmsItemSkuDO.java | 81 +++ .../dataobject/md/merchant/WmsMerchantDO.java | 81 +++ .../md/warehouse/WmsWarehouseDO.java | 46 ++ .../order/check/WmsCheckOrderDO.java | 77 +++ .../order/check/WmsCheckOrderDetailDO.java | 85 +++ .../order/movement/WmsMovementOrderDO.java | 82 +++ .../movement/WmsMovementOrderDetailDO.java | 81 +++ .../order/receipt/WmsReceiptOrderDO.java | 92 +++ .../receipt/WmsReceiptOrderDetailDO.java | 72 ++ .../order/shipment/WmsShipmentOrderDO.java | 92 +++ .../shipment/WmsShipmentOrderDetailDO.java | 72 ++ .../mysql/home/WmsHomeStatisticsMapper.java | 67 ++ .../inventory/WmsInventoryHistoryMapper.java | 39 ++ .../mysql/inventory/WmsInventoryMapper.java | 126 ++++ .../dal/mysql/md/item/WmsItemBrandMapper.java | 33 + .../mysql/md/item/WmsItemCategoryMapper.java | 49 ++ .../wms/dal/mysql/md/item/WmsItemMapper.java | 58 ++ .../dal/mysql/md/item/WmsItemSkuMapper.java | 76 +++ .../mysql/md/merchant/WmsMerchantMapper.java | 47 ++ .../md/warehouse/WmsWarehouseMapper.java | 42 ++ .../check/WmsCheckOrderDetailMapper.java | 38 ++ .../order/check/WmsCheckOrderMapper.java | 52 ++ .../WmsMovementOrderDetailMapper.java | 38 ++ .../movement/WmsMovementOrderMapper.java | 53 ++ .../receipt/WmsReceiptOrderDetailMapper.java | 38 ++ .../order/receipt/WmsReceiptOrderMapper.java | 57 ++ .../WmsShipmentOrderDetailMapper.java | 38 ++ .../shipment/WmsShipmentOrderMapper.java | 57 ++ .../module/wms/framework/package-info.java | 6 + .../rpc/config/RpcConfiguration.java | 10 + .../web/config/WmsWebConfiguration.java | 24 + .../wms/framework/web/package-info.java | 4 + .../yudao/module/wms/package-info.java | 10 + .../home/WmsHomeStatisticsService.java | 43 ++ .../home/WmsHomeStatisticsServiceImpl.java | 134 ++++ .../inventory/WmsInventoryHistoryService.java | 31 + .../WmsInventoryHistoryServiceImpl.java | 41 ++ .../inventory/WmsInventoryService.java | 65 ++ .../inventory/WmsInventoryServiceImpl.java | 297 ++++++++ .../dto/WmsInventoryChangeReqDTO.java | 73 ++ .../dto/WmsInventoryCheckReqDTO.java | 77 +++ .../service/md/item/WmsItemBrandService.java | 93 +++ .../md/item/WmsItemBrandServiceImpl.java | 137 ++++ .../md/item/WmsItemCategoryService.java | 94 +++ .../md/item/WmsItemCategoryServiceImpl.java | 193 ++++++ .../wms/service/md/item/WmsItemService.java | 111 +++ .../service/md/item/WmsItemServiceImpl.java | 171 +++++ .../service/md/item/WmsItemSkuService.java | 116 ++++ .../md/item/WmsItemSkuServiceImpl.java | 190 ++++++ .../md/merchant/WmsMerchantService.java | 113 ++++ .../md/merchant/WmsMerchantServiceImpl.java | 166 +++++ .../md/warehouse/WmsWarehouseService.java | 87 +++ .../md/warehouse/WmsWarehouseServiceImpl.java | 166 +++++ .../check/WmsCheckOrderDetailService.java | 71 ++ .../check/WmsCheckOrderDetailServiceImpl.java | 120 ++++ .../order/check/WmsCheckOrderService.java | 76 +++ .../order/check/WmsCheckOrderServiceImpl.java | 222 ++++++ .../WmsMovementOrderDetailService.java | 71 ++ .../WmsMovementOrderDetailServiceImpl.java | 132 ++++ .../movement/WmsMovementOrderService.java | 76 +++ .../movement/WmsMovementOrderServiceImpl.java | 243 +++++++ .../receipt/WmsReceiptOrderDetailService.java | 71 ++ .../WmsReceiptOrderDetailServiceImpl.java | 131 ++++ .../order/receipt/WmsReceiptOrderService.java | 84 +++ .../receipt/WmsReceiptOrderServiceImpl.java | 241 +++++++ .../WmsShipmentOrderDetailService.java | 71 ++ .../WmsShipmentOrderDetailServiceImpl.java | 131 ++++ .../shipment/WmsShipmentOrderService.java | 84 +++ .../shipment/WmsShipmentOrderServiceImpl.java | 244 +++++++ .../src/main/resources/application-dev.yaml | 120 ++++ .../src/main/resources/application-local.yaml | 139 ++++ .../src/main/resources/application.yaml | 124 ++++ .../src/main/resources/logback-spring.xml | 56 ++ .../mapper/home/WmsHomeStatisticsMapper.xml | 154 +++++ .../WmsInventoryServiceImplTest.java | 632 ++++++++++++++++++ .../md/item/WmsItemBrandServiceImplTest.java | 64 ++ .../WmsWarehouseServiceImplTest.java | 100 +++ .../WmsCheckOrderDetailServiceImplTest.java | 154 +++++ .../check/WmsCheckOrderServiceImplTest.java | 347 ++++++++++ ...WmsMovementOrderDetailServiceImplTest.java | 155 +++++ .../WmsMovementOrderServiceImplTest.java | 291 ++++++++ .../WmsReceiptOrderDetailServiceImplTest.java | 149 +++++ .../WmsReceiptOrderServiceImplTest.java | 271 ++++++++ ...WmsShipmentOrderDetailServiceImplTest.java | 150 +++++ .../WmsShipmentOrderServiceImplTest.java | 271 ++++++++ .../test/resources/application-unit-test.yaml | 35 + .../src/test/resources/sql/clean.sql | 16 + .../src/test/resources/sql/create_tables.sql | 320 +++++++++ 177 files changed, 15096 insertions(+), 1 deletion(-) create mode 100644 yudao-module-wms/pom.xml create mode 100644 yudao-module-wms/yudao-module-wms-api/pom.xml create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ApiConstants.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/md/WmsMerchantTypeEnum.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderStatusEnum.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeEnum.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsReceiptOrderTypeEnum.java create mode 100644 yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsShipmentOrderTypeEnum.java create mode 100644 yudao-module-wms/yudao-module-wms-server/Dockerfile create mode 100644 yudao-module-wms/yudao-module-wms-server/pom.xml create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/WmsServerApplication.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/WmsHomeStatisticsController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeInventorySummaryRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderStatusRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderSummaryRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderTrendRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryHistoryController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryListReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemBrandController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemCategoryController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemSkuController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryListReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategorySaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemListReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/WmsMerchantController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantListReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/WmsWarehouseController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehousePageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderDetailController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderDetailController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderDetailController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderDetailController.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderPageReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderRespVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderSaveReqVO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/package-info.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/package-info.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryHistoryDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemBrandDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemCategoryDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemSkuDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/merchant/WmsMerchantDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/warehouse/WmsWarehouseDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDetailDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDetailDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDetailDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDetailDO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/home/WmsHomeStatisticsMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryHistoryMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemBrandMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemCategoryMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemSkuMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/merchant/WmsMerchantMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/warehouse/WmsWarehouseMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderMapper.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/package-info.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/rpc/config/RpcConfiguration.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/config/WmsWebConfiguration.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/package-info.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/package-info.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryChangeReqDTO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryCheckReqDTO.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/resources/logback-spring.xml create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImplTest.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/resources/application-unit-test.yaml create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/clean.sql create mode 100644 yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/create_tables.sql diff --git a/pom.xml b/pom.xml index ec40c3b5b..84abf6468 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ yudao-module-erp yudao-module-iot yudao-module-mes + yudao-module-wms yudao-module-ai diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml index 2a06551b1..4730c850c 100644 --- a/yudao-gateway/src/main/resources/application.yaml +++ b/yudao-gateway/src/main/resources/application.yaml @@ -202,6 +202,13 @@ spring: - Path=/admin-api/mes/** filters: - RewritePath=/admin-api/mes/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs + ## wms-server 服务 + - id: wms-admin-api # 路由的编号 + uri: grayLb://wms-server + predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 + - Path=/admin-api/wms/** + filters: + - RewritePath=/admin-api/wms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs x-forwarded: prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀 default-filters: @@ -264,9 +271,12 @@ knife4j: - name: mes-server service-name: mes-server url: /admin-api/mes/v3/api-docs + - name: wms-server + service-name: wms-server + url: /admin-api/wms/v3/api-docs --- #################### 芋道相关配置 #################### yudao: info: - version: 1.0.0 \ No newline at end of file + version: 1.0.0 diff --git a/yudao-module-wms/pom.xml b/yudao-module-wms/pom.xml new file mode 100644 index 000000000..6e3ee5fcb --- /dev/null +++ b/yudao-module-wms/pom.xml @@ -0,0 +1,24 @@ + + + + cn.iocoder.cloud + yudao + ${revision} + + + yudao-module-wms-api + yudao-module-wms-server + + 4.0.0 + yudao-module-wms + pom + + ${project.artifactId} + + wms 模块下,仓库管理系统(Warehouse Management System)。 + 例如说:仓库、物料、库存、入库、出库、移库、盘库等等 + + + diff --git a/yudao-module-wms/yudao-module-wms-api/pom.xml b/yudao-module-wms/yudao-module-wms-api/pom.xml new file mode 100644 index 000000000..91f1914ff --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/pom.xml @@ -0,0 +1,33 @@ + + + + cn.iocoder.cloud + yudao-module-wms + ${revision} + + 4.0.0 + yudao-module-wms-api + jar + + ${project.artifactId} + + wms 模块 API,暴露给其它模块调用 + + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ApiConstants.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ApiConstants.java new file mode 100644 index 000000000..c374a4f8d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ApiConstants.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.wms.enums; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; + +/** + * API 相关的枚举 + * + * @author 芋道源码 + */ +public class ApiConstants { + + /** + * 服务名 + * + * 注意,需要保证和 spring.application.name 保持一致 + */ + public static final String NAME = "wms-server"; + + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/wms"; + + public static final String VERSION = "1.0.0"; + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java new file mode 100644 index 000000000..e854bc530 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/DictTypeConstants.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.wms.enums; + +/** + * WMS 字典类型常量 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String MERCHANT_TYPE = "merchant_type"; + String ORDER_STATUS = "wms_order_status"; + String ORDER_TYPE = "wms_order_type"; + String RECEIPT_ORDER_TYPE = "wms_receipt_order_type"; + String SHIPMENT_ORDER_TYPE = "wms_shipment_order_type"; + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java new file mode 100644 index 000000000..217562741 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/ErrorCodeConstants.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.wms.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * WMS 错误码枚举类 + *

+ * wms 系统,使用 1-060-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== WMS 基础数据-仓库 1-060-100-000 ========== + ErrorCode WAREHOUSE_NOT_EXISTS = new ErrorCode(1_060_100_000, "仓库不存在"); + ErrorCode WAREHOUSE_NAME_DUPLICATE = new ErrorCode(1_060_100_001, "仓库名称重复"); + ErrorCode WAREHOUSE_CODE_DUPLICATE = new ErrorCode(1_060_100_002, "仓库编号重复"); + ErrorCode WAREHOUSE_HAS_ORDER = new ErrorCode(1_060_100_004, "删除失败!仓库已被{}使用!"); + ErrorCode WAREHOUSE_HAS_INVENTORY = new ErrorCode(1_060_100_005, "删除失败!仓库已存在库存余额!"); + + // ========== WMS 基础数据-商品分类 1-060-102-000 ========== + ErrorCode ITEM_CATEGORY_NOT_EXISTS = new ErrorCode(1_060_102_000, "商品分类不存在"); + ErrorCode ITEM_CATEGORY_NAME_DUPLICATE = new ErrorCode(1_060_102_001, "商品分类名称重复"); + ErrorCode ITEM_CATEGORY_PARENT_NOT_EXISTS = new ErrorCode(1_060_102_002, "父商品分类不存在"); + ErrorCode ITEM_CATEGORY_PARENT_ERROR = new ErrorCode(1_060_102_003, "不能设置自己为父商品分类"); + ErrorCode ITEM_CATEGORY_PARENT_IS_CHILD = new ErrorCode(1_060_102_004, "不能设置自己的子商品分类为父商品分类"); + ErrorCode ITEM_CATEGORY_HAS_CHILDREN = new ErrorCode(1_060_102_005, "删除失败!请先删除该分类下的子分类!"); + ErrorCode ITEM_CATEGORY_HAS_ITEM = new ErrorCode(1_060_102_006, "删除失败!分类已被商品使用!"); + ErrorCode ITEM_CATEGORY_CODE_DUPLICATE = new ErrorCode(1_060_102_007, "商品分类编号重复"); + + // ========== WMS 基础数据-商品品牌 1-060-103-000 ========== + ErrorCode ITEM_BRAND_NOT_EXISTS = new ErrorCode(1_060_103_000, "商品品牌不存在"); + ErrorCode ITEM_BRAND_HAS_ITEM = new ErrorCode(1_060_103_001, "删除失败!品牌已被商品使用!"); + ErrorCode ITEM_BRAND_CODE_DUPLICATE = new ErrorCode(1_060_103_002, "商品品牌编号重复"); + ErrorCode ITEM_BRAND_NAME_DUPLICATE = new ErrorCode(1_060_103_003, "商品品牌名称重复"); + + // ========== WMS 基础数据-商品 1-060-104-000 ========== + ErrorCode ITEM_NOT_EXISTS = new ErrorCode(1_060_104_000, "商品不存在"); + ErrorCode ITEM_NAME_DUPLICATE = new ErrorCode(1_060_104_001, "商品名称重复"); + ErrorCode ITEM_CODE_DUPLICATE = new ErrorCode(1_060_104_007, "商品编号重复"); + ErrorCode ITEM_SKU_REQUIRED = new ErrorCode(1_060_104_002, "至少包含一个商品规格"); + ErrorCode ITEM_SKU_NAME_DUPLICATE = new ErrorCode(1_060_104_003, "商品规格名称【{}】重复"); + ErrorCode ITEM_SKU_NOT_EXISTS = new ErrorCode(1_060_104_004, "商品规格不存在"); + ErrorCode ITEM_SKU_HAS_INVENTORY = new ErrorCode(1_060_104_005, "删除失败!商品规格【{}】已被库存业务使用!"); + ErrorCode ITEM_SKU_HAS_ORDER = new ErrorCode(1_060_104_006, "删除失败!商品规格【{}】已被{}使用!"); + + // ========== WMS 基础数据-往来企业 1-060-105-000 ========== + ErrorCode MERCHANT_NOT_EXISTS = new ErrorCode(1_060_105_000, "往来企业不存在"); + ErrorCode MERCHANT_NOT_SUPPLIER = new ErrorCode(1_060_105_001, "往来企业必须是供应商或客户/供应商类型"); + ErrorCode MERCHANT_NOT_CUSTOMER = new ErrorCode(1_060_105_002, "往来企业必须是客户或客户/供应商类型"); + ErrorCode MERCHANT_HAS_ORDER = new ErrorCode(1_060_105_003, "删除失败!往来企业已被{}使用!"); + ErrorCode MERCHANT_CODE_DUPLICATE = new ErrorCode(1_060_105_004, "往来企业编号重复"); + ErrorCode MERCHANT_NAME_DUPLICATE = new ErrorCode(1_060_105_005, "往来企业名称重复"); + + // ========== WMS 入库单 1-060-200-000 ========== + ErrorCode RECEIPT_ORDER_NOT_EXISTS = new ErrorCode(1_060_200_000, "入库单不存在"); + ErrorCode RECEIPT_ORDER_NO_DUPLICATE = new ErrorCode(1_060_200_001, "入库单号重复"); + ErrorCode RECEIPT_ORDER_STATUS_NOT_PREPARE = new ErrorCode(1_060_200_002, "入库单状态不是草稿,不能操作"); + ErrorCode RECEIPT_ORDER_DETAIL_REQUIRED = new ErrorCode(1_060_200_003, "入库单至少包含一条明细"); + ErrorCode RECEIPT_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_200_005, "入库单状态不是草稿或已作废,不能删除"); + ErrorCode RECEIPT_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_200_007, "入库单明细不存在"); + + // ========== WMS 出库单 1-060-201-000 ========== + ErrorCode SHIPMENT_ORDER_NOT_EXISTS = new ErrorCode(1_060_201_000, "出库单不存在"); + ErrorCode SHIPMENT_ORDER_NO_DUPLICATE = new ErrorCode(1_060_201_001, "出库单号重复"); + ErrorCode SHIPMENT_ORDER_STATUS_NOT_PREPARE = new ErrorCode(1_060_201_002, "出库单状态不是草稿,不能操作"); + ErrorCode SHIPMENT_ORDER_DETAIL_REQUIRED = new ErrorCode(1_060_201_003, "出库单至少包含一条明细"); + ErrorCode SHIPMENT_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_201_005, "出库单状态不是草稿或已作废,不能删除"); + ErrorCode SHIPMENT_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_201_007, "出库单明细不存在"); + + // ========== WMS 移库单 1-060-202-000 ========== + ErrorCode MOVEMENT_ORDER_NOT_EXISTS = new ErrorCode(1_060_202_000, "移库单不存在"); + ErrorCode MOVEMENT_ORDER_NO_DUPLICATE = new ErrorCode(1_060_202_001, "移库单号重复"); + ErrorCode MOVEMENT_ORDER_STATUS_NOT_PREPARE = new ErrorCode(1_060_202_002, "移库单状态不是草稿,不能操作"); + ErrorCode MOVEMENT_ORDER_DETAIL_REQUIRED = new ErrorCode(1_060_202_003, "移库单至少包含一条明细"); + ErrorCode MOVEMENT_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_202_005, "移库单状态不是草稿或已作废,不能删除"); + ErrorCode MOVEMENT_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_202_006, "移库单明细不存在"); + ErrorCode MOVEMENT_ORDER_WAREHOUSE_SAME = new ErrorCode(1_060_202_007, "来源仓库和目标仓库不能相同"); + + // ========== WMS 盘库单 1-060-203-000 ========== + ErrorCode CHECK_ORDER_NOT_EXISTS = new ErrorCode(1_060_203_000, "盘库单不存在"); + ErrorCode CHECK_ORDER_NO_DUPLICATE = new ErrorCode(1_060_203_001, "盘库单号重复"); + ErrorCode CHECK_ORDER_STATUS_NOT_PREPARE = new ErrorCode(1_060_203_002, "盘库单状态不是草稿,不能操作"); + ErrorCode CHECK_ORDER_DETAIL_REQUIRED = new ErrorCode(1_060_203_003, "盘库单至少包含一条明细"); + ErrorCode CHECK_ORDER_STATUS_NOT_DELETABLE = new ErrorCode(1_060_203_005, "盘库单状态不是草稿或已作废,不能删除"); + ErrorCode CHECK_ORDER_DETAIL_NOT_EXISTS = new ErrorCode(1_060_203_006, "盘库单明细不存在"); + ErrorCode CHECK_ORDER_INVENTORY_CHANGED = new ErrorCode(1_060_203_007, "盘库单库存已变化,请重新加载库存后再完成"); + + // ========== WMS 库存 1-060-300-000 ========== + ErrorCode INVENTORY_QUANTITY_NOT_ENOUGH = new ErrorCode(1_060_300_000, + "库存不足,商品:{},商品规格:{},仓库:{},当前库存:{},变更数量:{}"); + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/md/WmsMerchantTypeEnum.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/md/WmsMerchantTypeEnum.java new file mode 100644 index 000000000..b6924e4c1 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/md/WmsMerchantTypeEnum.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.wms.enums.md; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * WMS 往来企业类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum WmsMerchantTypeEnum implements ArrayValuable { + + CUSTOMER(1, "客户"), + SUPPLIER(2, "供应商"), + CUSTOMER_SUPPLIER(3, "客户/供应商"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(WmsMerchantTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderStatusEnum.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderStatusEnum.java new file mode 100644 index 000000000..065655bda --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderStatusEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.wms.enums.order; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * WMS 单据状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum WmsOrderStatusEnum implements ArrayValuable { + + PREPARE(0, "草稿"), + FINISHED(4, "已完成"), + CANCELED(5, "已作废"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(WmsOrderStatusEnum::getStatus) + .toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer status; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java new file mode 100644 index 000000000..3f9017d1d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeConstants.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.wms.enums.order; + +/** + * WMS 单据业务类型常量 + * + * 集中管理业务类型枚举的编号,按业务域分段。 + * 各枚举类引用此处常量,避免硬编码数字。(也避免冲突!!!) + * + * @author 芋道源码 + */ +public final class WmsOrderTypeConstants { + + private WmsOrderTypeConstants() {} + + // ========== 入库单类型 [100, 200) ========== + + public static final int RECEIPT_PRODUCTION = 100; // 生产入库:WmsReceiptOrderDO + public static final int RECEIPT_PURCHASE = 101; // 采购入库:WmsReceiptOrderDO + public static final int RECEIPT_RETURN = 102; // 退货入库:WmsReceiptOrderDO + public static final int RECEIPT_GIVE_BACK = 103; // 归还入库:WmsReceiptOrderDO + + // ========== 出库单类型 [200, 300) ========== + + public static final int SHIPMENT_RETURN = 200; // 退货出库:WmsShipmentOrderDO + public static final int SHIPMENT_SALE = 201; // 销售出库:WmsShipmentOrderDO + public static final int SHIPMENT_PRODUCTION = 202; // 生产出库:WmsShipmentOrderDO + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeEnum.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeEnum.java new file mode 100644 index 000000000..701f7137b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsOrderTypeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.wms.enums.order; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * WMS 单据类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum WmsOrderTypeEnum implements ArrayValuable { + + RECEIPT(1, "入库单"), + SHIPMENT(2, "出库单"), + MOVEMENT(3, "移库单"), + CHECK(4, "盘库单"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(WmsOrderTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsReceiptOrderTypeEnum.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsReceiptOrderTypeEnum.java new file mode 100644 index 000000000..1ac21f49b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsReceiptOrderTypeEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.wms.enums.order; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * WMS 入库单类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum WmsReceiptOrderTypeEnum implements ArrayValuable { + + PRODUCTION(WmsOrderTypeConstants.RECEIPT_PRODUCTION, "生产入库"), + PURCHASE(WmsOrderTypeConstants.RECEIPT_PURCHASE, "采购入库"), + RETURN(WmsOrderTypeConstants.RECEIPT_RETURN, "退货入库"), + GIVE_BACK(WmsOrderTypeConstants.RECEIPT_GIVE_BACK, "归还入库"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(WmsReceiptOrderTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsShipmentOrderTypeEnum.java b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsShipmentOrderTypeEnum.java new file mode 100644 index 000000000..79545f12e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-api/src/main/java/cn/iocoder/yudao/module/wms/enums/order/WmsShipmentOrderTypeEnum.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.wms.enums.order; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * WMS 出库单类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum WmsShipmentOrderTypeEnum implements ArrayValuable { + + RETURN(WmsOrderTypeConstants.SHIPMENT_RETURN, "退货出库"), + SALE(WmsOrderTypeConstants.SHIPMENT_SALE, "销售出库"), + PRODUCTION(WmsOrderTypeConstants.SHIPMENT_PRODUCTION, "生产出库"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(WmsShipmentOrderTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/Dockerfile b/yudao-module-wms/yudao-module-wms-server/Dockerfile new file mode 100644 index 000000000..200c60fae --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/Dockerfile @@ -0,0 +1,19 @@ +## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性 +## 感谢复旦核博士的建议!灰子哥,牛皮! +FROM eclipse-temurin:21-jre + +## 创建目录,并使用它作为工作目录 +RUN mkdir -p /yudao-module-wms-server +WORKDIR /yudao-module-wms-server +## 将后端项目的 Jar 文件,复制到镜像中 +COPY ./target/yudao-module-wms-server.jar app.jar + +## 设置 TZ 时区 +## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖 +ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m" + +## 暴露后端项目的 48092 端口 +EXPOSE 48092 + +## 启动后端项目 +CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar app.jar diff --git a/yudao-module-wms/yudao-module-wms-server/pom.xml b/yudao-module-wms/yudao-module-wms-server/pom.xml new file mode 100644 index 000000000..0461e0340 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/pom.xml @@ -0,0 +1,119 @@ + + + + cn.iocoder.cloud + yudao-module-wms + ${revision} + + 4.0.0 + yudao-module-wms-server + + ${project.artifactId} + + wms 模块下,仓库管理系统(Warehouse Management System)。 + 例如说:仓库、物料、库存、入库、出库、移库、盘库等等 + + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-env + + + + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + + cn.iocoder.cloud + yudao-module-wms-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.cloud + yudao-spring-boot-starter-redis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-excel + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-monitor + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + + + diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/WmsServerApplication.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/WmsServerApplication.java new file mode 100644 index 000000000..a5ba71599 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/WmsServerApplication.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.wms; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 项目的启动类 + *

+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * + * @author 芋道源码 + */ +@SpringBootApplication +public class WmsServerApplication { + + public static void main(String[] args) { + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + + SpringApplication.run(WmsServerApplication.class, args); + + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/WmsHomeStatisticsController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/WmsHomeStatisticsController.java new file mode 100644 index 000000000..e9cbbef78 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/WmsHomeStatisticsController.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.wms.controller.admin.home; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeInventorySummaryRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeOrderSummaryRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeOrderTrendRespVO; +import cn.iocoder.yudao.module.wms.service.home.WmsHomeStatisticsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - WMS 首页统计") +@RestController +@RequestMapping("/wms/home-statistics") +@Validated +public class WmsHomeStatisticsController { + + @Resource + private WmsHomeStatisticsService homeStatisticsService; + + @GetMapping("/order-summary") + @Operation(summary = "获得首页单据汇总统计") + @Parameter(name = "warehouseId", description = "仓库编号", example = "1024") + @PreAuthorize("@ss.hasPermission('wms:home:query')") + public CommonResult> getOrderSummary( + @RequestParam(value = "warehouseId", required = false) Long warehouseId) { + return success(homeStatisticsService.getOrderSummary(warehouseId)); + } + + @GetMapping("/order-trend") + @Operation(summary = "获得首页单据趋势") + @Parameter(name = "days", description = "天数", example = "7") + @PreAuthorize("@ss.hasPermission('wms:home:query')") + public CommonResult> getOrderTrend( + @RequestParam(value = "days", defaultValue = "7") @Min(1) @Max(90) Integer days, + @RequestParam(value = "warehouseId", required = false) Long warehouseId) { + return success(homeStatisticsService.getOrderTrend(days, warehouseId)); + } + + @GetMapping("/inventory-summary") + @Operation(summary = "获得首页库存汇总统计") + @Parameter(name = "warehouseId", description = "仓库编号", example = "1024") + @PreAuthorize("@ss.hasPermission('wms:home:query')") + public CommonResult getInventorySummary( + @RequestParam(value = "warehouseId", required = false) Long warehouseId, + @RequestParam(value = "goodsLimit", defaultValue = "5") @Min(1) @Max(20) Integer goodsLimit, + @RequestParam(value = "warehouseLimit", defaultValue = "8") @Min(1) @Max(20) Integer warehouseLimit) { + return success(homeStatisticsService.getInventorySummary(warehouseId, goodsLimit, warehouseLimit)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeInventorySummaryRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeInventorySummaryRespVO.java new file mode 100644 index 000000000..18bb5c3e8 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeInventorySummaryRespVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.wms.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "管理后台 - WMS 首页库存汇总统计 Response VO") +@Data +public class WmsHomeInventorySummaryRespVO { + + @Schema(description = "总库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000.00") + private BigDecimal totalQuantity; + + @Schema(description = "商品库存占比列表") + private List goodsShareList; + + @Schema(description = "仓库库存分布列表") + private List warehouseDistributionList; + + @Schema(description = "管理后台 - WMS 首页商品库存排行 Response VO") + @Data + public static class ItemRank { + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "A4 复印纸") + private String name; + + @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal quantity; + + } + + @Schema(description = "管理后台 - WMS 首页仓库库存排行 Response VO") + @Data + public static class WarehouseRank { + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "仓库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海仓") + private String name; + + @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal quantity; + + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderStatusRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderStatusRespVO.java new file mode 100644 index 000000000..91b0fea11 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderStatusRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.wms.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - WMS 首页单据状态统计 Response VO") +@Data +public class WmsHomeOrderStatusRespVO { + + @Schema(description = "状态值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "单据数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Long count; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderSummaryRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderSummaryRespVO.java new file mode 100644 index 000000000..adb34dede --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderSummaryRespVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.wms.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - WMS 首页单据汇总统计 Response VO") +@Data +public class WmsHomeOrderSummaryRespVO { + + @Schema(description = "单据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "单据总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Long total; + + @Schema(description = "状态分布") + private List statuses; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderTrendRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderTrendRespVO.java new file mode 100644 index 000000000..3ebb477e7 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/home/vo/WmsHomeOrderTrendRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.wms.controller.admin.home.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 首页单据趋势 Response VO") +@Data +public class WmsHomeOrderTrendRespVO { + + @Schema(description = "时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1778169600000") + private LocalDateTime time; + + @Schema(description = "入库单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12") + private Long receiptCount; + + @Schema(description = "出库单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "8") + private Long shipmentCount; + + @Schema(description = "移库单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Long movementCount; + + @Schema(description = "盘库单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Long checkCount; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryController.java new file mode 100644 index 000000000..5142e05f8 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryController.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.wms.controller.admin.inventory; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - WMS 库存统计") +@RestController +@RequestMapping("/wms/inventory") +@Validated +public class WmsInventoryController { + + @Resource + private WmsInventoryService inventoryService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsWarehouseService warehouseService; + + @GetMapping("/page") + @Operation(summary = "获得库存统计分页") + @PreAuthorize("@ss.hasPermission('wms:inventory:query')") + public CommonResult> getInventoryPage(@Valid WmsInventoryPageReqVO pageReqVO) { + PageResult pageResult = inventoryService.getInventoryPage(pageReqVO); + return success(new PageResult<>(buildInventoryRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/list") + @Operation(summary = "获得库存统计列表") + @PreAuthorize("@ss.hasPermission('wms:inventory:query')") + public CommonResult> getInventoryList(@Valid WmsInventoryListReqVO listReqVO) { + List list = inventoryService.getInventoryList(listReqVO); + return success(buildInventoryRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private List buildInventoryRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsInventoryDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsInventoryDO::getWarehouseId)); + return BeanUtils.toBean(list, WmsInventoryRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryHistoryController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryHistoryController.java new file mode 100644 index 000000000..c7530bfaa --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/WmsInventoryHistoryController.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.wms.controller.admin.inventory; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.history.WmsInventoryHistoryPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.history.WmsInventoryHistoryRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryHistoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryHistoryService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - WMS 库存流水") +@RestController +@RequestMapping("/wms/inventory-history") +@Validated +public class WmsInventoryHistoryController { + + @Resource + private WmsInventoryHistoryService inventoryHistoryService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsWarehouseService warehouseService; + + @GetMapping("/page") + @Operation(summary = "获得库存流水分页") + @PreAuthorize("@ss.hasPermission('wms:inventory-history:query')") + public CommonResult> getInventoryHistoryPage( + @Valid WmsInventoryHistoryPageReqVO pageReqVO) { + PageResult pageResult = inventoryHistoryService.getInventoryHistoryPage(pageReqVO); + return success(new PageResult<>(buildInventoryHistoryRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + // ==================== 拼接 VO ==================== + + private List buildInventoryHistoryRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsInventoryHistoryDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsInventoryHistoryDO::getWarehouseId)); + return BeanUtils.toBean(list, WmsInventoryHistoryRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryListReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryListReqVO.java new file mode 100644 index 000000000..e3deb8cb2 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryListReqVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.wms.controller.admin.inventory.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - WMS 库存统计列表 Request VO") +@Data +public class WmsInventoryListReqVO { + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "仓库编号不能为空") + private Long warehouseId; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryPageReqVO.java new file mode 100644 index 000000000..4917a1e62 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryPageReqVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.wms.controller.admin.inventory.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - WMS 库存统计分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsInventoryPageReqVO extends PageParam { + + /** + * 按仓库维度统计 + */ + public static final String TYPE_WAREHOUSE = "warehouse"; + + /** + * 按商品维度统计 + */ + public static final String TYPE_ITEM = "item"; + + @Schema(description = "统计维度", requiredMode = Schema.RequiredMode.REQUIRED, example = "warehouse") + @NotBlank(message = "统计维度不能为空") + private String type; + + @Schema(description = "商品编号", example = "ITEM001") + private String itemCode; + + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + + @Schema(description = "商品 SKU 编号", example = "1024") + private Long skuId; + + @Schema(description = "规格编号", example = "SKU001") + private String skuCode; + + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "仓库编号", example = "2048") + private Long warehouseId; + + @Schema(description = "最小库存数量", example = "0.01") + private BigDecimal minQuantity; + + @Schema(description = "是否只查询正库存", example = "true") + private Boolean onlyPositiveQuantity; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryRespVO.java new file mode 100644 index 000000000..c5c3d34ec --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/WmsInventoryRespVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.wms.controller.admin.inventory.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 库存统计 Response VO") +@Data +public class WmsInventoryRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long itemId; + @Schema(description = "商品编码", example = "ITEM001") + private String itemCode; + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + @Schema(description = "商品单位", example = "箱") + private String unit; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private Long skuId; + @Schema(description = "规格编号", example = "SKU001") + private String skuCode; + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8192") + private Long warehouseId; + @Schema(description = "仓库名称", example = "成品仓") + private String warehouseName; + + @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal quantity; + @Schema(description = "备注", example = "备注") + private String remark; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryPageReqVO.java new file mode 100644 index 000000000..23b42bcd5 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryPageReqVO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.history; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - WMS 库存流水分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsInventoryHistoryPageReqVO extends PageParam { + + @Schema(description = "商品编号", example = "ITEM001") + private String itemCode; + + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + + @Schema(description = "商品 SKU 编号", example = "1024") + private Long skuId; + + @Schema(description = "规格编号", example = "SKU001") + private String skuCode; + + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "仓库编号", example = "2048") + private Long warehouseId; + + @Schema(description = "单据号", example = "RK202605110001") + private String orderNo; + + @Schema(description = "单据类型", example = "1") + private Integer orderType; + + @Schema(description = "操作时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryRespVO.java new file mode 100644 index 000000000..87c87251a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/inventory/vo/history/WmsInventoryHistoryRespVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.history; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 库存流水 Response VO") +@Data +public class WmsInventoryHistoryRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long itemId; + @Schema(description = "商品编码", example = "ITEM001") + private String itemCode; + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + @Schema(description = "商品单位", example = "箱") + private String unit; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private Long skuId; + @Schema(description = "规格编号", example = "SKU001") + private String skuCode; + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8192") + private Long warehouseId; + @Schema(description = "仓库名称", example = "成品仓") + private String warehouseName; + + @Schema(description = "库存变化数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.00") + private BigDecimal quantity; + @Schema(description = "变化前库存数量", example = "90.00") + private BigDecimal beforeQuantity; + @Schema(description = "变化后库存数量", example = "100.00") + private BigDecimal afterQuantity; + @Schema(description = "单价", example = "1000.00") + private BigDecimal price; + @Schema(description = "库存变化金额", example = "10000.00") + private BigDecimal totalPrice; + @Schema(description = "备注", example = "备注") + private String remark; + + @Schema(description = "单据编号", example = "1024") + private Long orderId; + @Schema(description = "单据号", example = "RK202605110001") + private String orderNo; + @Schema(description = "单据类型", example = "1") + private Integer orderType; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemBrandController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemBrandController.java new file mode 100644 index 000000000..6094e1150 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemBrandController.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemBrandDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemBrandService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - WMS 商品品牌") +@RestController +@RequestMapping("/wms/item-brand") +@Validated +public class WmsItemBrandController { + + @Resource + private WmsItemBrandService brandService; + + @PostMapping("/create") + @Operation(summary = "创建商品品牌") + @PreAuthorize("@ss.hasPermission('wms:item-brand:create')") + public CommonResult createItemBrand(@Valid @RequestBody WmsItemBrandSaveReqVO createReqVO) { + return success(brandService.createItemBrand(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新商品品牌") + @PreAuthorize("@ss.hasPermission('wms:item-brand:update')") + public CommonResult updateItemBrand(@Valid @RequestBody WmsItemBrandSaveReqVO updateReqVO) { + brandService.updateItemBrand(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除商品品牌") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:item-brand:delete')") + public CommonResult deleteItemBrand(@RequestParam("id") Long id) { + brandService.deleteItemBrand(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得商品品牌") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:item-brand:query')") + public CommonResult getItemBrand(@RequestParam("id") Long id) { + WmsItemBrandDO brand = brandService.getItemBrand(id); + return success(BeanUtils.toBean(brand, WmsItemBrandRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得商品品牌分页") + @PreAuthorize("@ss.hasPermission('wms:item-brand:query')") + public CommonResult> getItemBrandPage(@Valid WmsItemBrandPageReqVO pageReqVO) { + PageResult pageResult = brandService.getItemBrandPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, WmsItemBrandRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得商品品牌精简列表", description = "主要用于前端下拉") + @PreAuthorize("@ss.hasPermission('wms:item-brand:query')") + public CommonResult> getItemBrandSimpleList() { + List list = brandService.getItemBrandList(); + return success(BeanUtils.toBean(list, WmsItemBrandRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出商品品牌 Excel") + @PreAuthorize("@ss.hasPermission('wms:item-brand:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportItemBrandExcel(@Valid WmsItemBrandPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = brandService.getItemBrandPage(pageReqVO).getList(); + ExcelUtils.write(response, "商品品牌.xls", "数据", WmsItemBrandRespVO.class, + BeanUtils.toBean(list, WmsItemBrandRespVO.class)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemCategoryController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemCategoryController.java new file mode 100644 index 000000000..d30edfa24 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemCategoryController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategoryListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategoryRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategorySaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemCategoryDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemCategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - WMS 商品分类") +@RestController +@RequestMapping("/wms/item-category") +@Validated +public class WmsItemCategoryController { + + @Resource + private WmsItemCategoryService categoryService; + + @PostMapping("/create") + @Operation(summary = "创建商品分类") + @PreAuthorize("@ss.hasPermission('wms:item-category:create')") + public CommonResult createItemCategory(@Valid @RequestBody WmsItemCategorySaveReqVO createReqVO) { + return success(categoryService.createItemCategory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新商品分类") + @PreAuthorize("@ss.hasPermission('wms:item-category:update')") + public CommonResult updateItemCategory(@Valid @RequestBody WmsItemCategorySaveReqVO updateReqVO) { + categoryService.updateItemCategory(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除商品分类") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:item-category:delete')") + public CommonResult deleteItemCategory(@RequestParam("id") Long id) { + categoryService.deleteItemCategory(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得商品分类") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:item-category:query')") + public CommonResult getItemCategory(@RequestParam("id") Long id) { + WmsItemCategoryDO category = categoryService.getItemCategory(id); + return success(BeanUtils.toBean(category, WmsItemCategoryRespVO.class)); + } + + @GetMapping("/list") + @Operation(summary = "获得商品分类列表") + @PreAuthorize("@ss.hasPermission('wms:item-category:query')") + public CommonResult> getItemCategoryList(@Valid WmsItemCategoryListReqVO listReqVO) { + List list = categoryService.getItemCategoryList(listReqVO); + return success(BeanUtils.toBean(list, WmsItemCategoryRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得商品分类精简列表", description = "主要用于前端下拉") + @PreAuthorize("@ss.hasPermission('wms:item-category:query')") + public CommonResult> getItemCategorySimpleList() { + List list = categoryService.getItemCategoryList(new WmsItemCategoryListReqVO()); + return success(BeanUtils.toBean(list, WmsItemCategoryRespVO.class)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemController.java new file mode 100644 index 000000000..86a8592fc --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemController.java @@ -0,0 +1,150 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemBrandDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemCategoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemBrandService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemCategoryService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; + +@Tag(name = "管理后台 - WMS 商品") +@RestController +@RequestMapping("/wms/item") +@Validated +public class WmsItemController { + + @Resource + private WmsItemService itemService; + @Resource + private WmsItemCategoryService categoryService; + @Resource + private WmsItemBrandService brandService; + @Resource + private WmsItemSkuService itemSkuService; + + @PostMapping("/create") + @Operation(summary = "创建商品") + @PreAuthorize("@ss.hasPermission('wms:item:create')") + public CommonResult createItem(@Valid @RequestBody WmsItemSaveReqVO createReqVO) { + return success(itemService.createItem(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新商品") + @PreAuthorize("@ss.hasPermission('wms:item:update')") + public CommonResult updateItem(@Valid @RequestBody WmsItemSaveReqVO updateReqVO) { + itemService.updateItem(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除商品") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:item:delete')") + public CommonResult deleteItem(@RequestParam("id") Long id) { + itemService.deleteItem(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得商品") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:item:query')") + public CommonResult getItem(@RequestParam("id") Long id) { + WmsItemDO item = itemService.getItem(id); + return success(buildItemRespVO(item)); + } + + @GetMapping("/page") + @Operation(summary = "获得商品分页") + @PreAuthorize("@ss.hasPermission('wms:item:query')") + public CommonResult> getItemPage(@Valid WmsItemPageReqVO pageReqVO) { + PageResult pageResult = itemService.getItemPage(pageReqVO); + return success(new PageResult<>(buildItemRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得商品精简列表") + @PreAuthorize("@ss.hasPermission('wms:item:query')") + public CommonResult> getItemSimpleList(@Valid WmsItemListReqVO listReqVO) { + List list = itemService.getItemList(listReqVO); + return success(buildItemRespVOList(list)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出商品 Excel") + @PreAuthorize("@ss.hasPermission('wms:item:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportItemExcel(@Valid WmsItemPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = itemService.getItemPage(pageReqVO).getList(); + ExcelUtils.write(response, "商品.xls", "数据", WmsItemRespVO.class, + buildItemRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private WmsItemRespVO buildItemRespVO(WmsItemDO item) { + if (item == null) { + return null; + } + return getFirst(buildItemRespVOList(Collections.singletonList(item))); + } + + private List buildItemRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 查询关联数据 + Map categoryMap = categoryService.getItemCategoryMap( + convertSet(list, WmsItemDO::getCategoryId)); + Map brandMap = brandService.getItemBrandMap( + convertSet(list, WmsItemDO::getBrandId)); + Map> skuMap = itemSkuService.getItemSkuMultiMap( + convertSet(list, WmsItemDO::getId)); + // 拼接 VO + return BeanUtils.toBean(list, WmsItemRespVO.class, vo -> { + MapUtils.findAndThen(categoryMap, vo.getCategoryId(), category -> + vo.setCategoryName(category.getName())); + MapUtils.findAndThen(brandMap, vo.getBrandId(), brand -> + vo.setBrandName(brand.getName())); + vo.setSkus(BeanUtils.toBean(skuMap.getOrDefault(vo.getId(), Collections.emptyList()), + WmsItemSkuRespVO.class)); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemSkuController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemSkuController.java new file mode 100644 index 000000000..5ece6f3ba --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/WmsItemSkuController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemBrandDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemCategoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemBrandService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemCategoryService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * WMS 商品 SKU Controller。 + * + *

SKU 维度的查询入口:弹窗 / 选择器场景使用,按 SKU 一行展开,支持商品 / 品牌 / 分类多表联查筛选。 + * 复用商品权限 {@code wms:item:query},不单独建权限点(lite 也是商品/SKU 共用 {@code wms:item:list})。 + * + *

SKU 的新增 / 修改 / 删除仍随商品弹窗一并维护,不在本 Controller 暴露。 + */ +@Tag(name = "管理后台 - WMS 商品 SKU") +@RestController +@RequestMapping("/wms/item-sku") +@Validated +public class WmsItemSkuController { + + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemCategoryService categoryService; + @Resource + private WmsItemBrandService brandService; + + @GetMapping("/page") + @Operation(summary = "获得商品 SKU 分页", description = "按 SKU 维度分页,支持商品 / 品牌 / 分类多表联查筛选") + @PreAuthorize("@ss.hasPermission('wms:item:query')") + public CommonResult> getItemSkuPage(@Valid WmsItemSkuPageReqVO pageReqVO) { + PageResult pageResult = itemSkuService.getItemSkuPage(pageReqVO); + return success(new PageResult<>(buildItemSkuRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + // ==================== 拼接 VO ==================== + + private List buildItemSkuRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 查询关联数据 + Map itemMap = itemService.getItemMap(convertSet(list, WmsItemSkuDO::getItemId)); + Map categoryMap = categoryService.getItemCategoryMap( + convertSet(itemMap.values(), WmsItemDO::getCategoryId)); + Map brandMap = brandService.getItemBrandMap( + convertSet(itemMap.values(), WmsItemDO::getBrandId)); + // 拼接 VO + return BeanUtils.toBean(list, WmsItemSkuRespVO.class, vo -> MapUtils.findAndThen(itemMap, vo.getItemId(), item -> { + vo.setItemCode(item.getCode()).setItemName(item.getName()).setUnit(item.getUnit()) + .setCategoryId(item.getCategoryId()).setBrandId(item.getBrandId()); + MapUtils.findAndThen(categoryMap, item.getCategoryId(), category -> + vo.setCategoryName(category.getName())); + MapUtils.findAndThen(brandMap, item.getBrandId(), brand -> + vo.setBrandName(brand.getName())); + })); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandPageReqVO.java new file mode 100644 index 000000000..0e2d74525 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandPageReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - WMS 商品品牌分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsItemBrandPageReqVO extends PageParam { + + @Schema(description = "品牌编号", example = "B00000001") + private String code; + + @Schema(description = "品牌名称", example = "华为") + private String name; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandRespVO.java new file mode 100644 index 000000000..bc68efe24 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandRespVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 商品品牌 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsItemBrandRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "B00000001") + @ExcelProperty("品牌编号") + private String code; + + @Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "华为") + @ExcelProperty("品牌名称") + private String name; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandSaveReqVO.java new file mode 100644 index 000000000..7090cf30d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandSaveReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - WMS 商品品牌新增/修改 Request VO") +@Data +public class WmsItemBrandSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "B00000001") + @NotEmpty(message = "品牌编号不能为空") + @Size(max = 20, message = "品牌编号长度不能超过 20 个字符") + private String code; + + @Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "华为") + @NotEmpty(message = "品牌名称不能为空") + @Size(max = 30, message = "品牌名称长度不能超过 30 个字符") + private String name; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryListReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryListReqVO.java new file mode 100644 index 000000000..274cad45f --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryListReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - WMS 商品分类列表 Request VO") +@Data +public class WmsItemCategoryListReqVO { + + @Schema(description = "父级编号", example = "1") + private Long parentId; + + @Schema(description = "分类编号", example = "C00000001") + private String code; + + @Schema(description = "分类名称", example = "原料") + private String name; + + @Schema(description = "状态", example = "0") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryRespVO.java new file mode 100644 index 000000000..d629b989a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategoryRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 商品分类 Response VO") +@Data +public class WmsItemCategoryRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Long parentId; + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "C00000001") + private String code; + + @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "原料") + private String name; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategorySaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategorySaveReqVO.java new file mode 100644 index 000000000..8d0df2254 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/category/WmsItemCategorySaveReqVO.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - WMS 商品分类新增/修改 Request VO") +@Data +public class WmsItemCategorySaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "父级编号不能为空") + private Long parentId; + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "C00000001") + @NotEmpty(message = "分类编号不能为空") + @Size(max = 20, message = "分类编号长度不能超过 20 个字符") + private String code; + + @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "原料") + @NotEmpty(message = "分类名称不能为空") + @Size(max = 30, message = "分类名称长度不能超过 30 个字符") + private String name; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "排序不能为空") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemListReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemListReqVO.java new file mode 100644 index 000000000..8dde808fe --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemListReqVO.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - WMS 商品列表 Request VO") +@Data +public class WmsItemListReqVO { + + @Schema(description = "商品编号", example = "ITEM001") + private String code; + + @Schema(description = "商品名称", example = "华为 nova flip") + private String name; + + @Schema(description = "商品分类编号", example = "1024") + private Long categoryId; + + @Schema(description = "商品品牌编号", example = "2048") + private Long brandId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemPageReqVO.java new file mode 100644 index 000000000..5ae42c396 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemPageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - WMS 商品分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsItemPageReqVO extends PageParam { + + @Schema(description = "商品编号", example = "ITEM001") + private String code; + + @Schema(description = "商品名称", example = "华为 nova flip") + private String name; + + @Schema(description = "商品分类编号", example = "1024") + private Long categoryId; + + @Schema(description = "商品品牌编号", example = "2048") + private Long brandId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemRespVO.java new file mode 100644 index 000000000..d4e74167c --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemRespVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 商品 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsItemRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "商品编号", example = "ITEM001") + @ExcelProperty("商品编号") + private String code; + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "华为 nova flip") + @ExcelProperty("商品名称") + private String name; + + @Schema(description = "商品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long categoryId; + + @Schema(description = "商品分类名称", example = "手机") + @ExcelProperty("商品分类") + private String categoryName; + + @Schema(description = "单位", example = "台") + @ExcelProperty("单位") + private String unit; + + @Schema(description = "商品品牌编号", example = "1") + private Long brandId; + + @Schema(description = "商品品牌名称", example = "华为") + @ExcelProperty("商品品牌") + private String brandName; + + @Schema(description = "备注", example = "备注") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "规格列表") + private List skus; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemSaveReqVO.java new file mode 100644 index 000000000..6b8cadf1e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemSaveReqVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item; + +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - WMS 商品创建/更新 Request VO") +@Data +public class WmsItemSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001") + @NotBlank(message = "商品编号不能为空") + @Size(max = 20, message = "商品编号长度不能超过 20 个字符") + private String code; + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "华为 nova flip") + @NotBlank(message = "商品名称不能为空") + @Size(max = 60, message = "商品名称长度不能超过 60 个字符") + private String name; + + @Schema(description = "商品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品分类不能为空") + private Long categoryId; + + @Schema(description = "单位", example = "台") + @Size(max = 20, message = "单位长度不能超过 20 个字符") + private String unit; + + @Schema(description = "商品品牌编号", example = "1") + private Long brandId; + + @Schema(description = "备注", example = "备注") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + + @Schema(description = "规格列表") + @Valid + @NotEmpty(message = "至少包含一个商品规格") + private List skus; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuPageReqVO.java new file mode 100644 index 000000000..c091ea84e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuPageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - WMS 商品 SKU 分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsItemSkuPageReqVO extends PageParam { + + @Schema(description = "商品编号", example = "I00000001") + private String itemCode; + + @Schema(description = "商品名称", example = "华为 nova flip") + private String itemName; + + @Schema(description = "商品分类编号", example = "1") + private Long categoryId; + + @Schema(description = "商品品牌编号", example = "1") + private Long brandId; + + @Schema(description = "规格编号", example = "S00000001") + private String code; + + @Schema(description = "规格名称", example = "黑色") + private String name; + + @Schema(description = "条码", example = "12345678") + private String barCode; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuRespVO.java new file mode 100644 index 000000000..8bd57aa6a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuRespVO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 商品 SKU Response VO") +@Data +public class WmsItemSkuRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "规格名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "黑色") + private String name; + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long itemId; + + @Schema(description = "商品编码", example = "ITEM001") + private String itemCode; + + @Schema(description = "商品名称", example = "华为 nova flip") + private String itemName; + + @Schema(description = "商品分类编号", example = "1") + private Long categoryId; + + @Schema(description = "商品分类名称", example = "手机") + private String categoryName; + + @Schema(description = "单位", example = "台") + private String unit; + + @Schema(description = "商品品牌编号", example = "1") + private Long brandId; + + @Schema(description = "商品品牌名称", example = "华为") + private String brandName; + + @Schema(description = "条码", example = "690000000001") + private String barCode; + + @Schema(description = "规格编号", example = "SKU001") + private String code; + + @Schema(description = "长,单位 cm", example = "10.0") + private BigDecimal length; + + @Schema(description = "宽,单位 cm", example = "8.0") + private BigDecimal width; + + @Schema(description = "高,单位 cm", example = "1.0") + private BigDecimal height; + + @Schema(description = "毛重,单位 kg", example = "1.000") + private BigDecimal grossWeight; + + @Schema(description = "净重,单位 kg", example = "0.900") + private BigDecimal netWeight; + + @Schema(description = "成本价,单位元", example = "5000.00") + private BigDecimal costPrice; + + @Schema(description = "销售价,单位元", example = "5288.00") + private BigDecimal sellingPrice; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuSaveReqVO.java new file mode 100644 index 000000000..1a79dcbdc --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/sku/WmsItemSkuSaveReqVO.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - WMS 商品 SKU 创建/更新 Request VO") +@Data +public class WmsItemSkuSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "规格名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "黑色") + @NotBlank(message = "规格名称不能为空") + @Size(max = 255, message = "规格名称长度不能超过 255 个字符") + private String name; + + @Schema(description = "商品编号", example = "1") + private Long itemId; + + @Schema(description = "条码", example = "690000000001") + @Size(max = 64, message = "条码长度不能超过 64 个字符") + private String barCode; + + @Schema(description = "规格编号", example = "SKU001") + @Size(max = 64, message = "规格编号长度不能超过 64 个字符") + private String code; + + @Schema(description = "长,单位 cm", example = "10.0") + @DecimalMin(value = "0", message = "长不能小于 0") + private BigDecimal length; + + @Schema(description = "宽,单位 cm", example = "8.0") + @DecimalMin(value = "0", message = "宽不能小于 0") + private BigDecimal width; + + @Schema(description = "高,单位 cm", example = "1.0") + @DecimalMin(value = "0", message = "高不能小于 0") + private BigDecimal height; + + @Schema(description = "毛重,单位 kg", example = "1.000") + @DecimalMin(value = "0", message = "毛重不能小于 0") + private BigDecimal grossWeight; + + @Schema(description = "净重,单位 kg", example = "0.900") + @DecimalMin(value = "0", message = "净重不能小于 0") + private BigDecimal netWeight; + + @Schema(description = "成本价,单位元", example = "5000.00") + @DecimalMin(value = "0", message = "成本价不能小于 0") + private BigDecimal costPrice; + + @Schema(description = "销售价,单位元", example = "5288.00") + @DecimalMin(value = "0", message = "销售价不能小于 0") + private BigDecimal sellingPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/WmsMerchantController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/WmsMerchantController.java new file mode 100644 index 000000000..897f6351c --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/WmsMerchantController.java @@ -0,0 +1,101 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.merchant; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import cn.iocoder.yudao.module.wms.service.md.merchant.WmsMerchantService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - WMS 往来企业") +@RestController +@RequestMapping("/wms/merchant") +@Validated +public class WmsMerchantController { + + @Resource + private WmsMerchantService merchantService; + + @PostMapping("/create") + @Operation(summary = "创建往来企业") + @PreAuthorize("@ss.hasPermission('wms:merchant:create')") + public CommonResult createMerchant(@Valid @RequestBody WmsMerchantSaveReqVO createReqVO) { + return success(merchantService.createMerchant(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新往来企业") + @PreAuthorize("@ss.hasPermission('wms:merchant:update')") + public CommonResult updateMerchant(@Valid @RequestBody WmsMerchantSaveReqVO updateReqVO) { + merchantService.updateMerchant(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除往来企业") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:merchant:delete')") + public CommonResult deleteMerchant(@RequestParam("id") Long id) { + merchantService.deleteMerchant(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得往来企业") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:merchant:query')") + public CommonResult getMerchant(@RequestParam("id") Long id) { + WmsMerchantDO merchant = merchantService.getMerchant(id); + return success(BeanUtils.toBean(merchant, WmsMerchantRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得往来企业分页") + @PreAuthorize("@ss.hasPermission('wms:merchant:query')") + public CommonResult> getMerchantPage(@Valid WmsMerchantPageReqVO pageReqVO) { + PageResult pageResult = merchantService.getMerchantPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, WmsMerchantRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得往来企业精简列表", description = "主要用于前端下拉") + @PreAuthorize("@ss.hasPermission('wms:merchant:query')") + public CommonResult> getMerchantSimpleList(@Valid WmsMerchantListReqVO listReqVO) { + List list = merchantService.getMerchantList(listReqVO); + return success(BeanUtils.toBean(list, WmsMerchantRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出往来企业 Excel") + @PreAuthorize("@ss.hasPermission('wms:merchant:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportMerchantExcel(@Valid WmsMerchantPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = merchantService.getMerchantPage(pageReqVO).getList(); + ExcelUtils.write(response, "往来企业.xls", "数据", WmsMerchantRespVO.class, + BeanUtils.toBean(list, WmsMerchantRespVO.class)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantListReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantListReqVO.java new file mode 100644 index 000000000..287a1bcc7 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantListReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.enums.md.WmsMerchantTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - WMS 往来企业列表 Request VO") +@Data +public class WmsMerchantListReqVO { + + @Schema(description = "往来企业编号", example = "MER001") + private String code; + + @Schema(description = "往来企业名称", example = "华为") + private String name; + + @Schema(description = "往来企业类型", example = "2") + @InEnum(value = WmsMerchantTypeEnum.class, message = "往来企业类型必须是 {value}") + private Integer type; + + @Schema(description = "往来企业类型数组", example = "2,3") + @InEnum(value = WmsMerchantTypeEnum.class, message = "往来企业类型必须是 {value}") + private List types; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantPageReqVO.java new file mode 100644 index 000000000..eb71ab00b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantPageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.enums.md.WmsMerchantTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - WMS 往来企业分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsMerchantPageReqVO extends PageParam { + + @Schema(description = "往来企业编号", example = "MER001") + private String code; + + @Schema(description = "往来企业名称", example = "华为") + private String name; + + @Schema(description = "往来企业类型", example = "2") + @InEnum(value = WmsMerchantTypeEnum.class, message = "往来企业类型必须是 {value}") + private Integer type; + + @Schema(description = "往来企业类型数组", example = "2,3") + @InEnum(value = WmsMerchantTypeEnum.class, message = "往来企业类型必须是 {value}") + private List types; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantRespVO.java new file mode 100644 index 000000000..c88c8765e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantRespVO.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 往来企业 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsMerchantRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "往来企业编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "MER001") + @ExcelProperty("往来企业编号") + private String code; + + @Schema(description = "往来企业名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "华为") + @ExcelProperty("往来企业名称") + private String name; + + @Schema(description = "往来企业类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty(value = "往来企业类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.MERCHANT_TYPE) + private Integer type; + + @Schema(description = "级别", example = "A") + @ExcelProperty("级别") + private String level; + + @Schema(description = "开户行", example = "招商银行") + private String bankName; + + @Schema(description = "银行账户", example = "6225888888888888") + private String bankAccount; + + @Schema(description = "地址", example = "深圳市南山区") + private String address; + + @Schema(description = "手机号", example = "15601691300") + private String mobile; + + @Schema(description = "座机号", example = "0755-88888888") + private String telephone; + + @Schema(description = "联系人", example = "王五") + @ExcelProperty("联系人") + private String contact; + + @Schema(description = "Email", example = "wms@example.com") + private String email; + + @Schema(description = "备注", example = "备注") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantSaveReqVO.java new file mode 100644 index 000000000..5803a1d32 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantSaveReqVO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.enums.md.WmsMerchantTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - WMS 往来企业创建/更新 Request VO") +@Data +public class WmsMerchantSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "往来企业编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "MER001") + @NotBlank(message = "往来企业编号不能为空") + @Size(max = 20, message = "往来企业编号长度不能超过 20 个字符") + private String code; + + @Schema(description = "往来企业名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "华为") + @NotBlank(message = "往来企业名称不能为空") + @Size(max = 60, message = "往来企业名称长度不能超过 60 个字符") + private String name; + + @Schema(description = "往来企业类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "往来企业类型不能为空") + @InEnum(value = WmsMerchantTypeEnum.class, message = "往来企业类型必须是 {value}") + private Integer type; + + @Schema(description = "级别", example = "A") + @Size(max = 10, message = "级别长度不能超过 10 个字符") + private String level; + + @Schema(description = "开户行", example = "招商银行") + @Size(max = 255, message = "开户行长度不能超过 255 个字符") + private String bankName; + + @Schema(description = "银行账户", example = "6225888888888888") + @Size(max = 40, message = "银行账户长度不能超过 40 个字符") + private String bankAccount; + + @Schema(description = "地址", example = "深圳市南山区") + @Size(max = 200, message = "地址长度不能超过 200 个字符") + private String address; + + @Schema(description = "手机号", example = "15601691300") + @Size(max = 13, message = "手机号长度不能超过 13 个字符") + private String mobile; + + @Schema(description = "座机号", example = "0755-88888888") + @Size(max = 13, message = "座机号长度不能超过 13 个字符") + private String telephone; + + @Schema(description = "联系人", example = "王五") + @Size(max = 30, message = "联系人长度不能超过 30 个字符") + private String contact; + + @Schema(description = "Email", example = "wms@example.com") + @Size(max = 50, message = "Email 长度不能超过 50 个字符") + private String email; + + @Schema(description = "备注", example = "备注") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/WmsWarehouseController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/WmsWarehouseController.java new file mode 100644 index 000000000..fcb198137 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/WmsWarehouseController.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.warehouse; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehousePageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehouseRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehouseSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - WMS 仓库") +@RestController +@RequestMapping("/wms/warehouse") +@Validated +public class WmsWarehouseController { + + @Resource + private WmsWarehouseService warehouseService; + + @PostMapping("/create") + @Operation(summary = "创建仓库") + @PreAuthorize("@ss.hasPermission('wms:warehouse:create')") + public CommonResult createWarehouse(@Valid @RequestBody WmsWarehouseSaveReqVO createReqVO) { + return success(warehouseService.createWarehouse(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新仓库") + @PreAuthorize("@ss.hasPermission('wms:warehouse:update')") + public CommonResult updateWarehouse(@Valid @RequestBody WmsWarehouseSaveReqVO updateReqVO) { + warehouseService.updateWarehouse(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除仓库") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:warehouse:delete')") + public CommonResult deleteWarehouse(@RequestParam("id") Long id) { + warehouseService.deleteWarehouse(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得仓库") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:warehouse:query')") + public CommonResult getWarehouse(@RequestParam("id") Long id) { + WmsWarehouseDO warehouse = warehouseService.getWarehouse(id); + return success(BeanUtils.toBean(warehouse, WmsWarehouseRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得仓库分页") + @PreAuthorize("@ss.hasPermission('wms:warehouse:query')") + public CommonResult> getWarehousePage(@Valid WmsWarehousePageReqVO pageReqVO) { + PageResult pageResult = warehouseService.getWarehousePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, WmsWarehouseRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得仓库精简列表", description = "主要用于前端下拉") + @PreAuthorize("@ss.hasPermission('wms:warehouse:query')") + public CommonResult> getWarehouseSimpleList() { + List list = warehouseService.getWarehouseList(); + return success(BeanUtils.toBean(list, WmsWarehouseRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出仓库 Excel") + @PreAuthorize("@ss.hasPermission('wms:warehouse:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportWarehouseExcel(@Valid WmsWarehousePageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = warehouseService.getWarehousePage(pageReqVO).getList(); + ExcelUtils.write(response, "仓库.xls", "数据", WmsWarehouseRespVO.class, + BeanUtils.toBean(list, WmsWarehouseRespVO.class)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehousePageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehousePageReqVO.java new file mode 100644 index 000000000..43fcf5ca6 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehousePageReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - WMS 仓库分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsWarehousePageReqVO extends PageParam { + + @Schema(description = "仓库编号", example = "WH001") + private String code; + + @Schema(description = "仓库名称", example = "原料仓") + private String name; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseRespVO.java new file mode 100644 index 000000000..67c16bf95 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseRespVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 仓库 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsWarehouseRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @ExcelProperty("编号") + private Long id; + + @Schema(description = "仓库编号", example = "WH001") + @ExcelProperty("仓库编号") + private String code; + + @Schema(description = "仓库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "原料仓") + @ExcelProperty("仓库名称") + private String name; + + @Schema(description = "备注", example = "备注") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("排序") + private Integer sort; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseSaveReqVO.java new file mode 100644 index 000000000..cb2da871b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseSaveReqVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - WMS 仓库新增/修改 Request VO") +@Data +public class WmsWarehouseSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "WH001") + @NotEmpty(message = "仓库编号不能为空") + @Size(max = 20, message = "仓库编号长度不能超过 20 个字符") + private String code; + + @Schema(description = "仓库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "原料仓") + @NotEmpty(message = "仓库名称不能为空") + private String name; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "排序不能为空") + private Integer sort; + + @Schema(description = "备注", example = "备注") + private String remark; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderController.java new file mode 100644 index 000000000..9c0cd0944 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderController.java @@ -0,0 +1,198 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.check; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - WMS 盘库单") +@RestController +@RequestMapping("/wms/check-order") +@Validated +public class WmsCheckOrderController { + + @Resource + private WmsCheckOrderService checkOrderService; + @Resource + private WmsCheckOrderDetailService checkOrderDetailService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建盘库单") + @PreAuthorize("@ss.hasPermission('wms:check-order:create')") + public CommonResult createCheckOrder(@Valid @RequestBody WmsCheckOrderSaveReqVO createReqVO) { + return success(checkOrderService.createCheckOrder(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新盘库单") + @PreAuthorize("@ss.hasPermission('wms:check-order:update')") + public CommonResult updateCheckOrder(@Valid @RequestBody WmsCheckOrderSaveReqVO updateReqVO) { + checkOrderService.updateCheckOrder(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除盘库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:check-order:delete')") + public CommonResult deleteCheckOrder(@RequestParam("id") Long id) { + checkOrderService.deleteCheckOrder(id); + return success(true); + } + + @PutMapping("/complete") + @Operation(summary = "完成盘库") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:check-order:complete')") + public CommonResult completeCheckOrder(@RequestParam("id") Long id) { + checkOrderService.completeCheckOrder(id); + return success(true); + } + + @PutMapping("/cancel") + @Operation(summary = "作废盘库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:check-order:cancel')") + public CommonResult cancelCheckOrder(@RequestParam("id") Long id) { + checkOrderService.cancelCheckOrder(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得盘库单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:check-order:query')") + public CommonResult getCheckOrder(@RequestParam("id") Long id) { + WmsCheckOrderDO order = checkOrderService.getCheckOrder(id); + if (order == null) { + return success(null); + } + // 获得盘库单的明细列表 + List detailList = checkOrderDetailService.getCheckOrderDetailList(id); + // 拼接结果返回 + WmsCheckOrderRespVO respVO = buildCheckOrderRespVO(order) + .setDetails(buildCheckOrderDetailRespVOList(detailList)); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得盘库单分页") + @PreAuthorize("@ss.hasPermission('wms:check-order:query')") + public CommonResult> getCheckOrderPage(@Valid WmsCheckOrderPageReqVO pageReqVO) { + PageResult pageResult = checkOrderService.getCheckOrderPage(pageReqVO); + return success(new PageResult<>(buildCheckOrderRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出盘库单 Excel") + @PreAuthorize("@ss.hasPermission('wms:check-order:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportCheckOrderExcel(@Valid WmsCheckOrderPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = checkOrderService.getCheckOrderPage(pageReqVO).getList(); + ExcelUtils.write(response, "盘库单.xls", "数据", WmsCheckOrderRespVO.class, + buildCheckOrderRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private WmsCheckOrderRespVO buildCheckOrderRespVO(WmsCheckOrderDO order) { + if (order == null) { + return null; + } + List list = buildCheckOrderRespVOList(Collections.singletonList(order)); + return CollUtil.getFirst(list); + } + + private List buildCheckOrderRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的仓库、用户等数据 + Map warehouseMap = warehouseService.getWarehouseMap(convertSet(list, WmsCheckOrderDO::getWarehouseId)); + Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(list, + order -> Stream.of(parseUserId(order.getCreator()), parseUserId(order.getUpdater())))); + // 拼接数据 + return BeanUtils.toBean(list, WmsCheckOrderRespVO.class, vo -> { + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + MapUtils.findAndThen(userMap, parseUserId(vo.getCreator()), user -> vo.setCreatorName(user.getNickname())); + MapUtils.findAndThen(userMap, parseUserId(vo.getUpdater()), user -> vo.setUpdaterName(user.getNickname())); + }); + } + + private Long parseUserId(String userId) { + return NumberUtil.parseLong(userId, null); + } + + private List buildCheckOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的商品、SKU、仓库等数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsCheckOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsCheckOrderDetailDO::getWarehouseId)); + // 拼接数据 + return BeanUtils.toBean(list, WmsCheckOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderDetailController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderDetailController.java new file mode 100644 index 000000000..25198254f --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/WmsCheckOrderDetailController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.check; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderDetailService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - WMS 盘库单明细") +@RestController +@RequestMapping("/wms/check-order-detail") +@Validated +public class WmsCheckOrderDetailController { + + @Resource + private WmsCheckOrderDetailService checkOrderDetailService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsWarehouseService warehouseService; + + @GetMapping("/list-by-order-id") + @Operation(summary = "获得盘库单明细列表") + @Parameter(name = "orderId", description = "盘库单编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:check-order:query')") + public CommonResult> getCheckOrderDetailListByOrderId( + @RequestParam("orderId") Long orderId) { + List list = checkOrderDetailService.getCheckOrderDetailList(orderId); + return success(buildCheckOrderDetailRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private List buildCheckOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 查询关联数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsCheckOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsCheckOrderDetailDO::getWarehouseId)); + // 拼接数据 + return BeanUtils.toBean(list, WmsCheckOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailRespVO.java new file mode 100644 index 000000000..399caa3f9 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailRespVO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 盘库单明细 Response VO") +@Data +public class WmsCheckOrderDetailRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "盘库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + @Schema(description = "商品编号", example = "2048") + private Long itemId; + + @Schema(description = "商品编号", example = "SPU-APPLE") + private String itemCode; + + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + + @Schema(description = "商品单位", example = "箱") + private String unit; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long skuId; + + @Schema(description = "规格编号", example = "SKU-APPLE-10KG") + private String skuCode; + + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long warehouseId; + + @Schema(description = "仓库名称", example = "北京仓") + private String warehouseName; + + @Schema(description = "库存编号", example = "1024") + private Long inventoryId; + + @Schema(description = "入库时间") + private LocalDateTime receiptTime; + + @Schema(description = "账面数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal quantity; + + @Schema(description = "实盘数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "98.00") + private BigDecimal checkQuantity; + + @Schema(description = "单价", example = "1000.00") + private BigDecimal price; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailSaveReqVO.java new file mode 100644 index 000000000..709ddba9d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/detail/WmsCheckOrderDetailSaveReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 盘库单明细保存 Request VO") +@Data +public class WmsCheckOrderDetailSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "SKU 不能为空") + private Long skuId; + + @Schema(description = "库存编号", example = "1024") + private Long inventoryId; + + @Schema(description = "入库时间") + private LocalDateTime receiptTime; + + @Schema(description = "账面数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @NotNull(message = "账面数量不能为空") + @DecimalMin(value = "0", message = "账面数量不能小于 0") + private BigDecimal quantity; + + @Schema(description = "实盘数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "98.00") + @NotNull(message = "实盘数量不能为空") + @DecimalMin(value = "0", message = "实盘数量不能小于 0") + private BigDecimal checkQuantity; + + @Schema(description = "单价", example = "1000.00") + @DecimalMin(value = "0", message = "单价不能小于 0") + private BigDecimal price; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderPageReqVO.java new file mode 100644 index 000000000..2182fc69b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderPageReqVO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - WMS 盘库单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsCheckOrderPageReqVO extends PageParam { + + @Schema(description = "盘库单号", example = "PK202605110001") + private String no; + + @Schema(description = "单据状态", example = "0") + @InEnum(WmsOrderStatusEnum.class) + private Integer status; + + @Schema(description = "仓库编号", example = "1024") + private Long warehouseId; + + @Schema(description = "单据日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] orderTime; + + @Schema(description = "最小盈亏数量", example = "-10.00") + private BigDecimal totalQuantityMin; + + @Schema(description = "最大盈亏数量", example = "100.00") + private BigDecimal totalQuantityMax; + + @Schema(description = "最小总金额", example = "1.00") + private BigDecimal totalPriceMin; + + @Schema(description = "最大总金额", example = "1000.00") + private BigDecimal totalPriceMax; + + @Schema(description = "最小实际金额", example = "1.00") + private BigDecimal actualPriceMin; + + @Schema(description = "最大实际金额", example = "1000.00") + private BigDecimal actualPriceMax; + + @Schema(description = "创建用户", example = "1") + private String creator; + + @Schema(description = "更新用户", example = "1") + private String updater; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "更新时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] updateTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderRespVO.java new file mode 100644 index 000000000..962de8235 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderRespVO.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 盘库单 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsCheckOrderRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "盘库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "PK202605110001") + @ExcelProperty("盘库单号") + private String no; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("单据日期") + private LocalDateTime orderTime; + + @Schema(description = "盘库状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @ExcelProperty(value = "盘库状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ORDER_STATUS) + private Integer status; + + @Schema(description = "备注", example = "备注") + private String remark; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long warehouseId; + + @Schema(description = "仓库名称", example = "北京仓") + @ExcelProperty("仓库") + private String warehouseName; + + @Schema(description = "盈亏数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @ExcelProperty("盈亏数量") + private BigDecimal totalQuantity; + + @Schema(description = "总金额(账面金额)", example = "1000.00") + @ExcelProperty("总金额") + private BigDecimal totalPrice; + + @Schema(description = "实际金额", example = "980.00") + @ExcelProperty("实际金额") + private BigDecimal actualPrice; + + @Schema(description = "盘库明细") + private List details; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "创建者", example = "1") + private String creator; + @Schema(description = "创建者名称", example = "芋道") + private String creatorName; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "更新者", example = "1") + private String updater; + @Schema(description = "更新者名称", example = "芋道") + private String updaterName; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderSaveReqVO.java new file mode 100644 index 000000000..21660ce87 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderSaveReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order; + +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 盘库单保存 Request VO") +@Data +public class WmsCheckOrderSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "盘库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "PK202605110001") + @NotBlank(message = "盘库单号不能为空") + @Size(max = 64, message = "盘库单号长度不能超过 64 个字符") + private String no; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "单据日期不能为空") + private LocalDateTime orderTime; + + @Schema(description = "备注", example = "备注") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "仓库不能为空") + private Long warehouseId; + + @Schema(description = "盘库明细") + @Valid + private List details; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderController.java new file mode 100644 index 000000000..d993392b9 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderController.java @@ -0,0 +1,206 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.movement; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - WMS 移库单") +@RestController +@RequestMapping("/wms/movement-order") +@Validated +public class WmsMovementOrderController { + + @Resource + private WmsMovementOrderService movementOrderService; + @Resource + private WmsMovementOrderDetailService movementOrderDetailService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建移库单") + @PreAuthorize("@ss.hasPermission('wms:movement-order:create')") + public CommonResult createMovementOrder(@Valid @RequestBody WmsMovementOrderSaveReqVO createReqVO) { + return success(movementOrderService.createMovementOrder(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新移库单") + @PreAuthorize("@ss.hasPermission('wms:movement-order:update')") + public CommonResult updateMovementOrder(@Valid @RequestBody WmsMovementOrderSaveReqVO updateReqVO) { + movementOrderService.updateMovementOrder(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除移库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:movement-order:delete')") + public CommonResult deleteMovementOrder(@RequestParam("id") Long id) { + movementOrderService.deleteMovementOrder(id); + return success(true); + } + + @PutMapping("/complete") + @Operation(summary = "完成移库") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:movement-order:complete')") + public CommonResult completeMovementOrder(@RequestParam("id") Long id) { + movementOrderService.completeMovementOrder(id); + return success(true); + } + + @PutMapping("/cancel") + @Operation(summary = "作废移库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:movement-order:cancel')") + public CommonResult cancelMovementOrder(@RequestParam("id") Long id) { + movementOrderService.cancelMovementOrder(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得移库单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:movement-order:query')") + public CommonResult getMovementOrder(@RequestParam("id") Long id) { + WmsMovementOrderDO order = movementOrderService.getMovementOrder(id); + if (order == null) { + return success(null); + } + // 获得移库单的明细列表 + List detailList = movementOrderDetailService.getMovementOrderDetailList(id); + // 拼接结果返回 + WmsMovementOrderRespVO respVO = buildMovementOrderRespVO(order) + .setDetails(buildMovementOrderDetailRespVOList(detailList)); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得移库单分页") + @PreAuthorize("@ss.hasPermission('wms:movement-order:query')") + public CommonResult> getMovementOrderPage( + @Valid WmsMovementOrderPageReqVO pageReqVO) { + PageResult pageResult = movementOrderService.getMovementOrderPage(pageReqVO); + return success(new PageResult<>(buildMovementOrderRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出移库单 Excel") + @PreAuthorize("@ss.hasPermission('wms:movement-order:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportMovementOrderExcel(@Valid WmsMovementOrderPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = movementOrderService.getMovementOrderPage(pageReqVO).getList(); + ExcelUtils.write(response, "移库单.xls", "数据", WmsMovementOrderRespVO.class, + buildMovementOrderRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private WmsMovementOrderRespVO buildMovementOrderRespVO(WmsMovementOrderDO order) { + if (order == null) { + return null; + } + List list = buildMovementOrderRespVOList(Collections.singletonList(order)); + return CollUtil.getFirst(list); + } + + private List buildMovementOrderRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的仓库、用户等数据 + Map warehouseMap = warehouseService.getWarehouseMap(convertSetByFlatMap(list, + order -> Stream.of(order.getSourceWarehouseId(), order.getTargetWarehouseId()))); + Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(list, + order -> Stream.of(parseUserId(order.getCreator()), parseUserId(order.getUpdater())))); + // 拼接数据 + return BeanUtils.toBean(list, WmsMovementOrderRespVO.class, vo -> { + MapUtils.findAndThen(warehouseMap, vo.getSourceWarehouseId(), + warehouse -> vo.setSourceWarehouseName(warehouse.getName())); + MapUtils.findAndThen(warehouseMap, vo.getTargetWarehouseId(), + warehouse -> vo.setTargetWarehouseName(warehouse.getName())); + MapUtils.findAndThen(userMap, parseUserId(vo.getCreator()), user -> vo.setCreatorName(user.getNickname())); + MapUtils.findAndThen(userMap, parseUserId(vo.getUpdater()), user -> vo.setUpdaterName(user.getNickname())); + }); + } + + private Long parseUserId(String userId) { + return NumberUtil.parseLong(userId, null); + } + + private List buildMovementOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的商品、SKU、仓库等数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsMovementOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap(convertSetByFlatMap(list, + detail -> Stream.of(detail.getSourceWarehouseId(), detail.getTargetWarehouseId()))); + // 拼接数据 + return BeanUtils.toBean(list, WmsMovementOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getSourceWarehouseId(), + warehouse -> vo.setSourceWarehouseName(warehouse.getName())); + MapUtils.findAndThen(warehouseMap, vo.getTargetWarehouseId(), + warehouse -> vo.setTargetWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderDetailController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderDetailController.java new file mode 100644 index 000000000..092a367fd --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/WmsMovementOrderDetailController.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.movement; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderDetailService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - WMS 移库单明细") +@RestController +@RequestMapping("/wms/movement-order-detail") +@Validated +public class WmsMovementOrderDetailController { + + @Resource + private WmsMovementOrderDetailService movementOrderDetailService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsWarehouseService warehouseService; + + @GetMapping("/list-by-order-id") + @Operation(summary = "获得移库单明细列表") + @Parameter(name = "orderId", description = "移库单编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:movement-order:query')") + public CommonResult> getMovementOrderDetailListByOrderId( + @RequestParam("orderId") Long orderId) { + List list = movementOrderDetailService.getMovementOrderDetailList(orderId); + return success(buildMovementOrderDetailRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private List buildMovementOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 查询关联数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsMovementOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap(convertSetByFlatMap(list, + detail -> Stream.of(detail.getSourceWarehouseId(), detail.getTargetWarehouseId()))); + // 拼接数据 + return BeanUtils.toBean(list, WmsMovementOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getSourceWarehouseId(), + warehouse -> vo.setSourceWarehouseName(warehouse.getName())); + MapUtils.findAndThen(warehouseMap, vo.getTargetWarehouseId(), + warehouse -> vo.setTargetWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailRespVO.java new file mode 100644 index 000000000..513653460 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailRespVO.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 移库单明细 Response VO") +@Data +public class WmsMovementOrderDetailRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "移库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + @Schema(description = "商品编号", example = "2048") + private Long itemId; + + @Schema(description = "商品编号", example = "SPU-APPLE") + private String itemCode; + + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + + @Schema(description = "商品单位", example = "箱") + private String unit; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long skuId; + + @Schema(description = "规格编号", example = "SKU-APPLE-10KG") + private String skuCode; + + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "来源仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long sourceWarehouseId; + + @Schema(description = "来源仓库名称", example = "北京仓") + private String sourceWarehouseName; + + @Schema(description = "目标仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long targetWarehouseId; + + @Schema(description = "目标仓库名称", example = "上海仓") + private String targetWarehouseName; + + @Schema(description = "移库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal quantity; + + @Schema(description = "单价", example = "100.00") + private BigDecimal price; + + @Schema(description = "行金额", example = "1000.00") + private BigDecimal totalPrice; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailSaveReqVO.java new file mode 100644 index 000000000..28b412edf --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/detail/WmsMovementOrderDetailSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - WMS 移库单明细保存 Request VO") +@Data +public class WmsMovementOrderDetailSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "SKU 不能为空") + private Long skuId; + + @Schema(description = "移库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @NotNull(message = "移库数量不能为空") + @DecimalMin(value = "0", inclusive = false, message = "移库数量必须大于 0") + private BigDecimal quantity; + + @Schema(description = "单价", example = "100.00") + @DecimalMin(value = "0", message = "单价不能小于 0") + private BigDecimal price; + + @Schema(description = "行金额", example = "1000.00") + @DecimalMin(value = "0", message = "行金额不能小于 0") + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderPageReqVO.java new file mode 100644 index 000000000..c59bc113a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderPageReqVO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - WMS 移库单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsMovementOrderPageReqVO extends PageParam { + + @Schema(description = "移库单号", example = "YK202605110001") + private String no; + + @Schema(description = "单据状态", example = "0") + @InEnum(WmsOrderStatusEnum.class) + private Integer status; + + @Schema(description = "来源仓库编号", example = "1024") + private Long sourceWarehouseId; + + @Schema(description = "目标仓库编号", example = "2048") + private Long targetWarehouseId; + + @Schema(description = "单据日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] orderTime; + + @Schema(description = "最小数量", example = "1.00") + private BigDecimal totalQuantityMin; + + @Schema(description = "最大数量", example = "100.00") + private BigDecimal totalQuantityMax; + + @Schema(description = "最小总金额", example = "1.00") + private BigDecimal totalPriceMin; + + @Schema(description = "最大总金额", example = "1000.00") + private BigDecimal totalPriceMax; + + @Schema(description = "创建用户", example = "1") + private String creator; + + @Schema(description = "更新用户", example = "1") + private String updater; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "更新时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] updateTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderRespVO.java new file mode 100644 index 000000000..ecad02e05 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderRespVO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 移库单 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsMovementOrderRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "移库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "YK202605110001") + @ExcelProperty("移库单号") + private String no; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("单据日期") + private LocalDateTime orderTime; + + @Schema(description = "移库状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @ExcelProperty(value = "移库状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ORDER_STATUS) + private Integer status; + + @Schema(description = "备注", example = "备注") + private String remark; + + @Schema(description = "来源仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long sourceWarehouseId; + + @Schema(description = "来源仓库名称", example = "北京仓") + @ExcelProperty("来源仓库") + private String sourceWarehouseName; + + @Schema(description = "目标仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long targetWarehouseId; + + @Schema(description = "目标仓库名称", example = "上海仓") + @ExcelProperty("目标仓库") + private String targetWarehouseName; + + @Schema(description = "移库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @ExcelProperty("移库数量") + private BigDecimal totalQuantity; + + @Schema(description = "总金额", example = "1000.00") + @ExcelProperty("总金额") + private BigDecimal totalPrice; + + @Schema(description = "移库明细") + private List details; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "创建者", example = "1") + private String creator; + @Schema(description = "创建者名称", example = "芋道") + private String creatorName; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "更新者", example = "1") + private String updater; + @Schema(description = "更新者名称", example = "芋道") + private String updaterName; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderSaveReqVO.java new file mode 100644 index 000000000..982d30dff --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderSaveReqVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order; + +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailSaveReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 移库单保存 Request VO") +@Data +public class WmsMovementOrderSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "移库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "YK202605110001") + @NotBlank(message = "移库单号不能为空") + @Size(max = 64, message = "移库单号长度不能超过 64 个字符") + private String no; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "单据日期不能为空") + private LocalDateTime orderTime; + + @Schema(description = "备注", example = "备注") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + + @Schema(description = "来源仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "来源仓库不能为空") + private Long sourceWarehouseId; + + @Schema(description = "目标仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "目标仓库不能为空") + private Long targetWarehouseId; + + @Schema(description = "移库明细") + @Valid + private List details; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderController.java new file mode 100644 index 000000000..c1a2fe6d4 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderController.java @@ -0,0 +1,204 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.receipt; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.merchant.WmsMerchantService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - WMS 入库单") +@RestController +@RequestMapping("/wms/receipt-order") +@Validated +public class WmsReceiptOrderController { + + @Resource + private WmsReceiptOrderService receiptOrderService; + @Resource + private WmsReceiptOrderDetailService receiptOrderDetailService; + @Resource + private WmsMerchantService merchantService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建入库单") + @PreAuthorize("@ss.hasPermission('wms:receipt-order:create')") + public CommonResult createReceiptOrder(@Valid @RequestBody WmsReceiptOrderSaveReqVO createReqVO) { + return success(receiptOrderService.createReceiptOrder(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新入库单") + @PreAuthorize("@ss.hasPermission('wms:receipt-order:update')") + public CommonResult updateReceiptOrder(@Valid @RequestBody WmsReceiptOrderSaveReqVO updateReqVO) { + receiptOrderService.updateReceiptOrder(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除入库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:receipt-order:delete')") + public CommonResult deleteReceiptOrder(@RequestParam("id") Long id) { + receiptOrderService.deleteReceiptOrder(id); + return success(true); + } + + @PutMapping("/complete") + @Operation(summary = "完成入库") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:receipt-order:complete')") + public CommonResult completeReceiptOrder(@RequestParam("id") Long id) { + receiptOrderService.completeReceiptOrder(id); + return success(true); + } + + @PutMapping("/cancel") + @Operation(summary = "作废入库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:receipt-order:cancel')") + public CommonResult cancelReceiptOrder(@RequestParam("id") Long id) { + receiptOrderService.cancelReceiptOrder(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得入库单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:receipt-order:query')") + public CommonResult getReceiptOrder(@RequestParam("id") Long id) { + WmsReceiptOrderDO order = receiptOrderService.getReceiptOrder(id); + if (order == null) { + return success(null); + } + // 获得入库单的明细列表 + List detailList = receiptOrderDetailService.getReceiptOrderDetailList(id); + // 拼接结果返回 + WmsReceiptOrderRespVO respVO = buildReceiptOrderRespVO(order) + .setDetails(buildReceiptOrderDetailRespVOList(detailList)); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得入库单分页") + @PreAuthorize("@ss.hasPermission('wms:receipt-order:query')") + public CommonResult> getReceiptOrderPage(@Valid WmsReceiptOrderPageReqVO pageReqVO) { + PageResult pageResult = receiptOrderService.getReceiptOrderPage(pageReqVO); + return success(new PageResult<>(buildReceiptOrderRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出入库单 Excel") + @PreAuthorize("@ss.hasPermission('wms:receipt-order:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportReceiptOrderExcel(@Valid WmsReceiptOrderPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = receiptOrderService.getReceiptOrderPage(pageReqVO).getList(); + ExcelUtils.write(response, "入库单.xls", "数据", WmsReceiptOrderRespVO.class, + buildReceiptOrderRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private WmsReceiptOrderRespVO buildReceiptOrderRespVO(WmsReceiptOrderDO order) { + if (order == null) { + return null; + } + List list = buildReceiptOrderRespVOList(Collections.singletonList(order)); + return CollUtil.getFirst(list); + } + + private List buildReceiptOrderRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的商户、仓库、用户等数据 + Map merchantMap = merchantService.getMerchantMap(convertSet(list, WmsReceiptOrderDO::getMerchantId)); + Map warehouseMap = warehouseService.getWarehouseMap(convertSet(list, WmsReceiptOrderDO::getWarehouseId)); + Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(list, + order -> Stream.of(parseUserId(order.getCreator()), parseUserId(order.getUpdater())))); + // 拼接数据 + return BeanUtils.toBean(list, WmsReceiptOrderRespVO.class, vo -> { + MapUtils.findAndThen(merchantMap, vo.getMerchantId(), merchant -> vo.setMerchantName(merchant.getName())); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + MapUtils.findAndThen(userMap, parseUserId(vo.getCreator()), user -> vo.setCreatorName(user.getNickname())); + MapUtils.findAndThen(userMap, parseUserId(vo.getUpdater()), user -> vo.setUpdaterName(user.getNickname())); + }); + } + + private Long parseUserId(String userId) { + return NumberUtil.parseLong(userId, null); + } + + private List buildReceiptOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的商品、SKU、仓库等数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsReceiptOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsReceiptOrderDetailDO::getWarehouseId)); + // 拼接数据 + return BeanUtils.toBean(list, WmsReceiptOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderDetailController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderDetailController.java new file mode 100644 index 000000000..d443feb2f --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/WmsReceiptOrderDetailController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.receipt; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderDetailService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - WMS 入库单明细") +@RestController +@RequestMapping("/wms/receipt-order-detail") +@Validated +public class WmsReceiptOrderDetailController { + + @Resource + private WmsReceiptOrderDetailService receiptOrderDetailService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsWarehouseService warehouseService; + + @GetMapping("/list-by-order-id") + @Operation(summary = "获得入库单明细列表") + @Parameter(name = "orderId", description = "入库单编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:receipt-order:query')") + public CommonResult> getReceiptOrderDetailListByOrderId( + @RequestParam("orderId") Long orderId) { + List list = receiptOrderDetailService.getReceiptOrderDetailList(orderId); + return success(buildReceiptOrderDetailRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private List buildReceiptOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 查询关联数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsReceiptOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsReceiptOrderDetailDO::getWarehouseId)); + // 拼接数据 + return BeanUtils.toBean(list, WmsReceiptOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailRespVO.java new file mode 100644 index 000000000..4c72b9324 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailRespVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 入库单明细 Response VO") +@Data +public class WmsReceiptOrderDetailRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "入库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + @Schema(description = "商品编号", example = "2048") + private Long itemId; + + @Schema(description = "商品编号", example = "SPU-APPLE") + private String itemCode; + + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + + @Schema(description = "商品单位", example = "箱") + private String unit; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long skuId; + + @Schema(description = "规格编号", example = "SKU-APPLE-10KG") + private String skuCode; + + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long warehouseId; + + @Schema(description = "仓库名称", example = "北京仓") + private String warehouseName; + + @Schema(description = "入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal quantity; + + @Schema(description = "单价", example = "1000.00") + private BigDecimal price; + + @Schema(description = "行金额", example = "1500.00") + private BigDecimal totalPrice; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailSaveReqVO.java new file mode 100644 index 000000000..a74cc905d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/detail/WmsReceiptOrderDetailSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - WMS 入库单明细保存 Request VO") +@Data +public class WmsReceiptOrderDetailSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "SKU 不能为空") + private Long skuId; + + @Schema(description = "入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @NotNull(message = "入库数量不能为空") + @DecimalMin(value = "0", inclusive = false, message = "入库数量必须大于 0") + private BigDecimal quantity; + + @Schema(description = "单价", example = "1000.00") + @DecimalMin(value = "0", message = "单价不能小于 0") + private BigDecimal price; + + @Schema(description = "行金额", example = "1500.00") + @DecimalMin(value = "0", message = "行金额不能小于 0") + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderPageReqVO.java new file mode 100644 index 000000000..351d15004 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderPageReqVO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsReceiptOrderTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - WMS 入库单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsReceiptOrderPageReqVO extends PageParam { + + @Schema(description = "入库单号", example = "RK202605110001") + private String no; + + @Schema(description = "单据状态", example = "0") + @InEnum(WmsOrderStatusEnum.class) + private Integer status; + + @Schema(description = "仓库编号", example = "1024") + private Long warehouseId; + + @Schema(description = "供应商编号", example = "1024") + private Long merchantId; + + @Schema(description = "单据日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] orderTime; + + @Schema(description = "最小数量", example = "1.00") + private BigDecimal totalQuantityMin; + + @Schema(description = "最大数量", example = "100.00") + private BigDecimal totalQuantityMax; + + @Schema(description = "最小总金额", example = "1.00") + private BigDecimal totalPriceMin; + + @Schema(description = "最大总金额", example = "1000.00") + private BigDecimal totalPriceMax; + + @Schema(description = "入库类型", example = "101") + @InEnum(WmsReceiptOrderTypeEnum.class) + private Integer type; + + @Schema(description = "业务单号", example = "PO202605110001") + private String bizOrderNo; + + @Schema(description = "创建用户", example = "1") + private String creator; + + @Schema(description = "更新用户", example = "1") + private String updater; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "更新时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] updateTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderRespVO.java new file mode 100644 index 000000000..24d3b0449 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderRespVO.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 入库单 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsReceiptOrderRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "入库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "RK202605110001") + @ExcelProperty("入库单号") + private String no; + + @Schema(description = "入库类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "101") + @ExcelProperty(value = "入库类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.RECEIPT_ORDER_TYPE) + private Integer type; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("单据日期") + private LocalDateTime orderTime; + + @Schema(description = "入库状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @ExcelProperty(value = "入库状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ORDER_STATUS) + private Integer status; + + @Schema(description = "业务单号", example = "PO202605110001") + @ExcelProperty("业务单号") + private String bizOrderNo; + + @Schema(description = "供应商编号", example = "1024") + private Long merchantId; + + @Schema(description = "供应商名称", example = "某某公司") + @ExcelProperty("供应商") + private String merchantName; + + @Schema(description = "备注", example = "备注") + private String remark; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long warehouseId; + + @Schema(description = "仓库名称", example = "北京仓") + @ExcelProperty("仓库") + private String warehouseName; + + @Schema(description = "入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @ExcelProperty("入库数量") + private BigDecimal totalQuantity; + + @Schema(description = "总金额", example = "1000.00") + @ExcelProperty("总金额") + private BigDecimal totalPrice; + + @Schema(description = "入库明细") + private List details; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "创建者", example = "1") + private String creator; + @Schema(description = "创建者名称", example = "芋道") + private String creatorName; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "更新者", example = "1") + private String updater; + @Schema(description = "更新者名称", example = "芋道") + private String updaterName; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderSaveReqVO.java new file mode 100644 index 000000000..b8619fc41 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderSaveReqVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.enums.order.WmsReceiptOrderTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 入库单保存 Request VO") +@Data +public class WmsReceiptOrderSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "入库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "RK202605110001") + @NotBlank(message = "入库单号不能为空") + @Size(max = 64, message = "入库单号长度不能超过 64 个字符") + private String no; + + @Schema(description = "入库类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "101") + @NotNull(message = "入库类型不能为空") + @InEnum(WmsReceiptOrderTypeEnum.class) + private Integer type; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "单据日期不能为空") + private LocalDateTime orderTime; + + @Schema(description = "业务单号", example = "PO202605110001") + @Size(max = 64, message = "业务单号长度不能超过 64 个字符") + private String bizOrderNo; + + @Schema(description = "供应商编号", example = "1024") + private Long merchantId; + + @Schema(description = "备注", example = "备注") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "仓库不能为空") + private Long warehouseId; + + @Schema(description = "入库明细") + @Valid + private List details; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderController.java new file mode 100644 index 000000000..65e9153fb --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderController.java @@ -0,0 +1,204 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.shipment; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.merchant.WmsMerchantService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; + +@Tag(name = "管理后台 - WMS 出库单") +@RestController +@RequestMapping("/wms/shipment-order") +@Validated +public class WmsShipmentOrderController { + + @Resource + private WmsShipmentOrderService shipmentOrderService; + @Resource + private WmsShipmentOrderDetailService shipmentOrderDetailService; + @Resource + private WmsMerchantService merchantService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + + @Resource + private AdminUserApi adminUserApi; + + @PostMapping("/create") + @Operation(summary = "创建出库单") + @PreAuthorize("@ss.hasPermission('wms:shipment-order:create')") + public CommonResult createShipmentOrder(@Valid @RequestBody WmsShipmentOrderSaveReqVO createReqVO) { + return success(shipmentOrderService.createShipmentOrder(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新出库单") + @PreAuthorize("@ss.hasPermission('wms:shipment-order:update')") + public CommonResult updateShipmentOrder(@Valid @RequestBody WmsShipmentOrderSaveReqVO updateReqVO) { + shipmentOrderService.updateShipmentOrder(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除出库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:shipment-order:delete')") + public CommonResult deleteShipmentOrder(@RequestParam("id") Long id) { + shipmentOrderService.deleteShipmentOrder(id); + return success(true); + } + + @PutMapping("/complete") + @Operation(summary = "完成出库") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:shipment-order:complete')") + public CommonResult completeShipmentOrder(@RequestParam("id") Long id) { + shipmentOrderService.completeShipmentOrder(id); + return success(true); + } + + @PutMapping("/cancel") + @Operation(summary = "作废出库单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('wms:shipment-order:cancel')") + public CommonResult cancelShipmentOrder(@RequestParam("id") Long id) { + shipmentOrderService.cancelShipmentOrder(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得出库单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:shipment-order:query')") + public CommonResult getShipmentOrder(@RequestParam("id") Long id) { + WmsShipmentOrderDO order = shipmentOrderService.getShipmentOrder(id); + if (order == null) { + return success(null); + } + // 获得出库单的明细列表 + List detailList = shipmentOrderDetailService.getShipmentOrderDetailList(id); + // 拼接结果返回 + WmsShipmentOrderRespVO respVO = buildShipmentOrderRespVO(order) + .setDetails(buildShipmentOrderDetailRespVOList(detailList)); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得出库单分页") + @PreAuthorize("@ss.hasPermission('wms:shipment-order:query')") + public CommonResult> getShipmentOrderPage(@Valid WmsShipmentOrderPageReqVO pageReqVO) { + PageResult pageResult = shipmentOrderService.getShipmentOrderPage(pageReqVO); + return success(new PageResult<>(buildShipmentOrderRespVOList(pageResult.getList()), pageResult.getTotal())); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出出库单 Excel") + @PreAuthorize("@ss.hasPermission('wms:shipment-order:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportShipmentOrderExcel(@Valid WmsShipmentOrderPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = shipmentOrderService.getShipmentOrderPage(pageReqVO).getList(); + ExcelUtils.write(response, "出库单.xls", "数据", WmsShipmentOrderRespVO.class, + buildShipmentOrderRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private WmsShipmentOrderRespVO buildShipmentOrderRespVO(WmsShipmentOrderDO order) { + if (order == null) { + return null; + } + List list = buildShipmentOrderRespVOList(Collections.singletonList(order)); + return CollUtil.getFirst(list); + } + + private List buildShipmentOrderRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的商户、仓库、用户等数据 + Map merchantMap = merchantService.getMerchantMap(convertSet(list, WmsShipmentOrderDO::getMerchantId)); + Map warehouseMap = warehouseService.getWarehouseMap(convertSet(list, WmsShipmentOrderDO::getWarehouseId)); + Map userMap = adminUserApi.getUserMap(convertSetByFlatMap(list, + order -> Stream.of(parseUserId(order.getCreator()), parseUserId(order.getUpdater())))); + // 拼接数据 + return BeanUtils.toBean(list, WmsShipmentOrderRespVO.class, vo -> { + MapUtils.findAndThen(merchantMap, vo.getMerchantId(), merchant -> vo.setMerchantName(merchant.getName())); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + MapUtils.findAndThen(userMap, parseUserId(vo.getCreator()), user -> vo.setCreatorName(user.getNickname())); + MapUtils.findAndThen(userMap, parseUserId(vo.getUpdater()), user -> vo.setUpdaterName(user.getNickname())); + }); + } + + private Long parseUserId(String userId) { + return NumberUtil.parseLong(userId, null); + } + + private List buildShipmentOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 获取相关的商品、SKU、仓库等数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsShipmentOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsShipmentOrderDetailDO::getWarehouseId)); + // 拼接数据 + return BeanUtils.toBean(list, WmsShipmentOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderDetailController.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderDetailController.java new file mode 100644 index 000000000..f9cd14dd7 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/WmsShipmentOrderDetailController.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.shipment; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderDetailService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - WMS 出库单明细") +@RestController +@RequestMapping("/wms/shipment-order-detail") +@Validated +public class WmsShipmentOrderDetailController { + + @Resource + private WmsShipmentOrderDetailService shipmentOrderDetailService; + @Resource + private WmsItemService itemService; + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsWarehouseService warehouseService; + + @GetMapping("/list-by-order-id") + @Operation(summary = "获得出库单明细列表") + @Parameter(name = "orderId", description = "出库单编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('wms:shipment-order:query')") + public CommonResult> getShipmentOrderDetailListByOrderId( + @RequestParam("orderId") Long orderId) { + List list = shipmentOrderDetailService.getShipmentOrderDetailList(orderId); + return success(buildShipmentOrderDetailRespVOList(list)); + } + + // ==================== 拼接 VO ==================== + + private List buildShipmentOrderDetailRespVOList(List list) { + if (CollUtil.isEmpty(list)) { + return Collections.emptyList(); + } + // 查询关联数据 + Map skuMap = itemSkuService.getItemSkuMap(convertSet(list, WmsShipmentOrderDetailDO::getSkuId)); + Map itemMap = itemService.getItemMap(convertSet(skuMap.values(), WmsItemSkuDO::getItemId)); + Map warehouseMap = warehouseService.getWarehouseMap( + convertSet(list, WmsShipmentOrderDetailDO::getWarehouseId)); + // 拼接数据 + return BeanUtils.toBean(list, WmsShipmentOrderDetailRespVO.class, vo -> { + MapUtils.findAndThen(skuMap, vo.getSkuId(), sku -> { + vo.setSkuCode(sku.getCode()).setSkuName(sku.getName()).setItemId(sku.getItemId()); + MapUtils.findAndThen(itemMap, sku.getItemId(), item -> vo.setItemCode(item.getCode()) + .setItemName(item.getName()).setUnit(item.getUnit())); + }); + MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(), warehouse -> vo.setWarehouseName(warehouse.getName())); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailRespVO.java new file mode 100644 index 000000000..80e9538c1 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailRespVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - WMS 出库单明细 Response VO") +@Data +public class WmsShipmentOrderDetailRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "出库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + @Schema(description = "商品编号", example = "2048") + private Long itemId; + + @Schema(description = "商品编号", example = "SPU-APPLE") + private String itemCode; + + @Schema(description = "商品名称", example = "红富士苹果") + private String itemName; + + @Schema(description = "商品单位", example = "箱") + private String unit; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long skuId; + + @Schema(description = "规格编号", example = "SKU-APPLE-10KG") + private String skuCode; + + @Schema(description = "规格名称", example = "10kg 箱装") + private String skuName; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long warehouseId; + + @Schema(description = "仓库名称", example = "北京仓") + private String warehouseName; + + @Schema(description = "出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + private BigDecimal quantity; + + @Schema(description = "单价", example = "100.00") + private BigDecimal price; + + @Schema(description = "行金额", example = "1000.00") + private BigDecimal totalPrice; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailSaveReqVO.java new file mode 100644 index 000000000..be8a8baeb --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/detail/WmsShipmentOrderDetailSaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +@Schema(description = "管理后台 - WMS 出库单明细保存 Request VO") +@Data +public class WmsShipmentOrderDetailSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "SKU 不能为空") + private Long skuId; + + @Schema(description = "出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @NotNull(message = "出库数量不能为空") + @DecimalMin(value = "0", inclusive = false, message = "出库数量必须大于 0") + private BigDecimal quantity; + + @Schema(description = "单价", example = "100.00") + @DecimalMin(value = "0", message = "单价不能小于 0") + private BigDecimal price; + + @Schema(description = "行金额", example = "1000.00") + @DecimalMin(value = "0", message = "行金额不能小于 0") + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderPageReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderPageReqVO.java new file mode 100644 index 000000000..2a9f058ba --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderPageReqVO.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsShipmentOrderTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - WMS 出库单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class WmsShipmentOrderPageReqVO extends PageParam { + + @Schema(description = "出库单号", example = "CK202605110001") + private String no; + + @Schema(description = "单据状态", example = "0") + @InEnum(WmsOrderStatusEnum.class) + private Integer status; + + @Schema(description = "仓库编号", example = "1024") + private Long warehouseId; + + @Schema(description = "客户编号", example = "1024") + private Long merchantId; + + @Schema(description = "单据日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] orderTime; + + @Schema(description = "最小数量", example = "1.00") + private BigDecimal totalQuantityMin; + + @Schema(description = "最大数量", example = "100.00") + private BigDecimal totalQuantityMax; + + @Schema(description = "最小总金额", example = "1.00") + private BigDecimal totalPriceMin; + + @Schema(description = "最大总金额", example = "1000.00") + private BigDecimal totalPriceMax; + + @Schema(description = "出库类型", example = "201") + @InEnum(WmsShipmentOrderTypeEnum.class) + private Integer type; + + @Schema(description = "业务单号", example = "SO202605110001") + private String bizOrderNo; + + @Schema(description = "创建用户", example = "1") + private String creator; + + @Schema(description = "更新用户", example = "1") + private String updater; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "更新时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] updateTime; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderRespVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderRespVO.java new file mode 100644 index 000000000..b391c1d5e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderRespVO.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailRespVO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 出库单 Response VO") +@Data +@ExcelIgnoreUnannotated +public class WmsShipmentOrderRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "出库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CK202605110001") + @ExcelProperty("出库单号") + private String no; + + @Schema(description = "出库类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "201") + @ExcelProperty(value = "出库类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SHIPMENT_ORDER_TYPE) + private Integer type; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("单据日期") + private LocalDateTime orderTime; + + @Schema(description = "出库状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @ExcelProperty(value = "出库状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ORDER_STATUS) + private Integer status; + + @Schema(description = "业务单号", example = "SO202605110001") + @ExcelProperty("业务单号") + private String bizOrderNo; + + @Schema(description = "客户编号", example = "1024") + private Long merchantId; + + @Schema(description = "客户名称", example = "某某公司") + @ExcelProperty("客户") + private String merchantName; + + @Schema(description = "备注", example = "备注") + private String remark; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long warehouseId; + + @Schema(description = "仓库名称", example = "北京仓") + @ExcelProperty("仓库") + private String warehouseName; + + @Schema(description = "出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00") + @ExcelProperty("出库数量") + private BigDecimal totalQuantity; + + @Schema(description = "总金额", example = "1000.00") + @ExcelProperty("总金额") + private BigDecimal totalPrice; + + @Schema(description = "出库明细") + private List details; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "创建者", example = "1") + private String creator; + @Schema(description = "创建者名称", example = "芋道") + private String creatorName; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "更新者", example = "1") + private String updater; + @Schema(description = "更新者名称", example = "芋道") + private String updaterName; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderSaveReqVO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderSaveReqVO.java new file mode 100644 index 000000000..232b92dc0 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderSaveReqVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.enums.order.WmsShipmentOrderTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - WMS 出库单保存 Request VO") +@Data +public class WmsShipmentOrderSaveReqVO { + + @Schema(description = "编号", example = "1024") + private Long id; + + @Schema(description = "出库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CK202605110001") + @NotBlank(message = "出库单号不能为空") + @Size(max = 64, message = "出库单号长度不能超过 64 个字符") + private String no; + + @Schema(description = "出库类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "201") + @NotNull(message = "出库类型不能为空") + @InEnum(WmsShipmentOrderTypeEnum.class) + private Integer type; + + @Schema(description = "单据日期", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "单据日期不能为空") + private LocalDateTime orderTime; + + @Schema(description = "业务单号", example = "SO202605110001") + @Size(max = 64, message = "业务单号长度不能超过 64 个字符") + private String bizOrderNo; + + @Schema(description = "客户编号", example = "1024") + private Long merchantId; + + @Schema(description = "备注", example = "备注") + @Size(max = 255, message = "备注长度不能超过 255 个字符") + private String remark; + + @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "仓库不能为空") + private Long warehouseId; + + @Schema(description = "出库明细") + @Valid + private List details; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/package-info.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/package-info.java new file mode 100644 index 000000000..7f2a55f59 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/package-info.java @@ -0,0 +1,4 @@ +/** + * WMS 管理后台 API + */ +package cn.iocoder.yudao.module.wms.controller.admin; diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/package-info.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/package-info.java new file mode 100644 index 000000000..c75d9df62 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.iocoder.yudao.module.wms.controller; diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryDO.java new file mode 100644 index 000000000..577af728d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryDO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.inventory; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * WMS 库存 DO + * + * @author 芋道源码 + */ +@TableName("wms_inventory") +@KeySequence("wms_inventory_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsInventoryDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 商品 SKU 编号 + * + * 关联 {@link WmsItemSkuDO#getId()} + */ + private Long skuId; + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + /** + * 库存数量 + */ + private BigDecimal quantity; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryHistoryDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryHistoryDO.java new file mode 100644 index 000000000..224940ade --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/inventory/WmsInventoryHistoryDO.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.inventory; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * WMS 库存流水 DO + * + * @author 芋道源码 + */ +@TableName("wms_inventory_history") +@KeySequence("wms_inventory_history_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsInventoryHistoryDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + + // ========= 库存维度相关字段 ========= + + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + /** + * 商品 SKU 编号 + * + * 关联 {@link WmsItemSkuDO#getId()} + */ + private Long skuId; + /** + * 库存变化数量 + */ + private BigDecimal quantity; + /** + * 变化前库存数量 + */ + private BigDecimal beforeQuantity; + /** + * 变化后库存数量 + */ + private BigDecimal afterQuantity; + + // ========= 单价备注相关字段 ========= + + /** + * 单价 + */ + private BigDecimal price; + /** + * 库存变化金额 + */ + private BigDecimal totalPrice; + /** + * 备注 + */ + private String remark; + + // ========= 来源单据相关字段 ========= + + /** + * 单据编号 + */ + private Long orderId; + /** + * 单据号 + */ + private String orderNo; + /** + * 单据类型 + * + * 枚举 {@link WmsOrderTypeEnum#getType()} + */ + private Integer orderType; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemBrandDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemBrandDO.java new file mode 100644 index 000000000..ab41e05f0 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemBrandDO.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.md.item; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * WMS 商品品牌 DO + * + * @author 芋道源码 + */ +@TableName("wms_item_brand") +@KeySequence("wms_item_brand_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsItemBrandDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 品牌编号 + */ + private String code; + /** + * 品牌名称 + */ + private String name; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemCategoryDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemCategoryDO.java new file mode 100644 index 000000000..ded92281e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemCategoryDO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.md.item; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * WMS 商品分类 DO + * + * @author 芋道源码 + */ +@TableName("wms_item_category") +@KeySequence("wms_item_category_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsItemCategoryDO extends BaseDO { + + /** + * 父级编号 - 根节点 + */ + public static final Long PARENT_ID_ROOT = 0L; + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 父级编号 + * + * 关联 {@link #id} + */ + private Long parentId; + /** + * 分类编号 + */ + private String code; + /** + * 分类名称 + */ + private String name; + /** + * 排序 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemDO.java new file mode 100644 index 000000000..9a14d3ac2 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemDO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.md.item; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * WMS 商品 DO + * + * @author 芋道源码 + */ +@TableName("wms_item") +@KeySequence("wms_item_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsItemDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 商品编号 + */ + private String code; + /** + * 商品名称 + */ + private String name; + /** + * 单位 + */ + private String unit; + /** + * 商品分类编号 + * + * 关联 {@link WmsItemCategoryDO#getId()} + */ + private Long categoryId; + /** + * 商品品牌编号 + * + * 关联 {@link WmsItemBrandDO#getId()} + */ + private Long brandId; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemSkuDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemSkuDO.java new file mode 100644 index 000000000..7236b4928 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/item/WmsItemSkuDO.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.md.item; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * WMS 商品 SKU DO + * + * @author 芋道源码 + */ +@TableName("wms_item_sku") +@KeySequence("wms_item_sku_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsItemSkuDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 规格名称 + */ + private String name; + /** + * 商品编号 + * + * 关联 {@link WmsItemDO#getId()} + */ + private Long itemId; + /** + * 条码 + */ + private String barCode; + /** + * 规格编号 + */ + private String code; + + /** + * 长,单位 cm + */ + private BigDecimal length; + /** + * 宽,单位 cm + */ + private BigDecimal width; + /** + * 高,单位 cm + */ + private BigDecimal height; + + /** + * 毛重,单位 kg + */ + private BigDecimal grossWeight; + /** + * 净重,单位 kg + */ + private BigDecimal netWeight; + + /** + * 成本价,单位元 + */ + private BigDecimal costPrice; + /** + * 销售价,单位元 + */ + private BigDecimal sellingPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/merchant/WmsMerchantDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/merchant/WmsMerchantDO.java new file mode 100644 index 000000000..60b4c4dbf --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/merchant/WmsMerchantDO.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.enums.md.WmsMerchantTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * WMS 往来企业 DO + * + * @author 芋道源码 + */ +@TableName("wms_merchant") +@KeySequence("wms_merchant_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsMerchantDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 往来企业编号 + */ + private String code; + /** + * 往来企业名称 + */ + private String name; + /** + * 往来企业类型 + * + * 枚举 {@link WmsMerchantTypeEnum} + */ + private Integer type; + /** + * 级别 + */ + private String level; + /** + * 开户行 + */ + private String bankName; + /** + * 银行账户 + */ + private String bankAccount; + /** + * 地址 + */ + private String address; + /** + * 手机号 + */ + private String mobile; + /** + * 座机号 + */ + private String telephone; + /** + * 联系人 + */ + private String contact; + /** + * Email + */ + private String email; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/warehouse/WmsWarehouseDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/warehouse/WmsWarehouseDO.java new file mode 100644 index 000000000..e73c1123a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/md/warehouse/WmsWarehouseDO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * WMS 仓库 DO + * + * @author 芋道源码 + */ +@TableName("wms_warehouse") +@KeySequence("wms_warehouse_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsWarehouseDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 仓库编号 + */ + private String code; + /** + * 名称 + */ + private String name; + /** + * 备注 + */ + private String remark; + /** + * 排序 + */ + private Integer sort; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDO.java new file mode 100644 index 000000000..7ebe50a74 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDO.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.check; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * WMS 盘库单 DO + * + * @author 芋道源码 + */ +@TableName("wms_check_order") +@KeySequence("wms_check_order_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsCheckOrderDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 盘库单号 + */ + private String no; + /** + * 单据日期 + */ + private LocalDateTime orderTime; + /** + * 盘库状态 + * + * 字典 {@link DictTypeConstants#ORDER_STATUS} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + + // ========= 仓库字段 ========= + + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + + // ========= 汇总金额字段 ========= + + /** + * 盈亏数量(实盘数量 - 账面数量) + */ + private BigDecimal totalQuantity; + /** + * 总金额(账面数量 * 单价) + */ + private BigDecimal totalPrice; + /** + * 实际金额(实盘数量 * 单价) + */ + private BigDecimal actualPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDetailDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDetailDO.java new file mode 100644 index 000000000..56f35539f --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/check/WmsCheckOrderDetailDO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.check; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * WMS 盘库单明细 DO + * + * @author 芋道源码 + */ +@TableName("wms_check_order_detail") +@KeySequence("wms_check_order_detail_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsCheckOrderDetailDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + // ========= 单据商品字段 ========= + + /** + * 盘库单编号 + * + * 关联 {@link WmsCheckOrderDO#getId()} + */ + private Long orderId; + /** + * 商品 SKU 编号 + * + * 关联 {@link WmsItemSkuDO#getId()} + */ + private Long skuId; + + // ========= 仓库字段 ========= + + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + /** + * 库存编号 + * + * 关联 {@link WmsInventoryDO#getId()} + */ + private Long inventoryId; + + /** + * 入库时间 + */ + private LocalDateTime receiptTime; + + // ========= 数量金额字段 ========= + + /** + * 账面数量 + */ + private BigDecimal quantity; + /** + * 实盘数量 + */ + private BigDecimal checkQuantity; + /** + * 单价 + */ + private BigDecimal price; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDO.java new file mode 100644 index 000000000..628640991 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.movement; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * WMS 移库单 DO + * + * @author 芋道源码 + */ +@TableName("wms_movement_order") +@KeySequence("wms_movement_order_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsMovementOrderDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 移库单号 + */ + private String no; + /** + * 单据日期 + */ + private LocalDateTime orderTime; + /** + * 移库状态 + * + * 字典 {@link DictTypeConstants#ORDER_STATUS} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + + // ========= 来源仓库字段 ========= + + /** + * 来源仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long sourceWarehouseId; + + // ========= 目标仓库字段 ========= + + /** + * 目标仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long targetWarehouseId; + + // ========= 汇总金额字段 ========= + + /** + * 总数量 + */ + private BigDecimal totalQuantity; + /** + * 总金额 + */ + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDetailDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDetailDO.java new file mode 100644 index 000000000..e9e9d2f24 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/movement/WmsMovementOrderDetailDO.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.movement; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * WMS 移库单明细 DO + * + * @author 芋道源码 + */ +@TableName("wms_movement_order_detail") +@KeySequence("wms_movement_order_detail_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsMovementOrderDetailDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + // ========= 单据商品字段 ========= + + /** + * 移库单编号 + * + * 关联 {@link WmsMovementOrderDO#getId()} + */ + private Long orderId; + /** + * 商品 SKU 编号 + * + * 关联 {@link WmsItemSkuDO#getId()} + */ + private Long skuId; + + // ========= 来源仓库字段 ========= + + /** + * 来源仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long sourceWarehouseId; + + // ========= 目标仓库字段 ========= + + /** + * 目标仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long targetWarehouseId; + + // ========= 数量金额字段 ========= + + /** + * 移库数量 + */ + private BigDecimal quantity; + /** + * 单价 + */ + private BigDecimal price; + /** + * 行金额(数量 * 单价) + */ + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDO.java new file mode 100644 index 000000000..86458cfbb --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import cn.iocoder.yudao.module.wms.enums.order.WmsReceiptOrderTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * WMS 入库单 DO + * + * @author 芋道源码 + */ +@TableName("wms_receipt_order") +@KeySequence("wms_receipt_order_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsReceiptOrderDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 入库单号 + */ + private String no; + /** + * 入库类型 + * + * 枚举 {@link WmsReceiptOrderTypeEnum} + * 字典 {@link DictTypeConstants#RECEIPT_ORDER_TYPE} + */ + private Integer type; + /** + * 单据日期 + */ + private LocalDateTime orderTime; + /** + * 入库状态 + * + * 字典 {@link DictTypeConstants#ORDER_STATUS} + */ + private Integer status; + /** + * 业务订单号 + */ + private String bizOrderNo; + /** + * 供应商编号 + * + * 关联 {@link WmsMerchantDO#getId()} + */ + private Long merchantId; + /** + * 备注 + */ + private String remark; + + // ========= 仓库字段 ========= + + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + + // ========= 汇总金额字段 ========= + + /** + * 总数量 + */ + private BigDecimal totalQuantity; + /** + * 总金额 + */ + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDetailDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDetailDO.java new file mode 100644 index 000000000..6920d369d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/receipt/WmsReceiptOrderDetailDO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * WMS 入库单明细 DO + * + * @author 芋道源码 + */ +@TableName("wms_receipt_order_detail") +@KeySequence("wms_receipt_order_detail_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsReceiptOrderDetailDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + // ========= 单据商品字段 ========= + + /** + * 入库单编号 + * + * 关联 {@link WmsReceiptOrderDO#getId()} + */ + private Long orderId; + /** + * 商品 SKU 编号 + * + * 关联 {@link WmsItemSkuDO#getId()} + */ + private Long skuId; + + // ========= 仓库字段 ========= + + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + + // ========= 数量金额字段 ========= + + /** + * 入库数量 + */ + private BigDecimal quantity; + /** + * 单价 + */ + private BigDecimal price; + /** + * 行金额(数量 * 单价) + */ + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDO.java new file mode 100644 index 000000000..d2e3e4ac2 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.enums.DictTypeConstants; +import cn.iocoder.yudao.module.wms.enums.order.WmsShipmentOrderTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * WMS 出库单 DO + * + * @author 芋道源码 + */ +@TableName("wms_shipment_order") +@KeySequence("wms_shipment_order_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsShipmentOrderDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + /** + * 出库单号 + */ + private String no; + /** + * 出库类型 + * + * 枚举 {@link WmsShipmentOrderTypeEnum} + * 字典 {@link DictTypeConstants#SHIPMENT_ORDER_TYPE} + */ + private Integer type; + /** + * 单据日期 + */ + private LocalDateTime orderTime; + /** + * 出库状态 + * + * 字典 {@link DictTypeConstants#ORDER_STATUS} + */ + private Integer status; + /** + * 业务订单号 + */ + private String bizOrderNo; + /** + * 客户编号 + * + * 关联 {@link WmsMerchantDO#getId()} + */ + private Long merchantId; + /** + * 备注 + */ + private String remark; + + // ========= 仓库字段 ========= + + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + + // ========= 汇总金额字段 ========= + + /** + * 总数量 + */ + private BigDecimal totalQuantity; + /** + * 总金额 + */ + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDetailDO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDetailDO.java new file mode 100644 index 000000000..fdcb67e45 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/dataobject/order/shipment/WmsShipmentOrderDetailDO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * WMS 出库单明细 DO + * + * @author 芋道源码 + */ +@TableName("wms_shipment_order_detail") +@KeySequence("wms_shipment_order_detail_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WmsShipmentOrderDetailDO extends BaseDO { + + /** + * 主键编号 + */ + @TableId + private Long id; + // ========= 单据商品字段 ========= + + /** + * 出库单编号 + * + * 关联 {@link WmsShipmentOrderDO#getId()} + */ + private Long orderId; + /** + * 商品 SKU 编号 + * + * 关联 {@link WmsItemSkuDO#getId()} + */ + private Long skuId; + + // ========= 仓库字段 ========= + + /** + * 仓库编号 + * + * 关联 {@link WmsWarehouseDO#getId()} + */ + private Long warehouseId; + + // ========= 数量金额字段 ========= + + /** + * 出库数量 + */ + private BigDecimal quantity; + /** + * 单价 + */ + private BigDecimal price; + /** + * 行金额(数量 * 单价) + */ + private BigDecimal totalPrice; + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/home/WmsHomeStatisticsMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/home/WmsHomeStatisticsMapper.java new file mode 100644 index 000000000..b752e42d4 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/home/WmsHomeStatisticsMapper.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.home; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * WMS 首页统计 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsHomeStatisticsMapper { + + /** + * 按单据类型和状态统计单据数量 + * + * @param warehouseId 仓库编号 + * @return [{ "orderType": 1, "status": 0, "count": 5 }, ...] + */ + List> selectOrderCountGroupByTypeAndStatus(@Param("warehouseId") Long warehouseId); + + /** + * 按天聚合单据数量 + * + * @param beginTime >= 开始时间 + * @param endTime < 结束时间 + * @param warehouseId 仓库编号 + * @return [{ "date": "2026-05-14", "orderType": 1, "count": 5 }, ...] + */ + List> selectDailyOrderTrend(@Param("beginTime") LocalDateTime beginTime, + @Param("endTime") LocalDateTime endTime, + @Param("warehouseId") Long warehouseId); + + /** + * 统计库存总数量 + * + * @param warehouseId 仓库编号 + * @return 库存总数量 + */ + BigDecimal selectInventoryTotalQuantity(@Param("warehouseId") Long warehouseId); + + /** + * 按商品统计库存数量排行 + * + * @param warehouseId 仓库编号 + * @param limit 数量限制 + * @return [{ "id": 1, "name": "A4 复印纸", "quantity": 100 }, ...] + */ + List> selectInventoryItemRank(@Param("warehouseId") Long warehouseId, + @Param("limit") Integer limit); + + /** + * 按仓库统计库存数量排行 + * + * @param warehouseId 仓库编号 + * @param limit 数量限制 + * @return [{ "id": 1, "name": "上海仓", "quantity": 100 }, ...] + */ + List> selectInventoryWarehouseRank(@Param("warehouseId") Long warehouseId, + @Param("limit") Integer limit); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryHistoryMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryHistoryMapper.java new file mode 100644 index 000000000..196980dac --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryHistoryMapper.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.inventory; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.history.WmsInventoryHistoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryHistoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * WMS 库存流水 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsInventoryHistoryMapper extends BaseMapperX { + + default PageResult selectPage(WmsInventoryHistoryPageReqVO reqVO) { + MPJLambdaWrapperX query = new MPJLambdaWrapperX() + .selectAll(WmsInventoryHistoryDO.class) + .leftJoin(WmsItemSkuDO.class, WmsItemSkuDO::getId, WmsInventoryHistoryDO::getSkuId) + .leftJoin(WmsItemDO.class, WmsItemDO::getId, WmsItemSkuDO::getItemId) + .likeIfPresent(WmsItemDO::getCode, reqVO.getItemCode()) + .likeIfPresent(WmsItemDO::getName, reqVO.getItemName()) + .eqIfPresent(WmsInventoryHistoryDO::getSkuId, reqVO.getSkuId()) + .likeIfPresent(WmsItemSkuDO::getCode, reqVO.getSkuCode()) + .likeIfPresent(WmsItemSkuDO::getName, reqVO.getSkuName()) + .eqIfPresent(WmsInventoryHistoryDO::getWarehouseId, reqVO.getWarehouseId()) + .eqIfPresent(WmsInventoryHistoryDO::getOrderNo, reqVO.getOrderNo()) + .eqIfPresent(WmsInventoryHistoryDO::getOrderType, reqVO.getOrderType()) + .betweenIfPresent(WmsInventoryHistoryDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(WmsInventoryHistoryDO::getCreateTime) + .orderByDesc(WmsInventoryHistoryDO::getId); + return selectJoinPage(reqVO, WmsInventoryHistoryDO.class, query); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryMapper.java new file mode 100644 index 000000000..1e1a17a25 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/inventory/WmsInventoryMapper.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.inventory; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import org.apache.ibatis.annotations.Mapper; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * WMS 库存 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsInventoryMapper extends BaseMapperX { + + default PageResult selectPage(WmsInventoryPageReqVO reqVO) { + MPJLambdaWrapperX query = new MPJLambdaWrapperX() + .selectAll(WmsInventoryDO.class) + .innerJoin(WmsItemSkuDO.class, WmsItemSkuDO::getId, WmsInventoryDO::getSkuId) + .innerJoin(WmsItemDO.class, WmsItemDO::getId, WmsItemSkuDO::getItemId) + .likeIfPresent(WmsItemDO::getCode, reqVO.getItemCode()) + .likeIfPresent(WmsItemDO::getName, reqVO.getItemName()) + .eqIfPresent(WmsInventoryDO::getSkuId, reqVO.getSkuId()) + .likeIfPresent(WmsItemSkuDO::getCode, reqVO.getSkuCode()) + .likeIfPresent(WmsItemSkuDO::getName, reqVO.getSkuName()) + .eqIfPresent(WmsInventoryDO::getWarehouseId, reqVO.getWarehouseId()) + .geIfPresent(WmsInventoryDO::getQuantity, reqVO.getMinQuantity()); + if (Boolean.TRUE.equals(reqVO.getOnlyPositiveQuantity())) { + query.gt(WmsInventoryDO::getQuantity, BigDecimal.ZERO); + } + appendDimensionOrder(query, reqVO.getType()); + return selectJoinPage(reqVO, WmsInventoryDO.class, query); + } + + default Long selectCountBySkuId(Long skuId) { + return selectCount(WmsInventoryDO::getSkuId, skuId); + } + + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(WmsInventoryDO::getWarehouseId, warehouseId); + } + + default List selectList(WmsInventoryListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eq(WmsInventoryDO::getWarehouseId, reqVO.getWarehouseId()) + .orderByAsc(WmsInventoryDO::getSkuId) + .orderByAsc(WmsInventoryDO::getId)); + } + + default WmsInventoryDO selectBySkuIdAndWarehouseId(Long skuId, Long warehouseId) { + return selectOne(WmsInventoryDO::getSkuId, skuId, + WmsInventoryDO::getWarehouseId, warehouseId); + } + + default WmsInventoryDO selectByIdForUpdate(Long id) { + return selectOne(new LambdaQueryWrapperX() + .eq(WmsInventoryDO::getId, id) + .last("FOR UPDATE")); + } + + /** + * 根据多个唯一键,批量查询库存列表 + * + * @param keys 唯一键列表:由 SKU 编号 + 仓库编号组成 + * @return 库存列表 + */ + default List selectListByKeys(Collection keys) { + if (CollUtil.isEmpty(keys)) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .and(query -> { + boolean first = true; + for (WmsInventoryDO key : keys) { + if (!first) { + query.or(); + } + query.eq(WmsInventoryDO::getSkuId, key.getSkuId()) + .eq(WmsInventoryDO::getWarehouseId, key.getWarehouseId()); + first = false; + } + })); + } + + default List selectListByIdsForUpdate(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .in(WmsInventoryDO::getId, ids) + .orderByAsc(WmsInventoryDO::getId) + .last("FOR UPDATE")); + } + + static void appendDimensionOrder(MPJLambdaWrapperX query, String type) { + if (StrUtil.equals(WmsInventoryPageReqVO.TYPE_WAREHOUSE, type)) { + query.orderByAsc(WmsInventoryDO::getWarehouseId) + .orderByAsc(WmsItemSkuDO::getItemId) + .orderByAsc(WmsInventoryDO::getSkuId) + .orderByAsc(WmsInventoryDO::getId); + return; + } + if (StrUtil.equals(WmsInventoryPageReqVO.TYPE_ITEM, type)) { + query.orderByAsc(WmsItemSkuDO::getItemId) + .orderByAsc(WmsInventoryDO::getSkuId) + .orderByAsc(WmsInventoryDO::getWarehouseId) + .orderByAsc(WmsInventoryDO::getId); + return; + } + throw new IllegalArgumentException("未知库存统计维度:" + type); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemBrandMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemBrandMapper.java new file mode 100644 index 000000000..ffb94189b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemBrandMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.md.item; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemBrandDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * WMS 商品品牌 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsItemBrandMapper extends BaseMapperX { + + default PageResult selectPage(WmsItemBrandPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(WmsItemBrandDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsItemBrandDO::getName, reqVO.getName()) + .orderByDesc(WmsItemBrandDO::getId)); + } + + default WmsItemBrandDO selectByCode(String code) { + return selectOne(WmsItemBrandDO::getCode, code); + } + + default WmsItemBrandDO selectByName(String name) { + return selectOne(WmsItemBrandDO::getName, name); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemCategoryMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemCategoryMapper.java new file mode 100644 index 000000000..f95bc30c1 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemCategoryMapper.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.md.item; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategoryListReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemCategoryDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 商品分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsItemCategoryMapper extends BaseMapperX { + + default List selectList(WmsItemCategoryListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(WmsItemCategoryDO::getParentId, reqVO.getParentId()) + .likeIfPresent(WmsItemCategoryDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsItemCategoryDO::getName, reqVO.getName()) + .eqIfPresent(WmsItemCategoryDO::getStatus, reqVO.getStatus()) + .orderByAsc(WmsItemCategoryDO::getSort) + .orderByAsc(WmsItemCategoryDO::getId)); + } + + default WmsItemCategoryDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(WmsItemCategoryDO::getParentId, parentId, WmsItemCategoryDO::getName, name); + } + + default WmsItemCategoryDO selectByCode(String code) { + return selectOne(WmsItemCategoryDO::getCode, code); + } + + default List selectListByParentIds(Collection parentIds) { + return selectList(new LambdaQueryWrapperX() + .in(WmsItemCategoryDO::getParentId, parentIds) + .orderByAsc(WmsItemCategoryDO::getSort) + .orderByAsc(WmsItemCategoryDO::getId)); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(WmsItemCategoryDO::getParentId, parentId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemMapper.java new file mode 100644 index 000000000..2645ed080 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemMapper.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.md.item; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 商品 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsItemMapper extends BaseMapperX { + + default PageResult selectPage(WmsItemPageReqVO reqVO, Collection categoryIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(WmsItemDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsItemDO::getName, reqVO.getName()) + .inIfPresent(WmsItemDO::getCategoryId, categoryIds) + .eqIfPresent(WmsItemDO::getBrandId, reqVO.getBrandId()) + .betweenIfPresent(WmsItemDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(WmsItemDO::getId)); + } + + default List selectList(WmsItemListReqVO reqVO, Collection categoryIds) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(WmsItemDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsItemDO::getName, reqVO.getName()) + .inIfPresent(WmsItemDO::getCategoryId, categoryIds) + .eqIfPresent(WmsItemDO::getBrandId, reqVO.getBrandId()) + .betweenIfPresent(WmsItemDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(WmsItemDO::getId)); + } + + default WmsItemDO selectByName(String name) { + return selectOne(WmsItemDO::getName, name); + } + + default WmsItemDO selectByCode(String code) { + return selectOne(WmsItemDO::getCode, code); + } + + default Long selectCountByCategoryId(Long categoryId) { + return selectCount(WmsItemDO::getCategoryId, categoryId); + } + + default Long selectCountByBrandId(Long brandId) { + return selectCount(WmsItemDO::getBrandId, brandId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemSkuMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemSkuMapper.java new file mode 100644 index 000000000..1a0d528f9 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/item/WmsItemSkuMapper.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.md.item; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * WMS 商品 SKU Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsItemSkuMapper extends BaseMapperX { + + /** + * 按 SKU 维度分页查询,支持商品 / 品牌 / 分类多表联查筛选。 + * + * 使用 {@link MPJLambdaWrapperX} 替代 lite 的手写 XML JOIN(见 lite + * {@code ItemSkuMapper.selectByBo}),范式参照本模块 {@code WmsInventoryMapper.selectPage}。 + */ + default PageResult selectPage(WmsItemSkuPageReqVO reqVO) { + MPJLambdaWrapperX query = new MPJLambdaWrapperX() + .selectAll(WmsItemSkuDO.class) + .innerJoin(WmsItemDO.class, WmsItemDO::getId, WmsItemSkuDO::getItemId) + .likeIfPresent(WmsItemDO::getCode, reqVO.getItemCode()) + .likeIfPresent(WmsItemDO::getName, reqVO.getItemName()) + .eqIfPresent(WmsItemDO::getCategoryId, reqVO.getCategoryId()) + .eqIfPresent(WmsItemDO::getBrandId, reqVO.getBrandId()) + .likeIfPresent(WmsItemSkuDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsItemSkuDO::getName, reqVO.getName()) + .likeIfPresent(WmsItemSkuDO::getBarCode, reqVO.getBarCode()) + .orderByDesc(WmsItemSkuDO::getId); + return selectJoinPage(reqVO, WmsItemSkuDO.class, query); + } + + default List selectListByItemId(Long itemId) { + return selectList(new LambdaQueryWrapperX() + .eq(WmsItemSkuDO::getItemId, itemId) + .orderByAsc(WmsItemSkuDO::getId)); + } + + default List selectListByItemIds(Collection itemIds) { + if (CollUtil.isEmpty(itemIds)) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .inIfPresent(WmsItemSkuDO::getItemId, itemIds) + .orderByAsc(WmsItemSkuDO::getId)); + } + + default List selectList(Collection itemIds, String code, String name) { + if (itemIds != null && CollUtil.isEmpty(itemIds)) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .inIfPresent(WmsItemSkuDO::getItemId, itemIds) + .likeIfPresent(WmsItemSkuDO::getCode, code) + .likeIfPresent(WmsItemSkuDO::getName, name) + .orderByAsc(WmsItemSkuDO::getId)); + } + + default void deleteByItemId(Long itemId) { + delete(WmsItemSkuDO::getItemId, itemId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/merchant/WmsMerchantMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/merchant/WmsMerchantMapper.java new file mode 100644 index 000000000..efc79f130 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/merchant/WmsMerchantMapper.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.md.merchant; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * WMS 往来企业 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsMerchantMapper extends BaseMapperX { + + default PageResult selectPage(WmsMerchantPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(WmsMerchantDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsMerchantDO::getName, reqVO.getName()) + .eqIfPresent(WmsMerchantDO::getType, reqVO.getType()) + .inIfPresent(WmsMerchantDO::getType, reqVO.getTypes()) + .orderByDesc(WmsMerchantDO::getId)); + } + + default List selectList(WmsMerchantListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(WmsMerchantDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsMerchantDO::getName, reqVO.getName()) + .eqIfPresent(WmsMerchantDO::getType, reqVO.getType()) + .inIfPresent(WmsMerchantDO::getType, reqVO.getTypes()) + .orderByDesc(WmsMerchantDO::getId)); + } + + default WmsMerchantDO selectByCode(String code) { + return selectOne(WmsMerchantDO::getCode, code); + } + + default WmsMerchantDO selectByName(String name) { + return selectOne(WmsMerchantDO::getName, name); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/warehouse/WmsWarehouseMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/warehouse/WmsWarehouseMapper.java new file mode 100644 index 000000000..ff4fe46a8 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/md/warehouse/WmsWarehouseMapper.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.md.warehouse; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehousePageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * WMS 仓库 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsWarehouseMapper extends BaseMapperX { + + default PageResult selectPage(WmsWarehousePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(WmsWarehouseDO::getCode, reqVO.getCode()) + .likeIfPresent(WmsWarehouseDO::getName, reqVO.getName()) + .orderByAsc(WmsWarehouseDO::getSort) + .orderByDesc(WmsWarehouseDO::getId)); + } + + default WmsWarehouseDO selectByCode(String code) { + return selectOne(WmsWarehouseDO::getCode, code); + } + + default WmsWarehouseDO selectByName(String name) { + return selectOne(WmsWarehouseDO::getName, name); + } + + default List selectSimpleList() { + return selectList(new LambdaQueryWrapperX() + .orderByAsc(WmsWarehouseDO::getSort) + .orderByDesc(WmsWarehouseDO::getId)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java new file mode 100644 index 000000000..f2cd89280 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderDetailMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.check; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 盘库单明细 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsCheckOrderDetailMapper extends BaseMapperX { + + default List selectListByOrderId(Long orderId) { + return selectList(WmsCheckOrderDetailDO::getOrderId, orderId); + } + + default List selectListByOrderIds(Collection orderIds) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(WmsCheckOrderDetailDO::getOrderId, orderIds) + .orderByAsc(WmsCheckOrderDetailDO::getOrderId) + .orderByAsc(WmsCheckOrderDetailDO::getId)); + } + + default void deleteByOrderId(Long orderId) { + delete(WmsCheckOrderDetailDO::getOrderId, orderId); + } + + default Long selectCountBySkuId(Long skuId) { + return selectCount(WmsCheckOrderDetailDO::getSkuId, skuId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderMapper.java new file mode 100644 index 000000000..9871e76e5 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/check/WmsCheckOrderMapper.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.check; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * WMS 盘库单 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsCheckOrderMapper extends BaseMapperX { + + default PageResult selectPage(WmsCheckOrderPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(WmsCheckOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(WmsCheckOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(WmsCheckOrderDO::getWarehouseId, reqVO.getWarehouseId()) + .betweenIfPresent(WmsCheckOrderDO::getOrderTime, reqVO.getOrderTime()) + .geIfPresent(WmsCheckOrderDO::getTotalQuantity, reqVO.getTotalQuantityMin()) + .leIfPresent(WmsCheckOrderDO::getTotalQuantity, reqVO.getTotalQuantityMax()) + .geIfPresent(WmsCheckOrderDO::getTotalPrice, reqVO.getTotalPriceMin()) + .leIfPresent(WmsCheckOrderDO::getTotalPrice, reqVO.getTotalPriceMax()) + .geIfPresent(WmsCheckOrderDO::getActualPrice, reqVO.getActualPriceMin()) + .leIfPresent(WmsCheckOrderDO::getActualPrice, reqVO.getActualPriceMax()) + .eqIfPresent(WmsCheckOrderDO::getCreator, reqVO.getCreator()) + .eqIfPresent(WmsCheckOrderDO::getUpdater, reqVO.getUpdater()) + .betweenIfPresent(WmsCheckOrderDO::getCreateTime, reqVO.getCreateTime()) + .betweenIfPresent(WmsCheckOrderDO::getUpdateTime, reqVO.getUpdateTime()) + .orderByDesc(WmsCheckOrderDO::getId)); + } + + default WmsCheckOrderDO selectByNo(String no) { + return selectOne(WmsCheckOrderDO::getNo, no); + } + + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(WmsCheckOrderDO::getWarehouseId, warehouseId); + } + + default int updateByIdAndStatus(Long id, Integer status, WmsCheckOrderDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(WmsCheckOrderDO::getId, id) + .eq(WmsCheckOrderDO::getStatus, status)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java new file mode 100644 index 000000000..4e7eb3a56 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderDetailMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.movement; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 移库单明细 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsMovementOrderDetailMapper extends BaseMapperX { + + default List selectListByOrderId(Long orderId) { + return selectList(WmsMovementOrderDetailDO::getOrderId, orderId); + } + + default List selectListByOrderIds(Collection orderIds) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(WmsMovementOrderDetailDO::getOrderId, orderIds) + .orderByAsc(WmsMovementOrderDetailDO::getOrderId) + .orderByAsc(WmsMovementOrderDetailDO::getId)); + } + + default void deleteByOrderId(Long orderId) { + delete(WmsMovementOrderDetailDO::getOrderId, orderId); + } + + default Long selectCountBySkuId(Long skuId) { + return selectCount(WmsMovementOrderDetailDO::getSkuId, skuId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java new file mode 100644 index 000000000..337590278 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/movement/WmsMovementOrderMapper.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.movement; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * WMS 移库单 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsMovementOrderMapper extends BaseMapperX { + + default PageResult selectPage(WmsMovementOrderPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(WmsMovementOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(WmsMovementOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(WmsMovementOrderDO::getSourceWarehouseId, reqVO.getSourceWarehouseId()) + .eqIfPresent(WmsMovementOrderDO::getTargetWarehouseId, reqVO.getTargetWarehouseId()) + .betweenIfPresent(WmsMovementOrderDO::getOrderTime, reqVO.getOrderTime()) + .geIfPresent(WmsMovementOrderDO::getTotalQuantity, reqVO.getTotalQuantityMin()) + .leIfPresent(WmsMovementOrderDO::getTotalQuantity, reqVO.getTotalQuantityMax()) + .geIfPresent(WmsMovementOrderDO::getTotalPrice, reqVO.getTotalPriceMin()) + .leIfPresent(WmsMovementOrderDO::getTotalPrice, reqVO.getTotalPriceMax()) + .eqIfPresent(WmsMovementOrderDO::getCreator, reqVO.getCreator()) + .eqIfPresent(WmsMovementOrderDO::getUpdater, reqVO.getUpdater()) + .betweenIfPresent(WmsMovementOrderDO::getCreateTime, reqVO.getCreateTime()) + .betweenIfPresent(WmsMovementOrderDO::getUpdateTime, reqVO.getUpdateTime()) + .orderByDesc(WmsMovementOrderDO::getId)); + } + + default WmsMovementOrderDO selectByNo(String no) { + return selectOne(WmsMovementOrderDO::getNo, no); + } + + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(new LambdaQueryWrapperX() + .and(query -> query.eq(WmsMovementOrderDO::getSourceWarehouseId, warehouseId) + .or().eq(WmsMovementOrderDO::getTargetWarehouseId, warehouseId))); + } + + default int updateByIdAndStatus(Long id, Integer status, WmsMovementOrderDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(WmsMovementOrderDO::getId, id) + .eq(WmsMovementOrderDO::getStatus, status)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java new file mode 100644 index 000000000..b3ce109bb --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderDetailMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.receipt; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 入库单明细 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsReceiptOrderDetailMapper extends BaseMapperX { + + default List selectListByOrderId(Long orderId) { + return selectList(WmsReceiptOrderDetailDO::getOrderId, orderId); + } + + default List selectListByOrderIds(Collection orderIds) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(WmsReceiptOrderDetailDO::getOrderId, orderIds) + .orderByAsc(WmsReceiptOrderDetailDO::getOrderId) + .orderByAsc(WmsReceiptOrderDetailDO::getId)); + } + + default void deleteByOrderId(Long orderId) { + delete(WmsReceiptOrderDetailDO::getOrderId, orderId); + } + + default Long selectCountBySkuId(Long skuId) { + return selectCount(WmsReceiptOrderDetailDO::getSkuId, skuId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderMapper.java new file mode 100644 index 000000000..550e6e2df --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/receipt/WmsReceiptOrderMapper.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.receipt; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * WMS 入库单 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsReceiptOrderMapper extends BaseMapperX { + + default PageResult selectPage(WmsReceiptOrderPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(WmsReceiptOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(WmsReceiptOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(WmsReceiptOrderDO::getWarehouseId, reqVO.getWarehouseId()) + .eqIfPresent(WmsReceiptOrderDO::getMerchantId, reqVO.getMerchantId()) + .betweenIfPresent(WmsReceiptOrderDO::getOrderTime, reqVO.getOrderTime()) + .geIfPresent(WmsReceiptOrderDO::getTotalQuantity, reqVO.getTotalQuantityMin()) + .leIfPresent(WmsReceiptOrderDO::getTotalQuantity, reqVO.getTotalQuantityMax()) + .geIfPresent(WmsReceiptOrderDO::getTotalPrice, reqVO.getTotalPriceMin()) + .leIfPresent(WmsReceiptOrderDO::getTotalPrice, reqVO.getTotalPriceMax()) + .eqIfPresent(WmsReceiptOrderDO::getType, reqVO.getType()) + .likeIfPresent(WmsReceiptOrderDO::getBizOrderNo, reqVO.getBizOrderNo()) + .eqIfPresent(WmsReceiptOrderDO::getCreator, reqVO.getCreator()) + .eqIfPresent(WmsReceiptOrderDO::getUpdater, reqVO.getUpdater()) + .betweenIfPresent(WmsReceiptOrderDO::getCreateTime, reqVO.getCreateTime()) + .betweenIfPresent(WmsReceiptOrderDO::getUpdateTime, reqVO.getUpdateTime()) + .orderByDesc(WmsReceiptOrderDO::getId)); + } + + default WmsReceiptOrderDO selectByNo(String no) { + return selectOne(WmsReceiptOrderDO::getNo, no); + } + + default Long selectCountByMerchantId(Long merchantId) { + return selectCount(WmsReceiptOrderDO::getMerchantId, merchantId); + } + + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(WmsReceiptOrderDO::getWarehouseId, warehouseId); + } + + default int updateByIdAndStatus(Long id, Integer status, WmsReceiptOrderDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(WmsReceiptOrderDO::getId, id) + .eq(WmsReceiptOrderDO::getStatus, status)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java new file mode 100644 index 000000000..d59df25fb --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderDetailMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.shipment; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 出库单明细 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsShipmentOrderDetailMapper extends BaseMapperX { + + default List selectListByOrderId(Long orderId) { + return selectList(WmsShipmentOrderDetailDO::getOrderId, orderId); + } + + default List selectListByOrderIds(Collection orderIds) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(WmsShipmentOrderDetailDO::getOrderId, orderIds) + .orderByAsc(WmsShipmentOrderDetailDO::getOrderId) + .orderByAsc(WmsShipmentOrderDetailDO::getId)); + } + + default void deleteByOrderId(Long orderId) { + delete(WmsShipmentOrderDetailDO::getOrderId, orderId); + } + + default Long selectCountBySkuId(Long skuId) { + return selectCount(WmsShipmentOrderDetailDO::getSkuId, skuId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderMapper.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderMapper.java new file mode 100644 index 000000000..f14df68dd --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/order/shipment/WmsShipmentOrderMapper.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.wms.dal.mysql.order.shipment; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * WMS 出库单 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface WmsShipmentOrderMapper extends BaseMapperX { + + default PageResult selectPage(WmsShipmentOrderPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(WmsShipmentOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(WmsShipmentOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(WmsShipmentOrderDO::getWarehouseId, reqVO.getWarehouseId()) + .eqIfPresent(WmsShipmentOrderDO::getMerchantId, reqVO.getMerchantId()) + .betweenIfPresent(WmsShipmentOrderDO::getOrderTime, reqVO.getOrderTime()) + .geIfPresent(WmsShipmentOrderDO::getTotalQuantity, reqVO.getTotalQuantityMin()) + .leIfPresent(WmsShipmentOrderDO::getTotalQuantity, reqVO.getTotalQuantityMax()) + .geIfPresent(WmsShipmentOrderDO::getTotalPrice, reqVO.getTotalPriceMin()) + .leIfPresent(WmsShipmentOrderDO::getTotalPrice, reqVO.getTotalPriceMax()) + .eqIfPresent(WmsShipmentOrderDO::getType, reqVO.getType()) + .likeIfPresent(WmsShipmentOrderDO::getBizOrderNo, reqVO.getBizOrderNo()) + .eqIfPresent(WmsShipmentOrderDO::getCreator, reqVO.getCreator()) + .eqIfPresent(WmsShipmentOrderDO::getUpdater, reqVO.getUpdater()) + .betweenIfPresent(WmsShipmentOrderDO::getCreateTime, reqVO.getCreateTime()) + .betweenIfPresent(WmsShipmentOrderDO::getUpdateTime, reqVO.getUpdateTime()) + .orderByDesc(WmsShipmentOrderDO::getId)); + } + + default WmsShipmentOrderDO selectByNo(String no) { + return selectOne(WmsShipmentOrderDO::getNo, no); + } + + default Long selectCountByMerchantId(Long merchantId) { + return selectCount(WmsShipmentOrderDO::getMerchantId, merchantId); + } + + default Long selectCountByWarehouseId(Long warehouseId) { + return selectCount(WmsShipmentOrderDO::getWarehouseId, warehouseId); + } + + default int updateByIdAndStatus(Long id, Integer status, WmsShipmentOrderDO updateObj) { + return update(updateObj, new LambdaQueryWrapper() + .eq(WmsShipmentOrderDO::getId, id) + .eq(WmsShipmentOrderDO::getStatus, status)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/package-info.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/package-info.java new file mode 100644 index 000000000..115397b8c --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 WMS 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.wms.framework; diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/rpc/config/RpcConfiguration.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/rpc/config/RpcConfiguration.java new file mode 100644 index 000000000..c1daef0c6 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/rpc/config/RpcConfiguration.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.module.wms.framework.rpc.config; + +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false) +@EnableFeignClients(clients = AdminUserApi.class) +public class RpcConfiguration { +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/config/WmsWebConfiguration.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/config/WmsWebConfiguration.java new file mode 100644 index 000000000..37c75718f --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/config/WmsWebConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.wms.framework.web.config; + +import cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * WMS 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class WmsWebConfiguration { + + /** + * WMS 模块的 API 分组 + */ + @Bean + public GroupedOpenApi wmsGroupedOpenApi() { + return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("wms"); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/package-info.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/package-info.java new file mode 100644 index 000000000..13cb703d9 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * WMS 模块的 web 配置 + */ +package cn.iocoder.yudao.module.wms.framework.web; diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/package-info.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/package-info.java new file mode 100644 index 000000000..099e22cb8 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/package-info.java @@ -0,0 +1,10 @@ +/** + * wms 包下,仓库管理系统(Warehouse Management System) + * 例如说:仓库、物料、库存、入库、出库、移库、盘库等等 + * + * 1. Controller URL:以 /wms/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 wms_ 开头,方便在数据库中区分 + * + * 注意,由于 WMS 模块下,容易和其它模块重名,所以类名都加载 Wms 的前缀~ + */ +package cn.iocoder.yudao.module.wms; diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsService.java new file mode 100644 index 000000000..75015ef54 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsService.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.wms.service.home; + +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeInventorySummaryRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeOrderSummaryRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeOrderTrendRespVO; + +import java.util.List; + +/** + * WMS 首页统计 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsHomeStatisticsService { + + /** + * 获得单据汇总统计 + * + * @param warehouseId 仓库编号 + * @return 单据汇总统计 + */ + List getOrderSummary(Long warehouseId); + + /** + * 获得单据趋势,按日期统计入库、出库、移库、盘库单据数量。 + * + * @param days 最近天数,包含今天 + * @param warehouseId 仓库编号,为空时统计全部仓库 + * @return 单据趋势 + */ + List getOrderTrend(Integer days, Long warehouseId); + + /** + * 获得库存汇总统计 + * + * @param warehouseId 仓库编号 + * @param goodsLimit 商品排行数量 + * @param warehouseLimit 仓库排行数量 + * @return 库存汇总统计 + */ + WmsHomeInventorySummaryRespVO getInventorySummary(Long warehouseId, Integer goodsLimit, Integer warehouseLimit); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java new file mode 100644 index 000000000..8aa2926a2 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.wms.service.home; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeInventorySummaryRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeOrderStatusRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeOrderSummaryRespVO; +import cn.iocoder.yudao.module.wms.controller.admin.home.vo.WmsHomeOrderTrendRespVO; +import cn.iocoder.yudao.module.wms.dal.mysql.home.WmsHomeStatisticsMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.getBigDecimal; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.getDateList; + +/** + * WMS 首页统计 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class WmsHomeStatisticsServiceImpl implements WmsHomeStatisticsService { + + @Resource + private WmsHomeStatisticsMapper homeStatisticsMapper; + @Resource + private WmsWarehouseService warehouseService; + + @Override + public List getOrderSummary(Long warehouseId) { + validateWarehouseIfPresent(warehouseId); + // 查询单据数量 + List> stats = homeStatisticsMapper.selectOrderCountGroupByTypeAndStatus(warehouseId); + // 按照单据类型 + 单据状态分组 + return convertList(WmsOrderTypeEnum.values(), orderTypeEnum -> { + List statuses = convertList(WmsOrderStatusEnum.values(), statusEnum -> + new WmsHomeOrderStatusRespVO().setStatus(statusEnum.getStatus()) + .setCount(getOrderCount(stats, orderTypeEnum.getType(), statusEnum.getStatus()))); + Long total = getSumValue(statuses, WmsHomeOrderStatusRespVO::getCount, Long::sum, 0L); + return new WmsHomeOrderSummaryRespVO().setType(orderTypeEnum.getType()).setTotal(total).setStatuses(statuses); + }); + } + + @Override + public List getOrderTrend(Integer days, Long warehouseId) { + validateWarehouseIfPresent(warehouseId); + // 统计区间为 [今天 - days + 1, 明天),保证最近 days 天包含今天。 + LocalDate endDate = LocalDate.now().plusDays(1); + LocalDate startDate = endDate.minusDays(days); + LocalDateTime beginTime = startDate.atStartOfDay(); + LocalDateTime endTime = endDate.atStartOfDay(); + // 查询每天的单据数量 + List> dbData = homeStatisticsMapper.selectDailyOrderTrend(beginTime, endTime, warehouseId); + Map> dateMap = new LinkedHashMap<>(); + for (Map row : dbData) { + String date = MapUtil.getStr(row, "date"); + Integer orderType = MapUtil.getInt(row, "orderType"); + Long count = MapUtil.getLong(row, "count", 0L); + dateMap.computeIfAbsent(date, key -> new HashMap<>()).put(orderType, count); + } + // 构造结果,保证每天都有数据 + return convertList(getDateList(startDate, days), d -> { + String dateStr = DatePattern.NORM_DATE_FORMATTER.format(d); + Map row = dateMap.getOrDefault(dateStr, Collections.emptyMap()); + return new WmsHomeOrderTrendRespVO().setTime(d.atStartOfDay()) + .setReceiptCount(row.getOrDefault(WmsOrderTypeEnum.RECEIPT.getType(), 0L)) + .setShipmentCount(row.getOrDefault(WmsOrderTypeEnum.SHIPMENT.getType(), 0L)) + .setMovementCount(row.getOrDefault(WmsOrderTypeEnum.MOVEMENT.getType(), 0L)) + .setCheckCount(row.getOrDefault(WmsOrderTypeEnum.CHECK.getType(), 0L)); + }); + } + + @Override + public WmsHomeInventorySummaryRespVO getInventorySummary(Long warehouseId, Integer goodsLimit, Integer warehouseLimit) { + validateWarehouseIfPresent(warehouseId); + BigDecimal totalQuantity = ObjectUtil.defaultIfNull(homeStatisticsMapper.selectInventoryTotalQuantity(warehouseId), BigDecimal.ZERO); + List> itemRows = homeStatisticsMapper.selectInventoryItemRank(warehouseId, goodsLimit); + List> warehouseRows = homeStatisticsMapper.selectInventoryWarehouseRank(warehouseId, warehouseLimit); + return new WmsHomeInventorySummaryRespVO().setTotalQuantity(totalQuantity) + .setGoodsShareList(buildInventoryItemRankList(itemRows)) + .setWarehouseDistributionList(buildInventoryWarehouseRankList(warehouseRows)); + } + + // ==================== 工具方法 ==================== + + /** + * 仓库编号非空时,校验仓库是否存在,避免前端误传任意 id 直接落到 SQL。 + */ + private void validateWarehouseIfPresent(Long warehouseId) { + if (warehouseId != null) { + warehouseService.validateWarehouseExists(warehouseId); + } + } + + private long getOrderCount(List> stats, Integer orderType, Integer status) { + for (Map stat : stats) { + if (ObjectUtil.equal(MapUtil.getInt(stat, "orderType"), orderType) + && ObjectUtil.equal(MapUtil.getInt(stat, "status"), status)) { + return MapUtil.getLong(stat, "count", 0L); + } + } + return 0L; + } + + private List buildInventoryItemRankList(List> rows) { + return convertList(rows, row -> new WmsHomeInventorySummaryRespVO.ItemRank() + .setId(ObjectUtil.defaultIfNull(MapUtil.getLong(row, "id"), MapUtil.getLong(row, "itemId"))) + .setName(ObjectUtil.defaultIfNull(MapUtil.getStr(row, "name"), MapUtil.getStr(row, "itemName"))) + .setQuantity(getBigDecimal(row, "quantity", BigDecimal.ZERO))); + } + + private List buildInventoryWarehouseRankList(List> rows) { + return convertList(rows, row -> new WmsHomeInventorySummaryRespVO.WarehouseRank() + .setId(ObjectUtil.defaultIfNull(MapUtil.getLong(row, "id"), MapUtil.getLong(row, "warehouseId"))) + .setName(ObjectUtil.defaultIfNull(MapUtil.getStr(row, "name"), MapUtil.getStr(row, "warehouseName"))) + .setQuantity(getBigDecimal(row, "quantity", BigDecimal.ZERO))); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryService.java new file mode 100644 index 000000000..ef2bc2c70 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryService.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.wms.service.inventory; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.history.WmsInventoryHistoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryHistoryDO; + +import java.util.List; + +/** + * WMS 库存流水 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsInventoryHistoryService { + + /** + * 获得库存流水分页 + * + * @param pageReqVO 分页查询 + * @return 库存流水分页 + */ + PageResult getInventoryHistoryPage(WmsInventoryHistoryPageReqVO pageReqVO); + + /** + * 创建库存流水列表 + * + * @param list 库存流水列表 + */ + void createInventoryHistoryList(List list); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryServiceImpl.java new file mode 100644 index 000000000..5feef50d2 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryHistoryServiceImpl.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.wms.service.inventory; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.history.WmsInventoryHistoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryHistoryDO; +import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryHistoryMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +/** + * WMS 库存流水 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsInventoryHistoryServiceImpl implements WmsInventoryHistoryService { + + @Resource + private WmsInventoryHistoryMapper inventoryHistoryMapper; + + @Override + public PageResult getInventoryHistoryPage(WmsInventoryHistoryPageReqVO pageReqVO) { + return inventoryHistoryMapper.selectPage(pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createInventoryHistoryList(List list) { + if (CollUtil.isEmpty(list)) { + return; + } + inventoryHistoryMapper.insertBatch(list); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryService.java new file mode 100644 index 000000000..8eb2c5d50 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryService.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.wms.service.inventory; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDO; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryCheckReqDTO; + +import java.util.List; + +/** + * WMS 库存 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsInventoryService { + + /** + * 获得库存统计分页 + * + * @param pageReqVO 分页查询 + * @return 库存统计分页 + */ + PageResult getInventoryPage(WmsInventoryPageReqVO pageReqVO); + + /** + * 获得库存余额列表 + * + * @param listReqVO 列表查询 + * @return 库存余额列表 + */ + List getInventoryList(WmsInventoryListReqVO listReqVO); + + /** + * 获得指定 SKU 的库存数量 + * + * @param skuId SKU 编号 + * @return 库存数量 + */ + long getInventoryCountBySkuId(Long skuId); + + /** + * 获得指定仓库的库存数量 + * + * @param warehouseId 仓库编号 + * @return 库存数量 + */ + long getInventoryCountByWarehouseId(Long warehouseId); + + /** + * 盘点库存 + * + * @param reqDTO 盘点库存请求 + */ + void checkInventory(WmsInventoryCheckReqDTO reqDTO); + + /** + * 变更库存 + * + * @param reqDTO 库存变更请求 + */ + void changeInventory(WmsInventoryChangeReqDTO reqDTO); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java new file mode 100644 index 000000000..3ff81ad6d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java @@ -0,0 +1,297 @@ +package cn.iocoder.yudao.module.wms.service.inventory; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Tuple; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryHistoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryMapper; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryCheckReqDTO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 库存 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class WmsInventoryServiceImpl implements WmsInventoryService { + + @Resource + private WmsInventoryMapper inventoryMapper; + + @Resource + private WmsInventoryHistoryService inventoryHistoryService; + + @Resource + private WmsItemSkuService itemSkuService; + @Resource + private WmsItemService itemService; + + @Override + public PageResult getInventoryPage(WmsInventoryPageReqVO pageReqVO) { + return inventoryMapper.selectPage(pageReqVO); + } + + @Override + public List getInventoryList(WmsInventoryListReqVO listReqVO) { + return inventoryMapper.selectList(listReqVO); + } + + @Override + public long getInventoryCountBySkuId(Long skuId) { + return inventoryMapper.selectCountBySkuId(skuId); + } + + @Override + public long getInventoryCountByWarehouseId(Long warehouseId) { + return inventoryMapper.selectCountByWarehouseId(warehouseId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void checkInventory(WmsInventoryCheckReqDTO reqDTO) { + if (reqDTO == null || CollUtil.isEmpty(reqDTO.getItems())) { + return; + } + + // 1. 逐条复核账面库存,并计算库存调整和库存流水 + List updateInventories = new ArrayList<>(reqDTO.getItems().size()); + List histories = new ArrayList<>(reqDTO.getItems().size()); + for (WmsInventoryCheckReqDTO.Item item : reqDTO.getItems()) { + // 1.1 锁定或创建库存余额行 + WmsInventoryDO inventory = getOrCreateCheckInventory(item); + // 1.2 无盈亏时不更新库存,也不生成库存流水 + BigDecimal beforeQuantity = inventory.getQuantity(); + BigDecimal afterQuantity = item.getCheckQuantity(); + if (beforeQuantity.compareTo(afterQuantity) == 0) { + continue; + } + updateInventories.add(new WmsInventoryDO().setId(inventory.getId()).setQuantity(afterQuantity)); + histories.add(buildInventoryHistory(reqDTO, item, beforeQuantity, afterQuantity)); + } + + // 2.1 批量更新库存余额 + if (CollUtil.isNotEmpty(updateInventories)) { + inventoryMapper.updateBatch(updateInventories); + } + // 2.2 批量写入库存流水 + if (CollUtil.isNotEmpty(histories)) { + inventoryHistoryService.createInventoryHistoryList(histories); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void changeInventory(WmsInventoryChangeReqDTO reqDTO) { + if (reqDTO == null || CollUtil.isEmpty(reqDTO.getItems())) { + return; + } + + // 1. 补齐并锁定本次涉及的库存余额行,再计算库存变更 + Map resultMap = changeInventoryList(reqDTO.getItems()); + + // 2. 批量写入库存流水 + List histories = new ArrayList<>(reqDTO.getItems().size()); + for (WmsInventoryChangeReqDTO.Item item : reqDTO.getItems()) { + histories.add(buildInventoryHistory(reqDTO, item, resultMap.get(item))); + } + inventoryHistoryService.createInventoryHistoryList(histories); + } + + /** + * 批量变更库存余额 + * + * 1. 库存行按 ID 批量加锁 + * 2. 库存变更先在内存计算并校验,全部通过后批量覆盖库存数量。 + * + * @param items 库存变更明细列表 + * @return 每条变更明细对应的变更前数量、变更后数量 + */ + private Map changeInventoryList(List items) { + // 1.1 创建或锁定库存行 + List inventories = getOrCreateInventoryList(items); + // 1.2 锁定库存:避免 quantity 变更时的并发问题,导致 quantity 前后计算不对 + inventories = inventoryMapper.selectListByIdsForUpdate(convertSet(inventories, WmsInventoryDO::getId)); + + // 2.1 校验库存充足 + Map resultMap = new IdentityHashMap<>(items.size()); + for (WmsInventoryChangeReqDTO.Item item : items) { + WmsInventoryDO inventory = findInventory(inventories, item); + if (inventory == null) { + throw new IllegalStateException("库存行不存在,skuId=" + item.getSkuId() + + ", warehouseId=" + item.getWarehouseId()); + } + BigDecimal beforeQuantity = inventory.getQuantity(); + BigDecimal afterQuantity = beforeQuantity.add(item.getQuantity()); + if (afterQuantity.compareTo(BigDecimal.ZERO) < 0) { + throw buildInventoryQuantityNotEnoughException(item, beforeQuantity); + } + inventory.setQuantity(afterQuantity); + resultMap.put(item, new Tuple(beforeQuantity, afterQuantity)); + } + // 2.2 批量更新库存数量(加锁安全) + if (CollUtil.isNotEmpty(inventories)) { + inventoryMapper.updateBatch(convertList(inventories, inventory -> + new WmsInventoryDO().setId(inventory.getId()).setQuantity(inventory.getQuantity()))); + } + return resultMap; + } + + private List getOrCreateInventoryList(List items) { + // 1.1 先按库存维度在内存去重,避免同一批明细重复查询或重复补行 + List inventoryDimensions = new ArrayList<>(items.size()); + for (WmsInventoryChangeReqDTO.Item item : items) { + if (findInventory(inventoryDimensions, item) == null) { + inventoryDimensions.add(new WmsInventoryDO().setSkuId(item.getSkuId()) + .setWarehouseId(item.getWarehouseId())); + } + } + // 1.2 批量查询已存在的库存行(这里不加锁,后续统一按库存 ID 批量加锁) + List inventories = inventoryMapper.selectListByKeys(inventoryDimensions); + + // 2.1 对比库存维度,找出数据库中还不存在的库存行 + List missingInventories = new ArrayList<>(inventoryDimensions.size()); + for (WmsInventoryDO inventoryDimension : inventoryDimensions) { + if (findInventory(inventories, inventoryDimension) == null) { + missingInventories.add(inventoryDimension); + } + } + // 2.2 如果库存行都已存在,直接返回待加锁库存列表 + if (CollUtil.isEmpty(missingInventories)) { + return inventories; + } + // 2.3 对缺失库存行执行创建;并发冲突时,内部会按唯一索引回查已创建的库存行 + inventories.addAll(createMissingInventoryList(missingInventories)); + return inventories; + } + + private List createMissingInventoryList(List missingInventories) { + List createdInventories = new ArrayList<>(missingInventories.size()); + for (WmsInventoryDO missingInventory : missingInventories) { + WmsInventoryDO inventory = new WmsInventoryDO().setSkuId(missingInventory.getSkuId()) + .setWarehouseId(missingInventory.getWarehouseId()) + .setQuantity(BigDecimal.ZERO); + try { + inventoryMapper.insert(inventory); + } catch (DuplicateKeyException ex) { + inventory = inventoryMapper.selectBySkuIdAndWarehouseId( + missingInventory.getSkuId(), missingInventory.getWarehouseId()); + log.warn("[createMissingInventoryList][missingInventory({}) 插入库存行冲突,回查已有库存行]", missingInventory); + if (inventory == null) { + throw ex; + } + } + createdInventories.add(inventory); + } + return createdInventories; + } + + private WmsInventoryHistoryDO buildInventoryHistory(WmsInventoryChangeReqDTO reqDTO, + WmsInventoryChangeReqDTO.Item item, + Tuple result) { + return new WmsInventoryHistoryDO() + .setWarehouseId(item.getWarehouseId()).setSkuId(item.getSkuId()) + .setQuantity(item.getQuantity()).setBeforeQuantity(result.get(0)).setAfterQuantity(result.get(1)) + .setPrice(item.getPrice()).setTotalPrice(item.getTotalPrice()).setRemark(item.getRemark()) + .setOrderId(reqDTO.getOrderId()).setOrderNo(reqDTO.getOrderNo()).setOrderType(reqDTO.getOrderType()); + } + + private WmsInventoryHistoryDO buildInventoryHistory(WmsInventoryCheckReqDTO reqDTO, + WmsInventoryCheckReqDTO.Item item, + BigDecimal beforeQuantity, + BigDecimal afterQuantity) { + BigDecimal quantity = afterQuantity.subtract(beforeQuantity); + return new WmsInventoryHistoryDO().setWarehouseId(item.getWarehouseId()).setSkuId(item.getSkuId()) + .setQuantity(quantity).setBeforeQuantity(beforeQuantity).setAfterQuantity(afterQuantity) + .setPrice(item.getPrice()).setTotalPrice(MoneyUtils.priceMultiply(item.getPrice(), quantity)) + .setOrderId(reqDTO.getOrderId()).setOrderNo(reqDTO.getOrderNo()).setOrderType(reqDTO.getOrderType()) + .setRemark(item.getRemark()); + } + + private WmsInventoryDO getOrCreateCheckInventory(WmsInventoryCheckReqDTO.Item item) { + if (item.getInventoryId() == null) { + return createCheckInventory(item); + } + WmsInventoryDO inventory = inventoryMapper.selectByIdForUpdate(item.getInventoryId()); + if (inventory == null || !isSameInventory(inventory, item) + || inventory.getQuantity().compareTo(item.getQuantity()) != 0) { + throw exception(CHECK_ORDER_INVENTORY_CHANGED); + } + return inventory; + } + + private WmsInventoryDO createCheckInventory(WmsInventoryCheckReqDTO.Item item) { + WmsInventoryDO inventory = new WmsInventoryDO() + .setSkuId(item.getSkuId()).setWarehouseId(item.getWarehouseId()) + .setQuantity(item.getCheckQuantity()); + try { + inventoryMapper.insert(inventory); + } catch (DuplicateKeyException ex) { + throw exception(CHECK_ORDER_INVENTORY_CHANGED); + } + // 内存中重置为 0,让主循环按 0 -> checkQuantity 生成盘库流水 + inventory.setQuantity(BigDecimal.ZERO); + return inventory; + } + + private static WmsInventoryDO findInventory(List inventories, WmsInventoryChangeReqDTO.Item item) { + return CollUtil.findOne(inventories, inventory -> isSameInventory(inventory, item)); + } + + private static WmsInventoryDO findInventory(List inventories, WmsInventoryDO key) { + return CollUtil.findOne(inventories, inventory -> isSameInventory(inventory, key)); + } + + private static boolean isSameInventory(WmsInventoryDO inventory, WmsInventoryChangeReqDTO.Item item) { + return ObjectUtil.equal(inventory.getSkuId(), item.getSkuId()) + && ObjectUtil.equal(inventory.getWarehouseId(), item.getWarehouseId()); + } + + private static boolean isSameInventory(WmsInventoryDO inventory, WmsInventoryCheckReqDTO.Item item) { + return ObjectUtil.equal(inventory.getSkuId(), item.getSkuId()) + && ObjectUtil.equal(inventory.getWarehouseId(), item.getWarehouseId()); + } + + private static boolean isSameInventory(WmsInventoryDO inventory, WmsInventoryDO key) { + return ObjectUtil.equal(inventory.getSkuId(), key.getSkuId()) + && ObjectUtil.equal(inventory.getWarehouseId(), key.getWarehouseId()); + } + + private ServiceException buildInventoryQuantityNotEnoughException(WmsInventoryChangeReqDTO.Item item, + BigDecimal beforeQuantity) { + WmsItemSkuDO skuDO = itemSkuService.validateItemSkuExists(item.getSkuId()); + WmsItemDO itemDO = itemService.validateItemExists(skuDO.getItemId()); + return exception(INVENTORY_QUANTITY_NOT_ENOUGH, itemDO.getName(), skuDO.getName(), + item.getWarehouseId(), beforeQuantity, item.getQuantity()); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryChangeReqDTO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryChangeReqDTO.java new file mode 100644 index 000000000..79636a666 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryChangeReqDTO.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.wms.service.inventory.dto; + +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * WMS 库存变更请求 DTO + * + * @author 芋道源码 + */ +@Data +public class WmsInventoryChangeReqDTO { + + /** + * 单据编号 + */ + private Long orderId; + /** + * 单据号 + */ + private String orderNo; + /** + * 单据类型 + * + * 枚举 {@link WmsOrderTypeEnum#getType()} + */ + private Integer orderType; + + /** + * 库存变更明细 + */ + private List items; + + /** + * WMS 库存变更明细 + */ + @Data + public static class Item { + + /** + * SKU 编号 + */ + private Long skuId; + /** + * 仓库编号 + */ + private Long warehouseId; + /** + * 变更数量 + */ + private BigDecimal quantity; + + // ========= 单价备注相关字段 ========= + + /** + * 单价 + */ + private BigDecimal price; + /** + * 库存变化金额 + */ + private BigDecimal totalPrice; + /** + * 备注 + */ + private String remark; + + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryCheckReqDTO.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryCheckReqDTO.java new file mode 100644 index 000000000..f7a93e29b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/dto/WmsInventoryCheckReqDTO.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.wms.service.inventory.dto; + +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * WMS 库存盘点请求 DTO + * + * @author 芋道源码 + */ +@Data +public class WmsInventoryCheckReqDTO { + + /** + * 单据编号 + */ + private Long orderId; + /** + * 单据号 + */ + private String orderNo; + /** + * 单据类型 + * + * 枚举 {@link WmsOrderTypeEnum#getType()} + */ + private Integer orderType; + + /** + * 库存盘点明细 + */ + private List items; + + /** + * WMS 库存盘点明细 + */ + @Data + public static class Item { + + /** + * 库存编号 + */ + private Long inventoryId; + /** + * SKU 编号 + */ + private Long skuId; + /** + * 仓库编号 + */ + private Long warehouseId; + /** + * 账面数量 + */ + private BigDecimal quantity; + /** + * 实盘数量 + */ + private BigDecimal checkQuantity; + + // ========= 单价备注相关字段 ========= + + /** + * 单价 + */ + private BigDecimal price; + /** + * 备注 + */ + private String remark; + + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandService.java new file mode 100644 index 000000000..9ca393d5d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandService.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemBrandDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * WMS 商品品牌 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsItemBrandService { + + /** + * 创建商品品牌 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createItemBrand(@Valid WmsItemBrandSaveReqVO createReqVO); + + /** + * 更新商品品牌 + * + * @param updateReqVO 更新信息 + */ + void updateItemBrand(@Valid WmsItemBrandSaveReqVO updateReqVO); + + /** + * 删除商品品牌 + * + * @param id 编号 + */ + void deleteItemBrand(Long id); + + /** + * 校验商品品牌存在 + * + * @param id 编号 + * @return 商品品牌 + */ + WmsItemBrandDO validateItemBrandExists(Long id); + + /** + * 获得商品品牌 + * + * @param id 编号 + * @return 商品品牌 + */ + WmsItemBrandDO getItemBrand(Long id); + + /** + * 获得商品品牌分页 + * + * @param pageReqVO 分页查询 + * @return 商品品牌分页 + */ + PageResult getItemBrandPage(WmsItemBrandPageReqVO pageReqVO); + + /** + * 获得商品品牌列表 + * + * @return 商品品牌列表 + */ + List getItemBrandList(); + + /** + * 按编号集合获得商品品牌列表 + * + * @param ids 编号集合 + * @return 商品品牌列表 + */ + List getItemBrandList(Collection ids); + + /** + * 按编号集合获得商品品牌 Map + * + * @param ids 编号集合 + * @return 商品品牌 Map + */ + default Map getItemBrandMap(Collection ids) { + return convertMap(getItemBrandList(ids), WmsItemBrandDO::getId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImpl.java new file mode 100644 index 000000000..051bd9c4a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImpl.java @@ -0,0 +1,137 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemBrandDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemBrandMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.ITEM_BRAND_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.ITEM_BRAND_HAS_ITEM; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.ITEM_BRAND_NAME_DUPLICATE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.ITEM_BRAND_NOT_EXISTS; + +/** + * WMS 商品品牌 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsItemBrandServiceImpl implements WmsItemBrandService { + + @Resource + private WmsItemBrandMapper brandMapper; + @Resource + private WmsItemService itemService; + + @Override + public Long createItemBrand(WmsItemBrandSaveReqVO createReqVO) { + validateBrandSaveData(null, createReqVO); + + // 新增 + WmsItemBrandDO brand = BeanUtils.toBean(createReqVO, WmsItemBrandDO.class); + brandMapper.insert(brand); + return brand.getId(); + } + + @Override + public void updateItemBrand(WmsItemBrandSaveReqVO updateReqVO) { + // 校验存在 + validateItemBrandExists(updateReqVO.getId()); + validateBrandSaveData(updateReqVO.getId(), updateReqVO); + + // 更新 + WmsItemBrandDO updateObj = BeanUtils.toBean(updateReqVO, WmsItemBrandDO.class); + brandMapper.updateById(updateObj); + } + + private void validateBrandSaveData(Long id, WmsItemBrandSaveReqVO reqVO) { + validateBrandCodeUnique(id, reqVO.getCode()); + validateBrandNameUnique(id, reqVO.getName()); + } + + private void validateBrandCodeUnique(Long id, String code) { + WmsItemBrandDO brand = brandMapper.selectByCode(code); + if (brand == null) { + return; + } + // 如果 id 为空,说明新增;和数据库已有 code 重复 + if (id == null) { + throw exception(ITEM_BRAND_CODE_DUPLICATE); + } + if (ObjectUtil.notEqual(brand.getId(), id)) { + throw exception(ITEM_BRAND_CODE_DUPLICATE); + } + } + + private void validateBrandNameUnique(Long id, String name) { + WmsItemBrandDO brand = brandMapper.selectByName(name); + if (brand == null) { + return; + } + if (id == null) { + throw exception(ITEM_BRAND_NAME_DUPLICATE); + } + if (ObjectUtil.notEqual(brand.getId(), id)) { + throw exception(ITEM_BRAND_NAME_DUPLICATE); + } + } + + @Override + public void deleteItemBrand(Long id) { + // 校验存在 + validateItemBrandExists(id); + // 校验品牌下不存在商品 + if (itemService.getItemCountByBrandId(id) > 0) { + throw exception(ITEM_BRAND_HAS_ITEM); + } + + // 删除 + brandMapper.deleteById(id); + } + + @Override + public WmsItemBrandDO validateItemBrandExists(Long id) { + WmsItemBrandDO brand = brandMapper.selectById(id); + if (brand == null) { + throw exception(ITEM_BRAND_NOT_EXISTS); + } + return brand; + } + + @Override + public WmsItemBrandDO getItemBrand(Long id) { + return brandMapper.selectById(id); + } + + @Override + public PageResult getItemBrandPage(WmsItemBrandPageReqVO pageReqVO) { + return brandMapper.selectPage(pageReqVO); + } + + @Override + public List getItemBrandList() { + return brandMapper.selectList(); + } + + @Override + public List getItemBrandList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return brandMapper.selectByIds(ids); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryService.java new file mode 100644 index 000000000..a036d7e78 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryService.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategoryListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategorySaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemCategoryDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * WMS 商品分类 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsItemCategoryService { + + /** + * 创建商品分类 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createItemCategory(@Valid WmsItemCategorySaveReqVO createReqVO); + + /** + * 更新商品分类 + * + * @param updateReqVO 更新信息 + */ + void updateItemCategory(@Valid WmsItemCategorySaveReqVO updateReqVO); + + /** + * 删除商品分类 + * + * @param id 编号 + */ + void deleteItemCategory(Long id); + + /** + * 校验商品分类存在 + * + * @param id 编号 + * @return 商品分类 + */ + WmsItemCategoryDO validateItemCategoryExists(Long id); + + /** + * 获得商品分类 + * + * @param id 编号 + * @return 商品分类 + */ + WmsItemCategoryDO getItemCategory(Long id); + + /** + * 获得商品分类列表 + * + * @param listReqVO 查询条件 + * @return 商品分类列表 + */ + List getItemCategoryList(WmsItemCategoryListReqVO listReqVO); + + /** + * 按编号集合获得商品分类列表 + * + * @param ids 编号集合 + * @return 商品分类列表 + */ + List getItemCategoryList(Collection ids); + + /** + * 获得指定商品分类及其所有子分类编号集合 + * + * @param id 商品分类编号 + * @return 商品分类编号集合;当 id 为空时,返回 null + */ + Set getSelfAndChildItemCategoryIdList(Long id); + + /** + * 按编号集合获得商品分类 Map + * + * @param ids 编号集合 + * @return 商品分类 Map + */ + default Map getItemCategoryMap(Collection ids) { + return convertMap(getItemCategoryList(ids), WmsItemCategoryDO::getId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryServiceImpl.java new file mode 100644 index 000000000..0f63fdb39 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemCategoryServiceImpl.java @@ -0,0 +1,193 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategoryListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.category.WmsItemCategorySaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemCategoryDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemCategoryMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 商品分类 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsItemCategoryServiceImpl implements WmsItemCategoryService { + + @Resource + private WmsItemCategoryMapper categoryMapper; + + @Resource + private WmsItemService itemService; + + @Override + public Long createItemCategory(WmsItemCategorySaveReqVO createReqVO) { + // 校验数据 + validateCategorySaveData(createReqVO); + + // 插入 + WmsItemCategoryDO category = BeanUtils.toBean(createReqVO, WmsItemCategoryDO.class); + categoryMapper.insert(category); + return category.getId(); + } + + @Override + public void updateItemCategory(WmsItemCategorySaveReqVO updateReqVO) { + // 校验存在 + validateItemCategoryExists(updateReqVO.getId()); + // 校验数据 + validateCategorySaveData(updateReqVO); + + // 更新 + WmsItemCategoryDO updateObj = BeanUtils.toBean(updateReqVO, WmsItemCategoryDO.class); + categoryMapper.updateById(updateObj); + } + + private void validateCategorySaveData(WmsItemCategorySaveReqVO reqVO) { + validateCategoryCodeUnique(reqVO.getId(), reqVO.getCode()); + validateCategoryNameUnique(reqVO.getId(), reqVO.getParentId(), reqVO.getName()); + validateParentCategory(reqVO.getId(), reqVO.getParentId()); + } + + private void validateCategoryCodeUnique(Long id, String code) { + WmsItemCategoryDO category = categoryMapper.selectByCode(code); + if (category == null) { + return; + } + // 如果 id 为空,说明新增;和数据库已有 code 重复 + if (id == null) { + throw exception(ITEM_CATEGORY_CODE_DUPLICATE); + } + if (ObjectUtil.notEqual(category.getId(), id)) { + throw exception(ITEM_CATEGORY_CODE_DUPLICATE); + } + } + + private void validateCategoryNameUnique(Long id, Long parentId, String name) { + WmsItemCategoryDO category = categoryMapper.selectByParentIdAndName(parentId, name); + if (category == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的商品分类 + if (id == null) { + throw exception(ITEM_CATEGORY_NAME_DUPLICATE); + } + if (ObjectUtil.notEqual(category.getId(), id)) { + throw exception(ITEM_CATEGORY_NAME_DUPLICATE); + } + } + + private void validateParentCategory(Long id, Long parentId) { + if (parentId == null || WmsItemCategoryDO.PARENT_ID_ROOT.equals(parentId)) { + return; + } + // 1. 不能设置自己为父分类 + if (ObjectUtil.equal(parentId, id)) { + throw exception(ITEM_CATEGORY_PARENT_ERROR); + } + // 2. 父分类不存在 + WmsItemCategoryDO parentCategory = categoryMapper.selectById(parentId); + if (parentCategory == null) { + throw exception(ITEM_CATEGORY_PARENT_NOT_EXISTS); + } + // 3. 递归校验父分类,如果父分类是自己的子分类,则报错,避免形成环路 + if (id == null) { // id 为空,说明新增,不需要考虑环路 + return; + } + for (int i = 0; i < Short.MAX_VALUE; i++) { + // 3.1 校验环路 + parentId = parentCategory.getParentId(); + if (ObjectUtil.equal(id, parentId)) { + throw exception(ITEM_CATEGORY_PARENT_IS_CHILD); + } + // 3.2 继续递归上级父分类 + if (parentId == null || WmsItemCategoryDO.PARENT_ID_ROOT.equals(parentId)) { + break; + } + parentCategory = categoryMapper.selectById(parentId); + if (parentCategory == null) { + break; + } + } + } + + @Override + public void deleteItemCategory(Long id) { + // 校验存在 + validateItemCategoryExists(id); + // 有子分类不能删 + if (categoryMapper.selectCountByParentId(id) > 0) { + throw exception(ITEM_CATEGORY_HAS_CHILDREN); + } + // 校验分类下不存在商品 + if (itemService.getItemCountByCategoryId(id) > 0) { + throw exception(ITEM_CATEGORY_HAS_ITEM); + } + + // 删除 + categoryMapper.deleteById(id); + } + + @Override + public WmsItemCategoryDO validateItemCategoryExists(Long id) { + WmsItemCategoryDO category = categoryMapper.selectById(id); + if (category == null) { + throw exception(ITEM_CATEGORY_NOT_EXISTS); + } + return category; + } + + @Override + public WmsItemCategoryDO getItemCategory(Long id) { + return categoryMapper.selectById(id); + } + + @Override + public List getItemCategoryList(WmsItemCategoryListReqVO listReqVO) { + return categoryMapper.selectList(listReqVO); + } + + @Override + public List getItemCategoryList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return categoryMapper.selectByIds(ids); + } + + @Override + public Set getSelfAndChildItemCategoryIdList(Long id) { + if (id == null) { + return null; + } + Set ids = new HashSet<>(); + ids.add(id); + Collection parentIds = Collections.singleton(id); + for (int i = 0; i < Short.MAX_VALUE; i++) { + List children = categoryMapper.selectListByParentIds(parentIds); + if (CollUtil.isEmpty(children)) { + break; + } + ids.addAll(convertSet(children, WmsItemCategoryDO::getId)); + parentIds = convertSet(children, WmsItemCategoryDO::getId); + } + return ids; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemService.java new file mode 100644 index 000000000..c7488292a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemService.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * WMS 商品 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsItemService { + + /** + * 创建商品 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createItem(@Valid WmsItemSaveReqVO createReqVO); + + /** + * 更新商品 + * + * @param updateReqVO 更新信息 + */ + void updateItem(@Valid WmsItemSaveReqVO updateReqVO); + + /** + * 删除商品 + * + * @param id 编号 + */ + void deleteItem(Long id); + + /** + * 校验商品存在 + * + * @param id 编号 + * @return 商品 + */ + WmsItemDO validateItemExists(Long id); + + /** + * 获得商品 + * + * @param id 编号 + * @return 商品 + */ + WmsItemDO getItem(Long id); + + /** + * 获得商品分页 + * + * @param pageReqVO 分页查询 + * @return 商品分页 + */ + PageResult getItemPage(WmsItemPageReqVO pageReqVO); + + /** + * 获得商品列表 + * + * @param listReqVO 查询条件 + * @return 商品列表 + */ + List getItemList(WmsItemListReqVO listReqVO); + + /** + * 按编号集合获得商品列表 + * + * @param ids 编号集合 + * @return 商品列表 + */ + List getItemList(Collection ids); + + /** + * 按编号集合获得商品 Map + * + * @param ids 编号集合 + * @return 商品 Map + */ + default Map getItemMap(Collection ids) { + return convertMap(getItemList(ids), WmsItemDO::getId); + } + + /** + * 获得指定商品分类下的商品数量 + * + * @param categoryId 商品分类编号 + * @return 商品数量 + */ + long getItemCountByCategoryId(Long categoryId); + + /** + * 获得指定商品品牌下的商品数量 + * + * @param brandId 商品品牌编号 + * @return 商品数量 + */ + long getItemCountByBrandId(Long brandId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemServiceImpl.java new file mode 100644 index 000000000..191ca86a2 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemServiceImpl.java @@ -0,0 +1,171 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemMapper; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 商品 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsItemServiceImpl implements WmsItemService { + + @Resource + private WmsItemMapper itemMapper; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsItemCategoryService categoryService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsItemBrandService brandService; + @Resource + private WmsItemSkuService itemSkuService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createItem(WmsItemSaveReqVO createReqVO) { + // 校验数据 + validateItemSaveData(createReqVO); + + // 插入商品 + WmsItemDO item = BeanUtils.toBean(createReqVO, WmsItemDO.class); + itemMapper.insert(item); + // 插入 SKU + itemSkuService.createItemSkuList(item.getId(), createReqVO.getSkus()); + return item.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateItem(WmsItemSaveReqVO updateReqVO) { + // 校验存在 + validateItemExists(updateReqVO.getId()); + // 校验数据 + validateItemSaveData(updateReqVO); + + // 更新商品 + WmsItemDO updateObj = BeanUtils.toBean(updateReqVO, WmsItemDO.class); + itemMapper.updateById(updateObj); + // 更新 SKU + itemSkuService.updateItemSkuList(updateReqVO.getId(), updateReqVO.getSkus()); + } + + private void validateItemSaveData(WmsItemSaveReqVO reqVO) { + // 校验商品编号唯一 + validateItemCodeUnique(reqVO.getId(), reqVO.getCode()); + // 校验商品名称唯一 + validateItemNameUnique(reqVO.getId(), reqVO.getName()); + // 校验商品分类存在 + validateCategoryExists(reqVO.getCategoryId()); + // 校验商品品牌存在 + validateBrandExists(reqVO.getBrandId()); + } + + private void validateItemCodeUnique(Long id, String code) { + WmsItemDO item = itemMapper.selectByCode(code); + if (item == null) { + return; + } + if (id == null || ObjectUtil.notEqual(item.getId(), id)) { + throw exception(ITEM_CODE_DUPLICATE); + } + } + + private void validateItemNameUnique(Long id, String name) { + WmsItemDO item = itemMapper.selectByName(name); + if (item == null) { + return; + } + if (id == null || ObjectUtil.notEqual(item.getId(), id)) { + throw exception(ITEM_NAME_DUPLICATE); + } + } + + private void validateCategoryExists(Long categoryId) { + categoryService.validateItemCategoryExists(categoryId); + } + + private void validateBrandExists(Long brandId) { + if (brandId == null) { + return; + } + brandService.validateItemBrandExists(brandId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteItem(Long id) { + // 校验存在 + validateItemExists(id); + + // 删除商品和 SKU + itemSkuService.deleteItemSkuListByItemId(id); + itemMapper.deleteById(id); + } + + @Override + public WmsItemDO validateItemExists(Long id) { + WmsItemDO item = itemMapper.selectById(id); + if (item == null) { + throw exception(ITEM_NOT_EXISTS); + } + return item; + } + + @Override + public WmsItemDO getItem(Long id) { + return itemMapper.selectById(id); + } + + @Override + public PageResult getItemPage(WmsItemPageReqVO pageReqVO) { + return itemMapper.selectPage(pageReqVO, + categoryService.getSelfAndChildItemCategoryIdList(pageReqVO.getCategoryId())); + } + + @Override + public List getItemList(WmsItemListReqVO listReqVO) { + return itemMapper.selectList(listReqVO, + categoryService.getSelfAndChildItemCategoryIdList(listReqVO.getCategoryId())); + } + + @Override + public List getItemList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return itemMapper.selectByIds(ids); + } + + @Override + public long getItemCountByCategoryId(Long categoryId) { + return itemMapper.selectCountByCategoryId(categoryId); + } + + @Override + public long getItemCountByBrandId(Long brandId) { + return itemMapper.selectCountByBrandId(brandId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuService.java new file mode 100644 index 000000000..456742c34 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuService.java @@ -0,0 +1,116 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; + +/** + * WMS 商品 SKU Service 接口 + * + * @author 芋道源码 + */ +public interface WmsItemSkuService { + + /** + * 创建商品 SKU 列表 + * + * @param itemId 商品编号 + * @param skus SKU 列表 + */ + void createItemSkuList(Long itemId, @Valid List skus); + + /** + * 更新商品 SKU 列表 + * + * @param itemId 商品编号 + * @param skus SKU 列表 + */ + void updateItemSkuList(Long itemId, @Valid List skus); + + /** + * 按商品编号删除 SKU 列表 + * + * @param itemId 商品编号 + */ + void deleteItemSkuListByItemId(Long itemId); + + /** + * 校验 SKU 存在 + * + * @param id 编号 + * @return SKU + */ + WmsItemSkuDO validateItemSkuExists(Long id); + + /** + * 按商品编号获得 SKU 列表 + * + * @param itemId 商品编号 + * @return SKU 列表 + */ + List getItemSkuList(Long itemId); + + /** + * 按商品编号集合获得 SKU 列表 + * + * @param itemIds 商品编号集合 + * @return SKU 列表 + */ + List getItemSkuList(Collection itemIds); + + /** + * 获得 SKU 列表 + * + * @param itemIds 商品编号集合 + * @param code SKU 编号 + * @param name SKU 名称 + * @return SKU 列表 + */ + List getItemSkuList(Collection itemIds, String code, String name); + + /** + * 按编号集合获得 SKU 列表 + * + * @param ids 编号集合 + * @return SKU 列表 + */ + List getItemSkuListByIds(Collection ids); + + /** + * 按 SKU 维度分页查询,支持商品 / 品牌 / 分类多表联查筛选。 + * + * @param pageReqVO 分页与筛选条件 + * @return SKU 分页结果 + */ + PageResult getItemSkuPage(WmsItemSkuPageReqVO pageReqVO); + + /** + * 按编号集合获得 SKU Map + * + * @param ids 编号集合 + * @return SKU Map + */ + default Map getItemSkuMap(Collection ids) { + return convertMap(getItemSkuListByIds(ids), WmsItemSkuDO::getId); + } + + /** + * 按商品编号集合获得 SKU MultiMap + * + * @param itemIds 商品编号集合 + * @return 商品编号与 SKU 列表的映射 + */ + default Map> getItemSkuMultiMap(Collection itemIds) { + return convertMultiMap(getItemSkuList(itemIds), WmsItemSkuDO::getItemId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java new file mode 100644 index 000000000..8306fc03f --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemSkuServiceImpl.java @@ -0,0 +1,190 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemSkuMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderDetailService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderDetailService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 商品 SKU Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsItemSkuServiceImpl implements WmsItemSkuService { + + @Resource + private WmsItemSkuMapper itemSkuMapper; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsInventoryService inventoryService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderDetailService receiptOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderDetailService shipmentOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsMovementOrderDetailService movementOrderDetailService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsCheckOrderDetailService checkOrderDetailService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createItemSkuList(Long itemId, List skus) { + validateItemSkuList(skus); + List list = BeanUtils.toBean(skus, WmsItemSkuDO.class, sku -> + sku.setItemId(itemId).setId(null)); // id 由数据库自增分配,避免前端误操作 + itemSkuMapper.insertBatch(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateItemSkuList(Long itemId, List skus) { + validateItemSkuList(skus); + + // 第一步,对比新老数据,获得添加、修改、删除的列表 + List oldList = itemSkuMapper.selectListByItemId(itemId); + List newList = BeanUtils.toBean(skus, WmsItemSkuDO.class); + List> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录 + (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId())); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(2))) { + List deleteSkuIds = convertList(diffList.get(2), WmsItemSkuDO::getId); + // 校验 SKU 是否被使用 + validateItemSkuUnused(diffList.get(2)); + // 删除 SKU + itemSkuMapper.deleteByIds(deleteSkuIds); + } + if (CollUtil.isNotEmpty(diffList.get(0))) { + // 新增场景下忽略前端误传的 SKU id:统一置 null,由数据库自增分配 + diffList.get(0).forEach(sku -> { + sku.setItemId(itemId); + sku.setId(null); + }); + itemSkuMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + diffList.get(1).forEach(sku -> sku.setItemId(itemId)); + itemSkuMapper.updateBatch(diffList.get(1)); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteItemSkuListByItemId(Long itemId) { + // 校验 SKU 是否被使用 + List skus = itemSkuMapper.selectListByItemId(itemId); + validateItemSkuUnused(skus); + // 删除 SKU + itemSkuMapper.deleteByItemId(itemId); + } + + @Override + public WmsItemSkuDO validateItemSkuExists(Long id) { + WmsItemSkuDO sku = itemSkuMapper.selectById(id); + if (sku == null) { + throw exception(ITEM_SKU_NOT_EXISTS); + } + return sku; + } + + @Override + public List getItemSkuList(Long itemId) { + return itemSkuMapper.selectListByItemId(itemId); + } + + @Override + public List getItemSkuList(Collection itemIds) { + return itemSkuMapper.selectListByItemIds(itemIds); + } + + @Override + public List getItemSkuList(Collection itemIds, String code, String name) { + return itemSkuMapper.selectList(itemIds, code, name); + } + + @Override + public List getItemSkuListByIds(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return ListUtil.of(); + } + return itemSkuMapper.selectByIds(ids); + } + + @Override + public PageResult getItemSkuPage(WmsItemSkuPageReqVO pageReqVO) { + return itemSkuMapper.selectPage(pageReqVO); + } + + private void validateItemSkuList(List skus) { + // 校验至少存在一个商品 SKU + if (CollUtil.isEmpty(skus)) { + throw exception(ITEM_SKU_REQUIRED); + } + // 校验 SKU 名称不重复 + Set names = new HashSet<>(); + for (WmsItemSkuSaveReqVO sku : skus) { + if (!names.add(sku.getName())) { + throw exception(ITEM_SKU_NAME_DUPLICATE, sku.getName()); + } + } + } + + private void validateItemSkuUnused(List skus) { + if (CollUtil.isEmpty(skus)) { + return; + } + for (WmsItemSkuDO sku : skus) { + validateItemSkuOrderUnused(sku); + if (inventoryService.getInventoryCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_INVENTORY, sku.getName()); + } + } + } + + private void validateItemSkuOrderUnused(WmsItemSkuDO sku) { + if (receiptOrderDetailService.getReceiptOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), WmsOrderTypeEnum.RECEIPT.getName()); + } + if (shipmentOrderDetailService.getShipmentOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), WmsOrderTypeEnum.SHIPMENT.getName()); + } + if (movementOrderDetailService.getMovementOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), WmsOrderTypeEnum.MOVEMENT.getName()); + } + if (checkOrderDetailService.getCheckOrderDetailCountBySkuId(sku.getId()) > 0) { + throw exception(ITEM_SKU_HAS_ORDER, sku.getName(), WmsOrderTypeEnum.CHECK.getName()); + } + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantService.java new file mode 100644 index 000000000..aa77e2674 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantService.java @@ -0,0 +1,113 @@ +package cn.iocoder.yudao.module.wms.service.md.merchant; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * WMS 往来企业 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsMerchantService { + + /** + * 创建往来企业 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createMerchant(@Valid WmsMerchantSaveReqVO createReqVO); + + /** + * 更新往来企业 + * + * @param updateReqVO 更新信息 + */ + void updateMerchant(@Valid WmsMerchantSaveReqVO updateReqVO); + + /** + * 删除往来企业 + * + * @param id 编号 + */ + void deleteMerchant(Long id); + + /** + * 校验往来企业存在 + * + * @param id 编号 + * @return 往来企业 + */ + WmsMerchantDO validateMerchantExists(Long id); + + /** + * 校验供应商存在 + * + * @param id 编号 + * @return 往来企业 + */ + @SuppressWarnings("UnusedReturnValue") + WmsMerchantDO validateSupplierMerchantExists(Long id); + + /** + * 校验客户存在 + * + * @param id 编号 + * @return 往来企业 + */ + @SuppressWarnings("UnusedReturnValue") + WmsMerchantDO validateCustomerMerchantExists(Long id); + + /** + * 获得往来企业 + * + * @param id 编号 + * @return 往来企业 + */ + WmsMerchantDO getMerchant(Long id); + + /** + * 获得往来企业分页 + * + * @param pageReqVO 分页查询 + * @return 往来企业分页 + */ + PageResult getMerchantPage(WmsMerchantPageReqVO pageReqVO); + + /** + * 获得往来企业列表 + * + * @param listReqVO 查询条件 + * @return 往来企业列表 + */ + List getMerchantList(WmsMerchantListReqVO listReqVO); + + /** + * 按编号集合获得往来企业列表 + * + * @param ids 编号集合 + * @return 往来企业列表 + */ + List getMerchantList(Collection ids); + + /** + * 按编号集合获得往来企业 Map + * + * @param ids 编号集合 + * @return 往来企业 Map + */ + default Map getMerchantMap(Collection ids) { + return convertMap(getMerchantList(ids), WmsMerchantDO::getId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java new file mode 100644 index 000000000..8b4acddf5 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/merchant/WmsMerchantServiceImpl.java @@ -0,0 +1,166 @@ +package cn.iocoder.yudao.module.wms.service.md.merchant; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantListReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo.WmsMerchantSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.merchant.WmsMerchantDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.merchant.WmsMerchantMapper; +import cn.iocoder.yudao.module.wms.enums.md.WmsMerchantTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 往来企业 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsMerchantServiceImpl implements WmsMerchantService { + + @Resource + private WmsMerchantMapper merchantMapper; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderService receiptOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderService shipmentOrderService; + + @Override + public Long createMerchant(WmsMerchantSaveReqVO createReqVO) { + validateMerchantSaveData(null, createReqVO); + + // 新增 + WmsMerchantDO merchant = BeanUtils.toBean(createReqVO, WmsMerchantDO.class); + merchantMapper.insert(merchant); + return merchant.getId(); + } + + @Override + public void updateMerchant(WmsMerchantSaveReqVO updateReqVO) { + // 校验存在 + validateMerchantExists(updateReqVO.getId()); + validateMerchantSaveData(updateReqVO.getId(), updateReqVO); + + // 更新 + WmsMerchantDO updateObj = BeanUtils.toBean(updateReqVO, WmsMerchantDO.class); + merchantMapper.updateById(updateObj); + } + + private void validateMerchantSaveData(Long id, WmsMerchantSaveReqVO reqVO) { + // 校验 code 唯一 + validateMerchantCodeUnique(id, reqVO.getCode()); + // 校验 name 唯一 + validateMerchantNameUnique(id, reqVO.getName()); + } + + private void validateMerchantCodeUnique(Long id, String code) { + WmsMerchantDO merchant = merchantMapper.selectByCode(code); + if (merchant == null) { + return; + } + if (id == null || ObjectUtil.notEqual(merchant.getId(), id)) { + throw exception(MERCHANT_CODE_DUPLICATE); + } + } + + private void validateMerchantNameUnique(Long id, String name) { + WmsMerchantDO merchant = merchantMapper.selectByName(name); + if (merchant == null) { + return; + } + if (id == null || ObjectUtil.notEqual(merchant.getId(), id)) { + throw exception(MERCHANT_NAME_DUPLICATE); + } + } + + @Override + public void deleteMerchant(Long id) { + // 校验存在 + validateMerchantExists(id); + // 校验未被单据使用 + validateMerchantUnused(id); + + // 删除 + merchantMapper.deleteById(id); + } + + private void validateMerchantUnused(Long id) { + if (receiptOrderService.getReceiptOrderCountByMerchantId(id) > 0) { + throw exception(MERCHANT_HAS_ORDER, WmsOrderTypeEnum.RECEIPT.getName()); + } + if (shipmentOrderService.getShipmentOrderCountByMerchantId(id) > 0) { + throw exception(MERCHANT_HAS_ORDER, WmsOrderTypeEnum.SHIPMENT.getName()); + } + } + + @Override + public WmsMerchantDO validateMerchantExists(Long id) { + WmsMerchantDO merchant = merchantMapper.selectById(id); + if (merchant == null) { + throw exception(MERCHANT_NOT_EXISTS); + } + return merchant; + } + + @Override + public WmsMerchantDO validateSupplierMerchantExists(Long id) { + WmsMerchantDO merchant = validateMerchantExists(id); + if (ObjectUtil.notEqual(merchant.getType(), WmsMerchantTypeEnum.SUPPLIER.getType()) + && ObjectUtil.notEqual(merchant.getType(), WmsMerchantTypeEnum.CUSTOMER_SUPPLIER.getType())) { + throw exception(MERCHANT_NOT_SUPPLIER); + } + return merchant; + } + + @Override + public WmsMerchantDO validateCustomerMerchantExists(Long id) { + WmsMerchantDO merchant = validateMerchantExists(id); + if (ObjectUtil.notEqual(merchant.getType(), WmsMerchantTypeEnum.CUSTOMER.getType()) + && ObjectUtil.notEqual(merchant.getType(), WmsMerchantTypeEnum.CUSTOMER_SUPPLIER.getType())) { + throw exception(MERCHANT_NOT_CUSTOMER); + } + return merchant; + } + + @Override + public WmsMerchantDO getMerchant(Long id) { + return merchantMapper.selectById(id); + } + + @Override + public PageResult getMerchantPage(WmsMerchantPageReqVO pageReqVO) { + return merchantMapper.selectPage(pageReqVO); + } + + @Override + public List getMerchantList(WmsMerchantListReqVO listReqVO) { + return merchantMapper.selectList(listReqVO); + } + + @Override + public List getMerchantList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return ListUtil.of(); + } + return merchantMapper.selectByIds(ids); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseService.java new file mode 100644 index 000000000..dd73e7544 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseService.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.wms.service.md.warehouse; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehousePageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehouseSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * WMS 仓库 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsWarehouseService { + + /** + * 创建仓库 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createWarehouse(@Valid WmsWarehouseSaveReqVO createReqVO); + + /** + * 更新仓库 + * + * @param updateReqVO 更新信息 + */ + void updateWarehouse(@Valid WmsWarehouseSaveReqVO updateReqVO); + + /** + * 删除仓库 + * + * @param id 编号 + */ + void deleteWarehouse(Long id); + + /** + * 校验仓库存在 + * + * @param id 编号 + * @return 仓库 + */ + WmsWarehouseDO validateWarehouseExists(Long id); + + /** + * 获得仓库 + * + * @param id 编号 + * @return 仓库 + */ + WmsWarehouseDO getWarehouse(Long id); + + /** + * 获得仓库分页 + * + * @param pageReqVO 分页查询 + * @return 仓库分页 + */ + PageResult getWarehousePage(WmsWarehousePageReqVO pageReqVO); + + /** + * 获得仓库列表 + * + * @return 仓库列表 + */ + List getWarehouseList(); + + /** + * 按编号集合获得仓库列表 + * + * @param ids 编号集合 + * @return 仓库列表 + */ + List getWarehouseList(Collection ids); + + default Map getWarehouseMap(Collection ids) { + return convertMap(getWarehouseList(ids), WmsWarehouseDO::getId); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java new file mode 100644 index 000000000..cdcb69e40 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImpl.java @@ -0,0 +1,166 @@ +package cn.iocoder.yudao.module.wms.service.md.warehouse; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehousePageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehouseSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.warehouse.WmsWarehouseMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 仓库 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsWarehouseServiceImpl implements WmsWarehouseService { + + @Resource + private WmsWarehouseMapper warehouseMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsInventoryService inventoryService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsReceiptOrderService receiptOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsShipmentOrderService shipmentOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsMovementOrderService movementOrderService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private WmsCheckOrderService checkOrderService; + + @Override + public Long createWarehouse(WmsWarehouseSaveReqVO createReqVO) { + // 校验数据 + validateWarehouseSaveData(createReqVO); + + // 插入 + WmsWarehouseDO warehouse = BeanUtils.toBean(createReqVO, WmsWarehouseDO.class); + warehouseMapper.insert(warehouse); + return warehouse.getId(); + } + + @Override + public void updateWarehouse(WmsWarehouseSaveReqVO updateReqVO) { + // 校验存在 + validateWarehouseExists(updateReqVO.getId()); + // 校验数据 + validateWarehouseSaveData(updateReqVO); + + // 更新 + WmsWarehouseDO updateObj = BeanUtils.toBean(updateReqVO, WmsWarehouseDO.class); + warehouseMapper.updateById(updateObj); + } + + private void validateWarehouseSaveData(WmsWarehouseSaveReqVO reqVO) { + validateWarehouseNameUnique(reqVO.getId(), reqVO.getName()); + validateWarehouseCodeUnique(reqVO.getId(), reqVO.getCode()); + } + + private void validateWarehouseNameUnique(Long id, String name) { + WmsWarehouseDO warehouse = warehouseMapper.selectByName(name); + if (warehouse == null) { + return; + } + if (ObjUtil.notEqual(warehouse.getId(), id)) { + throw exception(WAREHOUSE_NAME_DUPLICATE); + } + } + + private void validateWarehouseCodeUnique(Long id, String code) { + WmsWarehouseDO warehouse = warehouseMapper.selectByCode(code); + if (warehouse == null) { + return; + } + if (ObjUtil.notEqual(warehouse.getId(), id)) { + throw exception(WAREHOUSE_CODE_DUPLICATE); + } + } + + @Override + public void deleteWarehouse(Long id) { + // 校验存在 + validateWarehouseExists(id); + // 校验未被单据使用 + validateWarehouseUnused(id); + + // 删除 + warehouseMapper.deleteById(id); + } + + private void validateWarehouseUnused(Long id) { + if (inventoryService.getInventoryCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_INVENTORY); + } + if (receiptOrderService.getReceiptOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, WmsOrderTypeEnum.RECEIPT.getName()); + } + if (shipmentOrderService.getShipmentOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, WmsOrderTypeEnum.SHIPMENT.getName()); + } + if (movementOrderService.getMovementOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, WmsOrderTypeEnum.MOVEMENT.getName()); + } + if (checkOrderService.getCheckOrderCountByWarehouseId(id) > 0) { + throw exception(WAREHOUSE_HAS_ORDER, WmsOrderTypeEnum.CHECK.getName()); + } + } + + @Override + public WmsWarehouseDO validateWarehouseExists(Long id) { + WmsWarehouseDO warehouse = warehouseMapper.selectById(id); + if (warehouse == null) { + throw exception(WAREHOUSE_NOT_EXISTS); + } + return warehouse; + } + + @Override + public WmsWarehouseDO getWarehouse(Long id) { + return warehouseMapper.selectById(id); + } + + @Override + public PageResult getWarehousePage(WmsWarehousePageReqVO pageReqVO) { + return warehouseMapper.selectPage(pageReqVO); + } + + @Override + public List getWarehouseList() { + return warehouseMapper.selectSimpleList(); + } + + @Override + public List getWarehouseList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return warehouseMapper.selectByIds(ids); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java new file mode 100644 index 000000000..023195150 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.wms.service.order.check; + +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 盘库单明细 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsCheckOrderDetailService { + + /** + * 创建盘库单明细列表 + * + * @param orderId 盘库单编号 + * @param reqVO 盘库单保存信息 + */ + void createCheckOrderDetailList(Long orderId, WmsCheckOrderSaveReqVO reqVO); + + /** + * 更新盘库单明细列表 + * + * @param orderId 盘库单编号 + * @param reqVO 盘库单保存信息 + */ + void updateCheckOrderDetailList(Long orderId, WmsCheckOrderSaveReqVO reqVO); + + /** + * 按盘库单编号删除明细列表 + * + * @param orderId 盘库单编号 + */ + void deleteCheckOrderDetailListByOrderId(Long orderId); + + /** + * 按盘库单编号获得明细列表 + * + * @param orderId 盘库单编号 + * @return 明细列表 + */ + List getCheckOrderDetailList(Long orderId); + + /** + * 按盘库单编号集合获得明细列表 + * + * @param orderIds 盘库单编号集合 + * @return 明细列表 + */ + List getCheckOrderDetailList(Collection orderIds); + + /** + * 校验盘库单明细列表存在 + * + * @param orderId 盘库单编号 + * @return 明细列表 + */ + List validateCheckOrderDetailListExists(Long orderId); + + /** + * 获得指定 SKU 的盘库单明细数量 + * + * @param skuId SKU 编号 + * @return 盘库单明细数量 + */ + long getCheckOrderDetailCountBySkuId(Long skuId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java new file mode 100644 index 000000000..6afeebc1e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImpl.java @@ -0,0 +1,120 @@ +package cn.iocoder.yudao.module.wms.service.order.check; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.check.WmsCheckOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 盘库单明细 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsCheckOrderDetailServiceImpl implements WmsCheckOrderDetailService { + + @Resource + private WmsCheckOrderDetailMapper checkOrderDetailMapper; + @Resource + private WmsItemSkuService itemSkuService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createCheckOrderDetailList(Long orderId, WmsCheckOrderSaveReqVO reqVO) { + List list = buildCheckOrderDetailList(reqVO); + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(detail -> detail.setId(null).setOrderId(orderId)); + checkOrderDetailMapper.insertBatch(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCheckOrderDetailList(Long orderId, WmsCheckOrderSaveReqVO reqVO) { + // 第一步,对比新老数据,获得添加、修改、删除的列表 + List oldList = checkOrderDetailMapper.selectListByOrderId(orderId); + List list = buildCheckOrderDetailList(reqVO); + List newList = CollUtil.isEmpty(list) ? ListUtil.of() : list; + List> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录 + (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId())); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + if (CollUtil.isNotEmpty(convertList(diffList.get(0), WmsCheckOrderDetailDO::getId))) { + throw exception(CHECK_ORDER_DETAIL_NOT_EXISTS); + } + diffList.get(0).forEach(detail -> detail.setOrderId(orderId)); + checkOrderDetailMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + diffList.get(1).forEach(detail -> detail.setOrderId(orderId)); + checkOrderDetailMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + checkOrderDetailMapper.deleteByIds(convertList(diffList.get(2), WmsCheckOrderDetailDO::getId)); + } + } + + @Override + public void deleteCheckOrderDetailListByOrderId(Long orderId) { + checkOrderDetailMapper.deleteByOrderId(orderId); + } + + @Override + public List getCheckOrderDetailList(Long orderId) { + return checkOrderDetailMapper.selectListByOrderId(orderId); + } + + @Override + public List getCheckOrderDetailList(Collection orderIds) { + if (CollUtil.isEmpty(orderIds)) { + return ListUtil.of(); + } + return checkOrderDetailMapper.selectListByOrderIds(orderIds); + } + + @Override + public List validateCheckOrderDetailListExists(Long orderId) { + List details = checkOrderDetailMapper.selectListByOrderId(orderId); + if (CollUtil.isEmpty(details)) { + throw exception(CHECK_ORDER_DETAIL_REQUIRED); + } + return details; + } + + @Override + public long getCheckOrderDetailCountBySkuId(Long skuId) { + return checkOrderDetailMapper.selectCountBySkuId(skuId); + } + + private List buildCheckOrderDetailList(WmsCheckOrderSaveReqVO reqVO) { + if (CollUtil.isEmpty(reqVO.getDetails())) { + return ListUtil.of(); + } + return convertList(reqVO.getDetails(), detail -> { + // 校验 SKU 存在 + itemSkuService.validateItemSkuExists(detail.getSkuId()); + // 构建对象 + return BeanUtils.toBean(detail, WmsCheckOrderDetailDO.class).setWarehouseId(reqVO.getWarehouseId()); + }); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java new file mode 100644 index 000000000..659d7c257 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderService.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.wms.service.order.check; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDO; +import jakarta.validation.Valid; + +/** + * WMS 盘库单 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsCheckOrderService { + + /** + * 创建盘库单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createCheckOrder(@Valid WmsCheckOrderSaveReqVO createReqVO); + + /** + * 更新盘库单 + * + * @param updateReqVO 更新信息 + */ + void updateCheckOrder(@Valid WmsCheckOrderSaveReqVO updateReqVO); + + /** + * 删除盘库单 + * + * @param id 编号 + */ + void deleteCheckOrder(Long id); + + /** + * 完成盘库 + * + * @param id 编号 + */ + void completeCheckOrder(Long id); + + /** + * 作废盘库单 + * + * @param id 编号 + */ + void cancelCheckOrder(Long id); + + /** + * 获得盘库单 + * + * @param id 编号 + * @return 盘库单 + */ + WmsCheckOrderDO getCheckOrder(Long id); + + /** + * 获得盘库单分页 + * + * @param pageReqVO 分页查询 + * @return 盘库单分页 + */ + PageResult getCheckOrderPage(WmsCheckOrderPageReqVO pageReqVO); + + /** + * 获得指定仓库的盘库单数量 + * + * @param warehouseId 仓库编号 + * @return 盘库单数量 + */ + long getCheckOrderCountByWarehouseId(Long warehouseId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java new file mode 100644 index 000000000..d7beb83b9 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImpl.java @@ -0,0 +1,222 @@ +package cn.iocoder.yudao.module.wms.service.order.check; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.check.WmsCheckOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryCheckReqDTO; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 盘库单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsCheckOrderServiceImpl implements WmsCheckOrderService { + + @Resource + private WmsCheckOrderMapper checkOrderMapper; + @Resource + private WmsCheckOrderDetailService checkOrderDetailService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsInventoryService inventoryService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createCheckOrder(WmsCheckOrderSaveReqVO createReqVO) { + // 1. 校验盘库单保存数据 + validateCheckOrderSaveData(createReqVO); + + // 2.1 插入盘库单 + WmsCheckOrderDO order = BeanUtils.toBean(createReqVO, WmsCheckOrderDO.class); + order.setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillCheckOrderTotal(order, createReqVO); + checkOrderMapper.insert(order); + // 2.2 插入盘库单明细 + checkOrderDetailService.createCheckOrderDetailList(order.getId(), createReqVO); + return order.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCheckOrder(WmsCheckOrderSaveReqVO updateReqVO) { + // 1. 校验盘库单保存数据 + validateCheckOrderPrepare(updateReqVO.getId()); + validateCheckOrderSaveData(updateReqVO); + + // 2.1 更新盘库单 + WmsCheckOrderDO updateObj = BeanUtils.toBean(updateReqVO, WmsCheckOrderDO.class) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillCheckOrderTotal(updateObj, updateReqVO); + int updateCount = checkOrderMapper.updateByIdAndStatus(updateReqVO.getId(), + WmsOrderStatusEnum.PREPARE.getStatus(), updateObj); + if (updateCount == 0) { + throw exception(CHECK_ORDER_STATUS_NOT_PREPARE); + } + // 2.2 更新盘库单明细 + checkOrderDetailService.updateCheckOrderDetailList(updateReqVO.getId(), updateReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCheckOrder(Long id) { + // 1. 校验存在,且可删除 + WmsCheckOrderDO order = validateCheckOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus()) + && ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.CANCELED.getStatus())) { + throw exception(CHECK_ORDER_STATUS_NOT_DELETABLE); + } + + // 2.1 删除盘库单 + checkOrderMapper.deleteById(id); + // 2.2 删除盘库单明细 + checkOrderDetailService.deleteCheckOrderDetailListByOrderId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeCheckOrder(Long id) { + // 1.1 校验存在,且草稿 + WmsCheckOrderDO order = validateCheckOrderPrepare(id); + // 1.2 校验盘库单明细存在 + List details = checkOrderDetailService.validateCheckOrderDetailListExists(id); + + // 2. 完成盘库单 + if (checkOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsCheckOrderDO().setStatus(WmsOrderStatusEnum.FINISHED.getStatus())) == 0) { + throw exception(CHECK_ORDER_STATUS_NOT_PREPARE); + } + + // 3. 盘点库存 + inventoryService.checkInventory(buildCheckInventoryReqDTO(order, details)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelCheckOrder(Long id) { + // 1. 校验存在,且草稿 + validateCheckOrderPrepare(id); + + // 2. 作废盘库单 + if (checkOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsCheckOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())) == 0) { + throw exception(CHECK_ORDER_STATUS_NOT_PREPARE); + } + } + + @Override + public WmsCheckOrderDO getCheckOrder(Long id) { + return checkOrderMapper.selectById(id); + } + + @Override + public PageResult getCheckOrderPage(WmsCheckOrderPageReqVO pageReqVO) { + return checkOrderMapper.selectPage(pageReqVO); + } + + @Override + public long getCheckOrderCountByWarehouseId(Long warehouseId) { + return checkOrderMapper.selectCountByWarehouseId(warehouseId); + } + + private void validateCheckOrderSaveData(WmsCheckOrderSaveReqVO reqVO) { + // 校验盘库单号唯一 + validateCheckOrderNoUnique(reqVO.getId(), reqVO.getNo()); + // 校验仓库存在 + warehouseService.validateWarehouseExists(reqVO.getWarehouseId()); + } + + private void validateCheckOrderNoUnique(Long id, String no) { + WmsCheckOrderDO order = checkOrderMapper.selectByNo(no); + if (order == null) { + return; + } + if (id == null || ObjectUtil.notEqual(order.getId(), id)) { + throw exception(CHECK_ORDER_NO_DUPLICATE); + } + } + + private void fillCheckOrderTotal(WmsCheckOrderDO order, WmsCheckOrderSaveReqVO reqVO) { + BigDecimal totalQuantity = BigDecimal.ZERO; + BigDecimal totalPrice = BigDecimal.ZERO; + BigDecimal actualPrice = BigDecimal.ZERO; + if (CollUtil.isNotEmpty(reqVO.getDetails())) { + for (WmsCheckOrderDetailSaveReqVO detail : reqVO.getDetails()) { + BigDecimal differenceQuantity = calculateDifferenceQuantity(detail.getQuantity(), detail.getCheckQuantity()); + totalQuantity = totalQuantity.add(differenceQuantity); + if (detail.getPrice() == null) { + continue; + } + if (detail.getQuantity() != null) { + totalPrice = totalPrice.add(MoneyUtils.priceMultiply(detail.getPrice(), detail.getQuantity())); + } + if (detail.getCheckQuantity() != null) { + actualPrice = actualPrice.add(MoneyUtils.priceMultiply(detail.getPrice(), detail.getCheckQuantity())); + } + } + } + order.setTotalQuantity(totalQuantity).setTotalPrice(totalPrice).setActualPrice(actualPrice); + } + + private BigDecimal calculateDifferenceQuantity(BigDecimal quantity, BigDecimal checkQuantity) { + if (quantity == null || checkQuantity == null) { + return BigDecimal.ZERO; + } + return checkQuantity.subtract(quantity); + } + + private WmsCheckOrderDO validateCheckOrderExists(Long id) { + WmsCheckOrderDO order = id == null ? null : checkOrderMapper.selectById(id); + if (order == null) { + throw exception(CHECK_ORDER_NOT_EXISTS); + } + return order; + } + + /** + * 校验盘库单存在且为草稿状态 + * + * @param id 盘库单编号 + * @return 盘库单 + */ + private WmsCheckOrderDO validateCheckOrderPrepare(Long id) { + WmsCheckOrderDO order = validateCheckOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus())) { + throw exception(CHECK_ORDER_STATUS_NOT_PREPARE); + } + return order; + } + + private WmsInventoryCheckReqDTO buildCheckInventoryReqDTO(WmsCheckOrderDO order, + List details) { + return new WmsInventoryCheckReqDTO() + .setOrderId(order.getId()).setOrderNo(order.getNo()) + .setOrderType(WmsOrderTypeEnum.CHECK.getType()) + .setItems(BeanUtils.toBean(details, WmsInventoryCheckReqDTO.Item.class)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java new file mode 100644 index 000000000..7675ee7eb --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.wms.service.order.movement; + +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 移库单明细 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsMovementOrderDetailService { + + /** + * 创建移库单明细列表 + * + * @param orderId 移库单编号 + * @param reqVO 移库单保存信息 + */ + void createMovementOrderDetailList(Long orderId, WmsMovementOrderSaveReqVO reqVO); + + /** + * 更新移库单明细列表 + * + * @param orderId 移库单编号 + * @param reqVO 移库单保存信息 + */ + void updateMovementOrderDetailList(Long orderId, WmsMovementOrderSaveReqVO reqVO); + + /** + * 按移库单编号删除明细列表 + * + * @param orderId 移库单编号 + */ + void deleteMovementOrderDetailListByOrderId(Long orderId); + + /** + * 按移库单编号获得明细列表 + * + * @param orderId 移库单编号 + * @return 明细列表 + */ + List getMovementOrderDetailList(Long orderId); + + /** + * 按移库单编号集合获得明细列表 + * + * @param orderIds 移库单编号集合 + * @return 明细列表 + */ + List getMovementOrderDetailList(Collection orderIds); + + /** + * 校验移库单明细列表存在 + * + * @param orderId 移库单编号 + * @return 明细列表 + */ + List validateMovementOrderDetailListExists(Long orderId); + + /** + * 获得指定 SKU 的移库单明细数量 + * + * @param skuId SKU 编号 + * @return 移库单明细数量 + */ + long getMovementOrderDetailCountBySkuId(Long skuId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java new file mode 100644 index 000000000..e5b4bb025 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImpl.java @@ -0,0 +1,132 @@ +package cn.iocoder.yudao.module.wms.service.order.movement; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.movement.WmsMovementOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 移库单明细 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsMovementOrderDetailServiceImpl implements WmsMovementOrderDetailService { + + @Resource + private WmsMovementOrderDetailMapper movementOrderDetailMapper; + @Resource + private WmsItemSkuService itemSkuService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createMovementOrderDetailList(Long orderId, WmsMovementOrderSaveReqVO reqVO) { + List list = buildMovementOrderDetailList(reqVO); + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(detail -> detail.setId(null).setOrderId(orderId)); + movementOrderDetailMapper.insertBatch(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMovementOrderDetailList(Long orderId, WmsMovementOrderSaveReqVO reqVO) { + // 第一步,对比新老数据,获得添加、修改、删除的列表 + List oldList = movementOrderDetailMapper.selectListByOrderId(orderId); + List list = buildMovementOrderDetailList(reqVO); + List newList = CollUtil.isEmpty(list) ? ListUtil.of() : list; + List> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录 + (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId())); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + if (CollUtil.isNotEmpty(convertList(diffList.get(0), WmsMovementOrderDetailDO::getId))) { + throw exception(MOVEMENT_ORDER_DETAIL_NOT_EXISTS); + } + diffList.get(0).forEach(detail -> detail.setOrderId(orderId)); + movementOrderDetailMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + diffList.get(1).forEach(detail -> detail.setOrderId(orderId)); + movementOrderDetailMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + movementOrderDetailMapper.deleteByIds(convertList(diffList.get(2), WmsMovementOrderDetailDO::getId)); + } + } + + @Override + public void deleteMovementOrderDetailListByOrderId(Long orderId) { + movementOrderDetailMapper.deleteByOrderId(orderId); + } + + @Override + public List getMovementOrderDetailList(Long orderId) { + return movementOrderDetailMapper.selectListByOrderId(orderId); + } + + @Override + public List getMovementOrderDetailList(Collection orderIds) { + if (CollUtil.isEmpty(orderIds)) { + return ListUtil.of(); + } + return movementOrderDetailMapper.selectListByOrderIds(orderIds); + } + + @Override + public List validateMovementOrderDetailListExists(Long orderId) { + List details = movementOrderDetailMapper.selectListByOrderId(orderId); + if (CollUtil.isEmpty(details)) { + throw exception(MOVEMENT_ORDER_DETAIL_REQUIRED); + } + return details; + } + + @Override + public long getMovementOrderDetailCountBySkuId(Long skuId) { + return movementOrderDetailMapper.selectCountBySkuId(skuId); + } + + private List buildMovementOrderDetailList(WmsMovementOrderSaveReqVO reqVO) { + if (CollUtil.isEmpty(reqVO.getDetails())) { + return ListUtil.of(); + } + return convertList(reqVO.getDetails(), detail -> { + // 校验 SKU 存在 + itemSkuService.validateItemSkuExists(detail.getSkuId()); + // 构建对象 + WmsMovementOrderDetailDO detailDO = BeanUtils.toBean(detail, WmsMovementOrderDetailDO.class) + .setSourceWarehouseId(reqVO.getSourceWarehouseId()) + .setTargetWarehouseId(reqVO.getTargetWarehouseId()); + fillDetailTotalPrice(detailDO); + return detailDO; + }); + } + + private static void fillDetailTotalPrice(WmsMovementOrderDetailDO detail) { + if (detail.getTotalPrice() != null || detail.getQuantity() == null || detail.getPrice() == null) { + return; + } + detail.setTotalPrice(MoneyUtils.priceMultiply(detail.getPrice(), detail.getQuantity())); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java new file mode 100644 index 000000000..52fbf7176 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderService.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.wms.service.order.movement; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDO; +import jakarta.validation.Valid; + +/** + * WMS 移库单 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsMovementOrderService { + + /** + * 创建移库单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createMovementOrder(@Valid WmsMovementOrderSaveReqVO createReqVO); + + /** + * 更新移库单 + * + * @param updateReqVO 更新信息 + */ + void updateMovementOrder(@Valid WmsMovementOrderSaveReqVO updateReqVO); + + /** + * 删除移库单 + * + * @param id 编号 + */ + void deleteMovementOrder(Long id); + + /** + * 完成移库 + * + * @param id 编号 + */ + void completeMovementOrder(Long id); + + /** + * 作废移库单 + * + * @param id 编号 + */ + void cancelMovementOrder(Long id); + + /** + * 获得移库单 + * + * @param id 编号 + * @return 移库单 + */ + WmsMovementOrderDO getMovementOrder(Long id); + + /** + * 获得移库单分页 + * + * @param pageReqVO 分页查询 + * @return 移库单分页 + */ + PageResult getMovementOrderPage(WmsMovementOrderPageReqVO pageReqVO); + + /** + * 获得指定仓库的移库单数量 + * + * @param warehouseId 仓库编号 + * @return 移库单数量 + */ + long getMovementOrderCountByWarehouseId(Long warehouseId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java new file mode 100644 index 000000000..ee2613e10 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImpl.java @@ -0,0 +1,243 @@ +package cn.iocoder.yudao.module.wms.service.order.movement; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.movement.WmsMovementOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 移库单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsMovementOrderServiceImpl implements WmsMovementOrderService { + + @Resource + private WmsMovementOrderMapper movementOrderMapper; + @Resource + private WmsMovementOrderDetailService movementOrderDetailService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsInventoryService inventoryService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createMovementOrder(WmsMovementOrderSaveReqVO createReqVO) { + // 1. 校验移库单保存数据 + validateMovementOrderSaveData(createReqVO); + + // 2.1 插入移库单 + WmsMovementOrderDO order = BeanUtils.toBean(createReqVO, WmsMovementOrderDO.class); + order.setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillMovementOrderTotal(order, createReqVO); + movementOrderMapper.insert(order); + // 2.2 插入移库单明细 + movementOrderDetailService.createMovementOrderDetailList(order.getId(), createReqVO); + return order.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMovementOrder(WmsMovementOrderSaveReqVO updateReqVO) { + // 1. 校验移库单保存数据 + validateMovementOrderPrepare(updateReqVO.getId()); + validateMovementOrderSaveData(updateReqVO); + + // 2.1 更新移库单 + WmsMovementOrderDO updateObj = BeanUtils.toBean(updateReqVO, WmsMovementOrderDO.class) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillMovementOrderTotal(updateObj, updateReqVO); + int updateCount = movementOrderMapper.updateByIdAndStatus(updateReqVO.getId(), + WmsOrderStatusEnum.PREPARE.getStatus(), updateObj); + if (updateCount == 0) { + throw exception(MOVEMENT_ORDER_STATUS_NOT_PREPARE); + } + // 2.2 更新移库单明细 + movementOrderDetailService.updateMovementOrderDetailList(updateReqVO.getId(), updateReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteMovementOrder(Long id) { + // 1. 校验存在,且可删除 + WmsMovementOrderDO order = validateMovementOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus()) + && ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.CANCELED.getStatus())) { + throw exception(MOVEMENT_ORDER_STATUS_NOT_DELETABLE); + } + + // 2.1 删除移库单 + movementOrderMapper.deleteById(id); + // 2.2 删除移库单明细 + movementOrderDetailService.deleteMovementOrderDetailListByOrderId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeMovementOrder(Long id) { + // 1.1 校验存在,且草稿 + WmsMovementOrderDO order = validateMovementOrderPrepare(id); + // 1.2 校验移库单明细存在 + List details = movementOrderDetailService.validateMovementOrderDetailListExists(id); + + // 2. 完成移库单 + if (movementOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsMovementOrderDO().setStatus(WmsOrderStatusEnum.FINISHED.getStatus())) == 0) { + throw exception(MOVEMENT_ORDER_STATUS_NOT_PREPARE); + } + + // 3. 移动库存 + changeInventory(order, details); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelMovementOrder(Long id) { + // 1. 校验存在,且草稿 + validateMovementOrderPrepare(id); + + // 2. 作废移库单 + if (movementOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsMovementOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())) == 0) { + throw exception(MOVEMENT_ORDER_STATUS_NOT_PREPARE); + } + } + + @Override + public WmsMovementOrderDO getMovementOrder(Long id) { + return movementOrderMapper.selectById(id); + } + + @Override + public PageResult getMovementOrderPage(WmsMovementOrderPageReqVO pageReqVO) { + return movementOrderMapper.selectPage(pageReqVO); + } + + @Override + public long getMovementOrderCountByWarehouseId(Long warehouseId) { + return movementOrderMapper.selectCountByWarehouseId(warehouseId); + } + + private void validateMovementOrderSaveData(WmsMovementOrderSaveReqVO reqVO) { + // 校验移库单号唯一 + validateMovementOrderNoUnique(reqVO.getId(), reqVO.getNo()); + // 校验仓库存在 + warehouseService.validateWarehouseExists(reqVO.getSourceWarehouseId()); + warehouseService.validateWarehouseExists(reqVO.getTargetWarehouseId()); + // 校验来源和目标不能相同 + if (ObjectUtil.equal(reqVO.getSourceWarehouseId(), reqVO.getTargetWarehouseId())) { + throw exception(MOVEMENT_ORDER_WAREHOUSE_SAME); + } + } + + private void validateMovementOrderNoUnique(Long id, String no) { + WmsMovementOrderDO order = movementOrderMapper.selectByNo(no); + if (order == null) { + return; + } + if (id == null || ObjectUtil.notEqual(order.getId(), id)) { + throw exception(MOVEMENT_ORDER_NO_DUPLICATE); + } + } + + private void fillMovementOrderTotal(WmsMovementOrderDO order, WmsMovementOrderSaveReqVO reqVO) { + BigDecimal totalQuantity = BigDecimal.ZERO; + BigDecimal totalPrice = BigDecimal.ZERO; + if (CollUtil.isNotEmpty(reqVO.getDetails())) { + for (WmsMovementOrderDetailSaveReqVO detail : reqVO.getDetails()) { + if (detail.getQuantity() != null) { + totalQuantity = totalQuantity.add(detail.getQuantity()); + } + BigDecimal detailTotalPrice = getDetailTotalPrice(detail.getQuantity(), detail.getPrice(), + detail.getTotalPrice()); + if (detailTotalPrice != null) { + totalPrice = totalPrice.add(detailTotalPrice); + } + } + } + order.setTotalQuantity(totalQuantity).setTotalPrice(totalPrice); + } + + private static BigDecimal getDetailTotalPrice(BigDecimal quantity, BigDecimal price, BigDecimal totalPrice) { + if (totalPrice != null) { + return totalPrice; + } + return MoneyUtils.priceMultiply(price, quantity); + } + + private WmsMovementOrderDO validateMovementOrderExists(Long id) { + WmsMovementOrderDO order = id == null ? null : movementOrderMapper.selectById(id); + if (order == null) { + throw exception(MOVEMENT_ORDER_NOT_EXISTS); + } + return order; + } + + /** + * 校验移库单存在且为草稿状态 + * + * @param id 移库单编号 + * @return 移库单 + */ + private WmsMovementOrderDO validateMovementOrderPrepare(Long id) { + WmsMovementOrderDO order = validateMovementOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus())) { + throw exception(MOVEMENT_ORDER_STATUS_NOT_PREPARE); + } + return order; + } + + /** + * 移动移库单对应库存 + * + * @param order 移库单 + * @param details 移库单明细列表 + */ + private void changeInventory(WmsMovementOrderDO order, List details) { + List items = new ArrayList<>(details.size() * 2); + for (WmsMovementOrderDetailDO detail : details) { + BigDecimal detailTotalPrice = getDetailTotalPrice(detail.getQuantity(), detail.getPrice(), + detail.getTotalPrice()); + items.add(BeanUtils.toBean(detail, WmsInventoryChangeReqDTO.Item.class) + .setWarehouseId(detail.getSourceWarehouseId()).setQuantity(detail.getQuantity().negate()) + .setTotalPrice(negate(detailTotalPrice))); + items.add(BeanUtils.toBean(detail, WmsInventoryChangeReqDTO.Item.class) + .setWarehouseId(detail.getTargetWarehouseId()).setQuantity(detail.getQuantity()) + .setTotalPrice(detailTotalPrice)); + } + inventoryService.changeInventory(new WmsInventoryChangeReqDTO() + .setOrderId(order.getId()).setOrderNo(order.getNo()) + .setOrderType(WmsOrderTypeEnum.MOVEMENT.getType()).setItems(items)); + } + + private static BigDecimal negate(BigDecimal value) { + return value == null ? null : value.negate(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java new file mode 100644 index 000000000..ba67d7b37 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.wms.service.order.receipt; + +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 入库单明细 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsReceiptOrderDetailService { + + /** + * 创建入库单明细列表 + * + * @param orderId 入库单编号 + * @param reqVO 入库单保存信息 + */ + void createReceiptOrderDetailList(Long orderId, WmsReceiptOrderSaveReqVO reqVO); + + /** + * 更新入库单明细列表 + * + * @param orderId 入库单编号 + * @param reqVO 入库单保存信息 + */ + void updateReceiptOrderDetailList(Long orderId, WmsReceiptOrderSaveReqVO reqVO); + + /** + * 按入库单编号删除明细列表 + * + * @param orderId 入库单编号 + */ + void deleteReceiptOrderDetailListByOrderId(Long orderId); + + /** + * 按入库单编号获得明细列表 + * + * @param orderId 入库单编号 + * @return 明细列表 + */ + List getReceiptOrderDetailList(Long orderId); + + /** + * 按入库单编号集合获得明细列表 + * + * @param orderIds 入库单编号集合 + * @return 明细列表 + */ + List getReceiptOrderDetailList(Collection orderIds); + + /** + * 校验入库单明细列表存在 + * + * @param orderId 入库单编号 + * @return 明细列表 + */ + List validateReceiptOrderDetailListExists(Long orderId); + + /** + * 获得指定 SKU 的入库单明细数量 + * + * @param skuId SKU 编号 + * @return 入库单明细数量 + */ + long getReceiptOrderDetailCountBySkuId(Long skuId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java new file mode 100644 index 000000000..67ae8d03e --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImpl.java @@ -0,0 +1,131 @@ +package cn.iocoder.yudao.module.wms.service.order.receipt; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.receipt.WmsReceiptOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 入库单明细 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsReceiptOrderDetailServiceImpl implements WmsReceiptOrderDetailService { + + @Resource + private WmsReceiptOrderDetailMapper receiptOrderDetailMapper; + @Resource + private WmsItemSkuService itemSkuService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createReceiptOrderDetailList(Long orderId, WmsReceiptOrderSaveReqVO reqVO) { + List list = buildReceiptOrderDetailList(reqVO); + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(detail -> detail.setId(null).setOrderId(orderId)); + receiptOrderDetailMapper.insertBatch(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateReceiptOrderDetailList(Long orderId, WmsReceiptOrderSaveReqVO reqVO) { + // 第一步,对比新老数据,获得添加、修改、删除的列表 + List oldList = receiptOrderDetailMapper.selectListByOrderId(orderId); + List list = buildReceiptOrderDetailList(reqVO); + List newList = CollUtil.isEmpty(list) ? ListUtil.of() : list; + List> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录 + (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId())); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + if (CollUtil.isNotEmpty(convertList(diffList.get(0), WmsReceiptOrderDetailDO::getId))) { + throw exception(RECEIPT_ORDER_DETAIL_NOT_EXISTS); + } + diffList.get(0).forEach(detail -> detail.setOrderId(orderId)); + receiptOrderDetailMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + diffList.get(1).forEach(detail -> detail.setOrderId(orderId)); + receiptOrderDetailMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + receiptOrderDetailMapper.deleteByIds(convertList(diffList.get(2), WmsReceiptOrderDetailDO::getId)); + } + } + + @Override + public void deleteReceiptOrderDetailListByOrderId(Long orderId) { + receiptOrderDetailMapper.deleteByOrderId(orderId); + } + + @Override + public List getReceiptOrderDetailList(Long orderId) { + return receiptOrderDetailMapper.selectListByOrderId(orderId); + } + + @Override + public List getReceiptOrderDetailList(Collection orderIds) { + if (CollUtil.isEmpty(orderIds)) { + return ListUtil.of(); + } + return receiptOrderDetailMapper.selectListByOrderIds(orderIds); + } + + @Override + public List validateReceiptOrderDetailListExists(Long orderId) { + List details = receiptOrderDetailMapper.selectListByOrderId(orderId); + if (CollUtil.isEmpty(details)) { + throw exception(RECEIPT_ORDER_DETAIL_REQUIRED); + } + return details; + } + + @Override + public long getReceiptOrderDetailCountBySkuId(Long skuId) { + return receiptOrderDetailMapper.selectCountBySkuId(skuId); + } + + private List buildReceiptOrderDetailList(WmsReceiptOrderSaveReqVO reqVO) { + if (CollUtil.isEmpty(reqVO.getDetails())) { + return ListUtil.of(); + } + return convertList(reqVO.getDetails(), detail -> { + // 校验 SKU 存在 + itemSkuService.validateItemSkuExists(detail.getSkuId()); + // 构建对象 + WmsReceiptOrderDetailDO detailDO = BeanUtils.toBean(detail, WmsReceiptOrderDetailDO.class) + .setWarehouseId(reqVO.getWarehouseId()); + fillDetailTotalPrice(detailDO); + return detailDO; + }); + } + + private static void fillDetailTotalPrice(WmsReceiptOrderDetailDO detail) { + if (detail.getTotalPrice() != null || detail.getQuantity() == null || detail.getPrice() == null) { + return; + } + detail.setTotalPrice(MoneyUtils.priceMultiply(detail.getPrice(), detail.getQuantity())); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java new file mode 100644 index 000000000..4223bfb11 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderService.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.wms.service.order.receipt; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDO; +import jakarta.validation.Valid; + +/** + * WMS 入库单 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsReceiptOrderService { + + /** + * 创建入库单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createReceiptOrder(@Valid WmsReceiptOrderSaveReqVO createReqVO); + + /** + * 更新入库单 + * + * @param updateReqVO 更新信息 + */ + void updateReceiptOrder(@Valid WmsReceiptOrderSaveReqVO updateReqVO); + + /** + * 删除入库单 + * + * @param id 编号 + */ + void deleteReceiptOrder(Long id); + + /** + * 完成入库 + * + * @param id 编号 + */ + void completeReceiptOrder(Long id); + + /** + * 作废入库单 + * + * @param id 编号 + */ + void cancelReceiptOrder(Long id); + + /** + * 获得入库单 + * + * @param id 编号 + * @return 入库单 + */ + WmsReceiptOrderDO getReceiptOrder(Long id); + + /** + * 获得入库单分页 + * + * @param pageReqVO 分页查询 + * @return 入库单分页 + */ + PageResult getReceiptOrderPage(WmsReceiptOrderPageReqVO pageReqVO); + + /** + * 获得指定往来企业的入库单数量 + * + * @param merchantId 往来企业编号 + * @return 入库单数量 + */ + long getReceiptOrderCountByMerchantId(Long merchantId); + + /** + * 获得指定仓库的入库单数量 + * + * @param warehouseId 仓库编号 + * @return 入库单数量 + */ + long getReceiptOrderCountByWarehouseId(Long warehouseId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java new file mode 100644 index 000000000..70ac13c4c --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImpl.java @@ -0,0 +1,241 @@ +package cn.iocoder.yudao.module.wms.service.order.receipt; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.receipt.WmsReceiptOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.md.merchant.WmsMerchantService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 入库单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsReceiptOrderServiceImpl implements WmsReceiptOrderService { + + @Resource + private WmsReceiptOrderMapper receiptOrderMapper; + @Resource + private WmsReceiptOrderDetailService receiptOrderDetailService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsMerchantService merchantService; + @Resource + private WmsInventoryService inventoryService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createReceiptOrder(WmsReceiptOrderSaveReqVO createReqVO) { + // 1. 校验入库单保存数据 + validateReceiptOrderSaveData(createReqVO); + + // 2.1 插入入库单 + WmsReceiptOrderDO order = BeanUtils.toBean(createReqVO, WmsReceiptOrderDO.class); + order.setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillReceiptOrderTotal(order, createReqVO); + receiptOrderMapper.insert(order); + // 2.2 插入入库单明细 + receiptOrderDetailService.createReceiptOrderDetailList(order.getId(), createReqVO); + return order.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateReceiptOrder(WmsReceiptOrderSaveReqVO updateReqVO) { + // 1. 校验入库单保存数据 + validateReceiptOrderPrepare(updateReqVO.getId()); + validateReceiptOrderSaveData(updateReqVO); + + // 2.1 更新入库单 + WmsReceiptOrderDO updateObj = BeanUtils.toBean(updateReqVO, WmsReceiptOrderDO.class) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillReceiptOrderTotal(updateObj, updateReqVO); + int updateCount = receiptOrderMapper.updateByIdAndStatus(updateReqVO.getId(), + WmsOrderStatusEnum.PREPARE.getStatus(), updateObj); + if (updateCount == 0) { + throw exception(RECEIPT_ORDER_STATUS_NOT_PREPARE); + } + // 2.2 更新入库单明细 + receiptOrderDetailService.updateReceiptOrderDetailList(updateReqVO.getId(), updateReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteReceiptOrder(Long id) { + // 1. 校验存在,且可删除 + WmsReceiptOrderDO order = validateReceiptOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus()) + && ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.CANCELED.getStatus())) { + throw exception(RECEIPT_ORDER_STATUS_NOT_DELETABLE); + } + + // 2.1 删除入库单 + receiptOrderMapper.deleteById(id); + // 2.2 删除入库单明细 + receiptOrderDetailService.deleteReceiptOrderDetailListByOrderId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeReceiptOrder(Long id) { + // 1.1 校验存在,且草稿 + WmsReceiptOrderDO order = validateReceiptOrderPrepare(id); + // 1.2 校验入库单明细存在 + List details = receiptOrderDetailService.validateReceiptOrderDetailListExists(id); + + // 2. 完成入库单 + if (receiptOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsReceiptOrderDO().setStatus(WmsOrderStatusEnum.FINISHED.getStatus())) == 0) { + throw exception(RECEIPT_ORDER_STATUS_NOT_PREPARE); + } + + // 3. 写入库存 + createInventory(order, details); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelReceiptOrder(Long id) { + // 1. 校验存在,且草稿 + validateReceiptOrderPrepare(id); + + // 2. 作废入库单 + if (receiptOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsReceiptOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())) == 0) { + throw exception(RECEIPT_ORDER_STATUS_NOT_PREPARE); + } + } + + @Override + public WmsReceiptOrderDO getReceiptOrder(Long id) { + return receiptOrderMapper.selectById(id); + } + + @Override + public PageResult getReceiptOrderPage(WmsReceiptOrderPageReqVO pageReqVO) { + return receiptOrderMapper.selectPage(pageReqVO); + } + + @Override + public long getReceiptOrderCountByMerchantId(Long merchantId) { + return receiptOrderMapper.selectCountByMerchantId(merchantId); + } + + @Override + public long getReceiptOrderCountByWarehouseId(Long warehouseId) { + return receiptOrderMapper.selectCountByWarehouseId(warehouseId); + } + + private void validateReceiptOrderSaveData(WmsReceiptOrderSaveReqVO reqVO) { + // 校验入库单号唯一 + validateReceiptOrderNoUnique(reqVO.getId(), reqVO.getNo()); + // 校验仓库存在 + warehouseService.validateWarehouseExists(reqVO.getWarehouseId()); + // 校验供应商类型 + if (reqVO.getMerchantId() != null) { + merchantService.validateSupplierMerchantExists(reqVO.getMerchantId()); + } + } + + private void validateReceiptOrderNoUnique(Long id, String no) { + WmsReceiptOrderDO order = receiptOrderMapper.selectByNo(no); + if (order == null) { + return; + } + if (id == null || ObjectUtil.notEqual(order.getId(), id)) { + throw exception(RECEIPT_ORDER_NO_DUPLICATE); + } + } + + private void fillReceiptOrderTotal(WmsReceiptOrderDO order, WmsReceiptOrderSaveReqVO reqVO) { + BigDecimal totalQuantity = BigDecimal.ZERO; + BigDecimal totalPrice = BigDecimal.ZERO; + if (CollUtil.isNotEmpty(reqVO.getDetails())) { + for (WmsReceiptOrderDetailSaveReqVO detail : reqVO.getDetails()) { + if (detail.getQuantity() != null) { + totalQuantity = totalQuantity.add(detail.getQuantity()); + } + BigDecimal detailTotalPrice = getDetailTotalPrice(detail.getQuantity(), detail.getPrice(), + detail.getTotalPrice()); + if (detailTotalPrice != null) { + totalPrice = totalPrice.add(detailTotalPrice); + } + } + } + order.setTotalQuantity(totalQuantity).setTotalPrice(totalPrice); + } + + private static BigDecimal getDetailTotalPrice(BigDecimal quantity, BigDecimal price, BigDecimal totalPrice) { + if (totalPrice != null) { + return totalPrice; + } + return MoneyUtils.priceMultiply(price, quantity); + } + + private WmsReceiptOrderDO validateReceiptOrderExists(Long id) { + WmsReceiptOrderDO order = id == null ? null : receiptOrderMapper.selectById(id); + if (order == null) { + throw exception(RECEIPT_ORDER_NOT_EXISTS); + } + return order; + } + + /** + * 校验入库单存在且为草稿状态 + * + * @param id 入库单编号 + * @return 入库单 + */ + private WmsReceiptOrderDO validateReceiptOrderPrepare(Long id) { + WmsReceiptOrderDO order = validateReceiptOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus())) { + throw exception(RECEIPT_ORDER_STATUS_NOT_PREPARE); + } + return order; + } + + /** + * 增加入库单对应库存。 + * + * 入库在库存变更模型中使用正数数量。 + * + * @param order 入库单 + * @param details 入库单明细列表 + */ + private void createInventory(WmsReceiptOrderDO order, List details) { + List items = convertList(details, + detail -> BeanUtils.toBean(detail, WmsInventoryChangeReqDTO.Item.class) + .setTotalPrice(getDetailTotalPrice(detail.getQuantity(), detail.getPrice(), + detail.getTotalPrice()))); + inventoryService.changeInventory(new WmsInventoryChangeReqDTO() + .setOrderId(order.getId()).setOrderNo(order.getNo()) + .setOrderType(WmsOrderTypeEnum.RECEIPT.getType()).setItems(items)); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java new file mode 100644 index 000000000..39f5d86db --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.wms.service.order.shipment; + +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; + +import java.util.Collection; +import java.util.List; + +/** + * WMS 出库单明细 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsShipmentOrderDetailService { + + /** + * 创建出库单明细列表 + * + * @param orderId 出库单编号 + * @param reqVO 出库单保存信息 + */ + void createShipmentOrderDetailList(Long orderId, WmsShipmentOrderSaveReqVO reqVO); + + /** + * 更新出库单明细列表 + * + * @param orderId 出库单编号 + * @param reqVO 出库单保存信息 + */ + void updateShipmentOrderDetailList(Long orderId, WmsShipmentOrderSaveReqVO reqVO); + + /** + * 按出库单编号删除明细列表 + * + * @param orderId 出库单编号 + */ + void deleteShipmentOrderDetailListByOrderId(Long orderId); + + /** + * 按出库单编号获得明细列表 + * + * @param orderId 出库单编号 + * @return 明细列表 + */ + List getShipmentOrderDetailList(Long orderId); + + /** + * 按出库单编号集合获得明细列表 + * + * @param orderIds 出库单编号集合 + * @return 明细列表 + */ + List getShipmentOrderDetailList(Collection orderIds); + + /** + * 校验出库单明细列表存在 + * + * @param orderId 出库单编号 + * @return 明细列表 + */ + List validateShipmentOrderDetailListExists(Long orderId); + + /** + * 获得指定 SKU 的出库单明细数量 + * + * @param skuId SKU 编号 + * @return 出库单明细数量 + */ + long getShipmentOrderDetailCountBySkuId(Long skuId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java new file mode 100644 index 000000000..ed9e6339a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImpl.java @@ -0,0 +1,131 @@ +package cn.iocoder.yudao.module.wms.service.order.shipment; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.shipment.WmsShipmentOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 出库单明细 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsShipmentOrderDetailServiceImpl implements WmsShipmentOrderDetailService { + + @Resource + private WmsShipmentOrderDetailMapper shipmentOrderDetailMapper; + @Resource + private WmsItemSkuService itemSkuService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createShipmentOrderDetailList(Long orderId, WmsShipmentOrderSaveReqVO reqVO) { + List list = buildShipmentOrderDetailList(reqVO); + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(detail -> detail.setId(null).setOrderId(orderId)); + shipmentOrderDetailMapper.insertBatch(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateShipmentOrderDetailList(Long orderId, WmsShipmentOrderSaveReqVO reqVO) { + // 第一步,对比新老数据,获得添加、修改、删除的列表 + List oldList = shipmentOrderDetailMapper.selectListByOrderId(orderId); + List list = buildShipmentOrderDetailList(reqVO); + List newList = CollUtil.isEmpty(list) ? ListUtil.of() : list; + List> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录 + (oldVal, newVal) -> ObjectUtil.equal(oldVal.getId(), newVal.getId())); + + // 第二步,批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffList.get(0))) { + if (CollUtil.isNotEmpty(convertList(diffList.get(0), WmsShipmentOrderDetailDO::getId))) { + throw exception(SHIPMENT_ORDER_DETAIL_NOT_EXISTS); + } + diffList.get(0).forEach(detail -> detail.setOrderId(orderId)); + shipmentOrderDetailMapper.insertBatch(diffList.get(0)); + } + if (CollUtil.isNotEmpty(diffList.get(1))) { + diffList.get(1).forEach(detail -> detail.setOrderId(orderId)); + shipmentOrderDetailMapper.updateBatch(diffList.get(1)); + } + if (CollUtil.isNotEmpty(diffList.get(2))) { + shipmentOrderDetailMapper.deleteByIds(convertList(diffList.get(2), WmsShipmentOrderDetailDO::getId)); + } + } + + @Override + public void deleteShipmentOrderDetailListByOrderId(Long orderId) { + shipmentOrderDetailMapper.deleteByOrderId(orderId); + } + + @Override + public List getShipmentOrderDetailList(Long orderId) { + return shipmentOrderDetailMapper.selectListByOrderId(orderId); + } + + @Override + public List getShipmentOrderDetailList(Collection orderIds) { + if (CollUtil.isEmpty(orderIds)) { + return ListUtil.of(); + } + return shipmentOrderDetailMapper.selectListByOrderIds(orderIds); + } + + @Override + public List validateShipmentOrderDetailListExists(Long orderId) { + List details = shipmentOrderDetailMapper.selectListByOrderId(orderId); + if (CollUtil.isEmpty(details)) { + throw exception(SHIPMENT_ORDER_DETAIL_REQUIRED); + } + return details; + } + + @Override + public long getShipmentOrderDetailCountBySkuId(Long skuId) { + return shipmentOrderDetailMapper.selectCountBySkuId(skuId); + } + + private List buildShipmentOrderDetailList(WmsShipmentOrderSaveReqVO reqVO) { + if (CollUtil.isEmpty(reqVO.getDetails())) { + return ListUtil.of(); + } + return convertList(reqVO.getDetails(), detail -> { + // 校验 SKU 存在 + itemSkuService.validateItemSkuExists(detail.getSkuId()); + // 构建对象 + WmsShipmentOrderDetailDO detailDO = BeanUtils.toBean(detail, WmsShipmentOrderDetailDO.class) + .setWarehouseId(reqVO.getWarehouseId()); + fillDetailTotalPrice(detailDO); + return detailDO; + }); + } + + private static void fillDetailTotalPrice(WmsShipmentOrderDetailDO detail) { + if (detail.getTotalPrice() != null || detail.getQuantity() == null || detail.getPrice() == null) { + return; + } + detail.setTotalPrice(MoneyUtils.priceMultiply(detail.getPrice(), detail.getQuantity())); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java new file mode 100644 index 000000000..2a2b2f6a8 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderService.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.wms.service.order.shipment; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDO; +import jakarta.validation.Valid; + +/** + * WMS 出库单 Service 接口 + * + * @author 芋道源码 + */ +public interface WmsShipmentOrderService { + + /** + * 创建出库单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createShipmentOrder(@Valid WmsShipmentOrderSaveReqVO createReqVO); + + /** + * 更新出库单 + * + * @param updateReqVO 更新信息 + */ + void updateShipmentOrder(@Valid WmsShipmentOrderSaveReqVO updateReqVO); + + /** + * 删除出库单 + * + * @param id 编号 + */ + void deleteShipmentOrder(Long id); + + /** + * 完成出库 + * + * @param id 编号 + */ + void completeShipmentOrder(Long id); + + /** + * 作废出库单 + * + * @param id 编号 + */ + void cancelShipmentOrder(Long id); + + /** + * 获得出库单 + * + * @param id 编号 + * @return 出库单 + */ + WmsShipmentOrderDO getShipmentOrder(Long id); + + /** + * 获得出库单分页 + * + * @param pageReqVO 分页查询 + * @return 出库单分页 + */ + PageResult getShipmentOrderPage(WmsShipmentOrderPageReqVO pageReqVO); + + /** + * 获得指定往来企业的出库单数量 + * + * @param merchantId 往来企业编号 + * @return 出库单数量 + */ + long getShipmentOrderCountByMerchantId(Long merchantId); + + /** + * 获得指定仓库的出库单数量 + * + * @param warehouseId 仓库编号 + * @return 出库单数量 + */ + long getShipmentOrderCountByWarehouseId(Long warehouseId); + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java new file mode 100644 index 000000000..cebb35781 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImpl.java @@ -0,0 +1,244 @@ +package cn.iocoder.yudao.module.wms.service.order.shipment; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderPageReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.shipment.WmsShipmentOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.md.merchant.WmsMerchantService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.math.BigDecimal; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.*; + +/** + * WMS 出库单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class WmsShipmentOrderServiceImpl implements WmsShipmentOrderService { + + @Resource + private WmsShipmentOrderMapper shipmentOrderMapper; + @Resource + private WmsShipmentOrderDetailService shipmentOrderDetailService; + @Resource + private WmsWarehouseService warehouseService; + @Resource + private WmsMerchantService merchantService; + @Resource + private WmsInventoryService inventoryService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createShipmentOrder(WmsShipmentOrderSaveReqVO createReqVO) { + // 1. 校验出库单保存数据 + validateShipmentOrderSaveData(createReqVO); + + // 2.1 插入出库单 + WmsShipmentOrderDO order = BeanUtils.toBean(createReqVO, WmsShipmentOrderDO.class); + order.setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillShipmentOrderTotal(order, createReqVO); + shipmentOrderMapper.insert(order); + // 2.2 插入出库单明细 + shipmentOrderDetailService.createShipmentOrderDetailList(order.getId(), createReqVO); + return order.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateShipmentOrder(WmsShipmentOrderSaveReqVO updateReqVO) { + // 1. 校验出库单保存数据 + validateShipmentOrderPrepare(updateReqVO.getId()); + validateShipmentOrderSaveData(updateReqVO); + + // 2.1 更新出库单 + WmsShipmentOrderDO updateObj = BeanUtils.toBean(updateReqVO, WmsShipmentOrderDO.class) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()); + fillShipmentOrderTotal(updateObj, updateReqVO); + int updateCount = shipmentOrderMapper.updateByIdAndStatus(updateReqVO.getId(), + WmsOrderStatusEnum.PREPARE.getStatus(), updateObj); + if (updateCount == 0) { + throw exception(SHIPMENT_ORDER_STATUS_NOT_PREPARE); + } + // 2.2 更新出库单明细 + shipmentOrderDetailService.updateShipmentOrderDetailList(updateReqVO.getId(), updateReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteShipmentOrder(Long id) { + // 1. 校验存在,且可删除 + WmsShipmentOrderDO order = validateShipmentOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus()) + && ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.CANCELED.getStatus())) { + throw exception(SHIPMENT_ORDER_STATUS_NOT_DELETABLE); + } + + // 2.1 删除出库单 + shipmentOrderMapper.deleteById(id); + // 2.2 删除出库单明细 + shipmentOrderDetailService.deleteShipmentOrderDetailListByOrderId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void completeShipmentOrder(Long id) { + // 1.1 校验存在,且草稿 + WmsShipmentOrderDO order = validateShipmentOrderPrepare(id); + // 1.2 校验出库单明细存在 + List details = shipmentOrderDetailService.validateShipmentOrderDetailListExists(id); + + // 2. 完成出库单 + if (shipmentOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsShipmentOrderDO().setStatus(WmsOrderStatusEnum.FINISHED.getStatus())) == 0) { + throw exception(SHIPMENT_ORDER_STATUS_NOT_PREPARE); + } + + // 3. 扣减库存 + changeInventory(order, details); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelShipmentOrder(Long id) { + // 1. 校验存在,且草稿 + validateShipmentOrderPrepare(id); + + // 2. 作废出库单 + if (shipmentOrderMapper.updateByIdAndStatus(id, WmsOrderStatusEnum.PREPARE.getStatus(), + new WmsShipmentOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())) == 0) { + throw exception(SHIPMENT_ORDER_STATUS_NOT_PREPARE); + } + } + + @Override + public WmsShipmentOrderDO getShipmentOrder(Long id) { + return shipmentOrderMapper.selectById(id); + } + + @Override + public PageResult getShipmentOrderPage(WmsShipmentOrderPageReqVO pageReqVO) { + return shipmentOrderMapper.selectPage(pageReqVO); + } + + @Override + public long getShipmentOrderCountByMerchantId(Long merchantId) { + return shipmentOrderMapper.selectCountByMerchantId(merchantId); + } + + @Override + public long getShipmentOrderCountByWarehouseId(Long warehouseId) { + return shipmentOrderMapper.selectCountByWarehouseId(warehouseId); + } + + private void validateShipmentOrderSaveData(WmsShipmentOrderSaveReqVO reqVO) { + // 校验出库单号唯一 + validateShipmentOrderNoUnique(reqVO.getId(), reqVO.getNo()); + // 校验仓库存在 + warehouseService.validateWarehouseExists(reqVO.getWarehouseId()); + // 校验客户类型 + if (reqVO.getMerchantId() != null) { + merchantService.validateCustomerMerchantExists(reqVO.getMerchantId()); + } + } + + private void validateShipmentOrderNoUnique(Long id, String no) { + WmsShipmentOrderDO order = shipmentOrderMapper.selectByNo(no); + if (order == null) { + return; + } + if (id == null || ObjectUtil.notEqual(order.getId(), id)) { + throw exception(SHIPMENT_ORDER_NO_DUPLICATE); + } + } + + private void fillShipmentOrderTotal(WmsShipmentOrderDO order, WmsShipmentOrderSaveReqVO reqVO) { + BigDecimal totalQuantity = BigDecimal.ZERO; + BigDecimal totalPrice = BigDecimal.ZERO; + if (CollUtil.isNotEmpty(reqVO.getDetails())) { + for (WmsShipmentOrderDetailSaveReqVO detail : reqVO.getDetails()) { + if (detail.getQuantity() != null) { + totalQuantity = totalQuantity.add(detail.getQuantity()); + } + BigDecimal detailTotalPrice = getDetailTotalPrice(detail.getQuantity(), detail.getPrice(), + detail.getTotalPrice()); + if (detailTotalPrice != null) { + totalPrice = totalPrice.add(detailTotalPrice); + } + } + } + order.setTotalQuantity(totalQuantity).setTotalPrice(totalPrice); + } + + private static BigDecimal getDetailTotalPrice(BigDecimal quantity, BigDecimal price, BigDecimal totalPrice) { + if (totalPrice != null) { + return totalPrice; + } + return MoneyUtils.priceMultiply(price, quantity); + } + + private WmsShipmentOrderDO validateShipmentOrderExists(Long id) { + WmsShipmentOrderDO order = id == null ? null : shipmentOrderMapper.selectById(id); + if (order == null) { + throw exception(SHIPMENT_ORDER_NOT_EXISTS); + } + return order; + } + + /** + * 校验出库单存在且为草稿状态 + * + * @param id 出库单编号 + * @return 出库单 + */ + private WmsShipmentOrderDO validateShipmentOrderPrepare(Long id) { + WmsShipmentOrderDO order = validateShipmentOrderExists(id); + if (ObjectUtil.notEqual(order.getStatus(), WmsOrderStatusEnum.PREPARE.getStatus())) { + throw exception(SHIPMENT_ORDER_STATUS_NOT_PREPARE); + } + return order; + } + + /** + * 扣减出库单对应库存 + * + * @param order 出库单 + * @param details 出库单明细列表 + */ + private void changeInventory(WmsShipmentOrderDO order, List details) { + List items = convertList(details, + detail -> BeanUtils.toBean(detail, WmsInventoryChangeReqDTO.Item.class) + .setQuantity(detail.getQuantity().negate()) + .setTotalPrice(negate(getDetailTotalPrice(detail.getQuantity(), detail.getPrice(), + detail.getTotalPrice())))); + inventoryService.changeInventory(new WmsInventoryChangeReqDTO() + .setOrderId(order.getId()).setOrderNo(order.getNo()) + .setOrderType(WmsOrderTypeEnum.SHIPMENT.getType()).setItems(items)); + } + + private static BigDecimal negate(BigDecimal value) { + return value == null ? null : value.negate(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml new file mode 100644 index 000000000..c819a254d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml @@ -0,0 +1,120 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + nawmspace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + nawmspace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 + primary: master + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 400-infra.server.iocoder.cn # 地址 + port: 6379 # 端口 + database: 1 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + username: admin + password: admin + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + access-log: # 访问日志的配置项 + enable: false + demo: false # 关闭演示模式 diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml new file mode 100644 index 000000000..fdd75ad7b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml @@ -0,0 +1,139 @@ +--- #################### 注册中心 + 配置中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + username: # Nacos 账号 + password: # Nacos 密码 + discovery: # 【配置中心】配置项 + nawmspace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + config: # 【注册中心】配置项 + nawmspace: dev # 命名空间。这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 1 # 初始连接数 + min-idle: 1 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 + primary: master + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例 + # url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例 + username: root + password: 123456 + # username: sa # SQL Server 连接的示例 + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例 + # username: SYSDBA # DM 连接的示例 + # password: SYSDBA # DM 连接的示例 + slave: # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + username: admin + password: admin + +# 日志文件配置 +logging: + level: + # 配置自己写的 MyBatis Mapper 打印日志 + cn.iocoder.yudao.module.wms.dal.mysql: debug + org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + env: # 多环境的配置项 + tag: ${HOSTNAME} + security: + mock-enable: true + access-log: # 访问日志的配置项 + enable: false diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml new file mode 100644 index 000000000..7dfc4055d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml @@ -0,0 +1,124 @@ +spring: + application: + name: wms-server + + profiles: + active: local + + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务 + + config: + import: + - optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置 + - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + + # Jackson 配置项 + jackson: + serialization: + write-dates-as-tiwmstamps: true # 设置 LocalDateTime 的格式,使用时间戳 + write-date-tiwmstamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-tiwmstamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean + + # Cache 配置项 + cache: + type: REDIS + redis: + time-to-live: 1h # 设置过期时间为 1 小时 + +server: + port: 48092 + +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true # 1. 是否开启 Swagger 接文档的元数据 + path: /v3/api-docs + swagger-ui: + enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面 + path: /swagger-ui + default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 + +knife4j: + enable: true + setting: + language: zh_cn + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + id-type: NONE # "智能"模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 + # id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 + # id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 + # id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + banner: false # 关闭控制台的 Banner 打印 + type-aliases-package: ${yudao.info.base-package}.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +mybatis-plus-join: + banner: false # 关闭控制台的 Banner 打印 + +# Spring Data Redis 配置 +spring: + data: + redis: + repositories: + enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度 + +# VO 转换(数据翻译)相关 +easy-trans: + is-enable-global: false # 【默认禁用,对性能确认压力大】启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### + +xxl: + job: + executor: + appname: ${spring.application.name} # 执行器 AppName + logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径 + accessToken: default_token # 执行器通讯TOKEN + +--- #################### 芋道相关配置 #################### + +yudao: + info: + version: 1.0.0 + base-package: cn.iocoder.yudao.module.wms + web: + admin-ui: + url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址 + xss: + enable: false + exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + swagger: + title: 管理后台 + description: 提供管理员管理的所有功能 + version: ${yudao.info.version} + tenant: # 多租户相关配置项 + enable: true + ignore-urls: + +debug: false diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/logback-spring.xml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..15b28cfdf --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/logback-spring.xml @@ -0,0 +1,56 @@ + + + + + + + + +       + + ${CONSOLE_LOG_PATTERN} + + + + + + + + ${FILE_LOG_PATTERN} + + + ${LOG_FILE} + + + ${LOG_FILE}.%d{yyyy-MM-dd}.%i.log + 30 + 10MB + + + + + 0 + 512 + + + + + + + + + + + + + + + diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml new file mode 100644 index 000000000..38c7bdf5d --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java new file mode 100644 index 000000000..3dc11dbf1 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java @@ -0,0 +1,632 @@ +package cn.iocoder.yudao.module.wms.service.inventory; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.inventory.vo.WmsInventoryPageReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryHistoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.inventory.WmsInventoryDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO; +import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryHistoryMapper; +import cn.iocoder.yudao.module.wms.dal.mysql.inventory.WmsInventoryMapper; +import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemMapper; +import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemSkuMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryCheckReqDTO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemService; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.CHECK_ORDER_INVENTORY_CHANGED; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_QUANTITY_NOT_ENOUGH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +@Import({WmsInventoryServiceImpl.class, WmsInventoryHistoryServiceImpl.class}) +public class WmsInventoryServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsInventoryServiceImpl inventoryService; + + @Resource + private WmsInventoryMapper inventoryMapper; + @Resource + private WmsInventoryHistoryMapper inventoryHistoryMapper; + @Resource + private WmsItemMapper itemMapper; + @Resource + private WmsItemSkuMapper skuMapper; + + @MockitoBean + private WmsItemSkuService itemSkuService; + @MockitoBean + private WmsItemService itemService; + + @Test + public void testChangeInventory_createInventory() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "5.00"); + + // 调用 + inventoryService.changeInventory(reqDTO); + + // 断言:库存余额 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, new BigDecimal("5.00").compareTo(inventory.getQuantity())); + // 断言:库存流水 + List histories = inventoryHistoryMapper.selectList(); + assertEquals(1, histories.size()); + WmsInventoryHistoryDO history = histories.get(0); + assertEquals(sku.getId(), history.getSkuId()); + assertEquals(100L, history.getWarehouseId()); + assertEquals(0, BigDecimal.ZERO.compareTo(history.getBeforeQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(history.getAfterQuantity())); + assertEquals(0, new BigDecimal("500.00").compareTo(history.getTotalPrice())); + assertEquals(reqDTO.getOrderId(), history.getOrderId()); + assertEquals(reqDTO.getOrderNo(), history.getOrderNo()); + assertEquals(reqDTO.getOrderType(), history.getOrderType()); + } + + @Test + public void testChangeInventory_updateInventory() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "2.00")); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "3.00"); + + // 调用 + inventoryService.changeInventory(reqDTO); + + // 断言:库存余额 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, new BigDecimal("5.00").compareTo(inventory.getQuantity())); + // 断言:库存流水 + List histories = inventoryHistoryMapper.selectList(); + assertEquals(1, histories.size()); + WmsInventoryHistoryDO history = histories.get(0); + assertEquals(0, new BigDecimal("2.00").compareTo(history.getBeforeQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(history.getAfterQuantity())); + assertEquals(0, new BigDecimal("3.00").compareTo(history.getQuantity())); + assertEquals(0, new BigDecimal("300.00").compareTo(history.getTotalPrice())); + } + + @Test + public void testChangeInventory_sameInventoryKeyKeepItemGranularity() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "5.00"); + List items = new ArrayList<>(reqDTO.getItems()); + items.add(new WmsInventoryChangeReqDTO.Item() + .setSkuId(sku.getId()) + .setWarehouseId(100L) + .setQuantity(new BigDecimal("7.00")) + .setPrice(new BigDecimal("140.00")) + .setTotalPrice(new BigDecimal("980.00")) + .setRemark("测试入库 2")); + reqDTO.setItems(items); + + // 调用 + inventoryService.changeInventory(reqDTO); + + // 断言:库存余额只保留一条,数量逐条累加 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, new BigDecimal("12.00").compareTo(inventory.getQuantity())); + // 断言:库存流水保持用户明细颗粒度 + List histories = inventoryHistoryMapper.selectList(); + histories.sort(Comparator.comparing(WmsInventoryHistoryDO::getId)); + assertEquals(2, histories.size()); + assertEquals(0, BigDecimal.ZERO.compareTo(histories.get(0).getBeforeQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getAfterQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getQuantity())); + assertEquals(0, new BigDecimal("500.00").compareTo(histories.get(0).getTotalPrice())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(1).getBeforeQuantity())); + assertEquals(0, new BigDecimal("12.00").compareTo(histories.get(1).getAfterQuantity())); + assertEquals(0, new BigDecimal("7.00").compareTo(histories.get(1).getQuantity())); + assertEquals(0, new BigDecimal("980.00").compareTo(histories.get(1).getTotalPrice())); + } + + @Test + public void testChangeInventory_sameExistingInventoryKeyKeepItemGranularity() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "2.00")); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "3.00"); + List items = new ArrayList<>(reqDTO.getItems()); + items.add(new WmsInventoryChangeReqDTO.Item() + .setSkuId(sku.getId()) + .setWarehouseId(100L) + .setQuantity(new BigDecimal("4.00")) + .setPrice(new BigDecimal("80.00")) + .setTotalPrice(new BigDecimal("320.00")) + .setRemark("测试入库 2")); + reqDTO.setItems(items); + + // 调用 + inventoryService.changeInventory(reqDTO); + + // 断言:库存余额只保留一条,数量逐条累加 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, new BigDecimal("9.00").compareTo(inventory.getQuantity())); + // 断言:库存流水保持用户明细颗粒度 + List histories = inventoryHistoryMapper.selectList(); + histories.sort(Comparator.comparing(WmsInventoryHistoryDO::getId)); + assertEquals(2, histories.size()); + assertEquals(0, new BigDecimal("2.00").compareTo(histories.get(0).getBeforeQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getAfterQuantity())); + assertEquals(0, new BigDecimal("3.00").compareTo(histories.get(0).getQuantity())); + assertEquals(0, new BigDecimal("300.00").compareTo(histories.get(0).getTotalPrice())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(1).getBeforeQuantity())); + assertEquals(0, new BigDecimal("9.00").compareTo(histories.get(1).getAfterQuantity())); + assertEquals(0, new BigDecimal("4.00").compareTo(histories.get(1).getQuantity())); + assertEquals(0, new BigDecimal("320.00").compareTo(histories.get(1).getTotalPrice())); + } + + @Test + public void testChangeInventory_multipleInventoryKeysKeepItemGranularity() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "5.00"); + List items = new ArrayList<>(reqDTO.getItems()); + items.add(new WmsInventoryChangeReqDTO.Item() + .setSkuId(sku.getId()) + .setWarehouseId(200L) + .setQuantity(new BigDecimal("3.00")) + .setPrice(new BigDecimal("60.00")) + .setTotalPrice(new BigDecimal("180.00")) + .setRemark("测试入库 2")); + items.add(new WmsInventoryChangeReqDTO.Item() + .setSkuId(sku.getId()) + .setWarehouseId(100L) + .setQuantity(new BigDecimal("-2.00")) + .setPrice(new BigDecimal("40.00")) + .setTotalPrice(new BigDecimal("-80.00")) + .setRemark("测试出库")); + reqDTO.setItems(items); + + // 调用 + inventoryService.changeInventory(reqDTO); + + // 断言:不同库存余额分别更新 + WmsInventoryDO inventory1 = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory1); + assertEquals(0, new BigDecimal("3.00").compareTo(inventory1.getQuantity())); + WmsInventoryDO inventory2 = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 200L); + assertNotNull(inventory2); + assertEquals(0, new BigDecimal("3.00").compareTo(inventory2.getQuantity())); + // 断言:批量加锁后仍按明细颗粒度记录 before/after + List histories = inventoryHistoryMapper.selectList(); + histories.sort(Comparator.comparing(WmsInventoryHistoryDO::getId)); + assertEquals(3, histories.size()); + assertEquals(100L, histories.get(0).getWarehouseId()); + assertEquals(0, BigDecimal.ZERO.compareTo(histories.get(0).getBeforeQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getAfterQuantity())); + assertEquals(0, new BigDecimal("500.00").compareTo(histories.get(0).getTotalPrice())); + assertEquals(200L, histories.get(1).getWarehouseId()); + assertEquals(0, BigDecimal.ZERO.compareTo(histories.get(1).getBeforeQuantity())); + assertEquals(0, new BigDecimal("3.00").compareTo(histories.get(1).getAfterQuantity())); + assertEquals(0, new BigDecimal("180.00").compareTo(histories.get(1).getTotalPrice())); + assertEquals(100L, histories.get(2).getWarehouseId()); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(2).getBeforeQuantity())); + assertEquals(0, new BigDecimal("3.00").compareTo(histories.get(2).getAfterQuantity())); + assertEquals(0, new BigDecimal("-80.00").compareTo(histories.get(2).getTotalPrice())); + } + + @Test + public void testChangeInventory_concurrentCreateSameInventoryOnlyOneBalance() throws Exception { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + int threadCount = 4; + CountDownLatch readyLatch = new CountDownLatch(threadCount); + CountDownLatch startLatch = new CountDownLatch(1); + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(threadCount); + for (int i = 0; i < threadCount; i++) { + futures.add(executorService.submit(() -> { + readyLatch.countDown(); + try { + startLatch.await(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + inventoryService.changeInventory(createChangeReq(sku.getId(), 100L, "1.00")); + })); + } + + try { + // 调用 + assertTrue(readyLatch.await(5, TimeUnit.SECONDS)); + startLatch.countDown(); + for (Future future : futures) { + future.get(10, TimeUnit.SECONDS); + } + } finally { + executorService.shutdownNow(); + } + + // 断言:并发补行后仍只有一条库存余额,数量累计正确 + List inventories = inventoryMapper.selectListByKeys(Collections.singletonList( + new WmsInventoryDO().setSkuId(sku.getId()).setWarehouseId(100L))); + assertEquals(1, inventories.size()); + assertEquals(0, new BigDecimal("4.00").compareTo(inventories.get(0).getQuantity())); + assertEquals(threadCount, inventoryHistoryMapper.selectCount()); + } + + @Test + public void testChangeInventory_quantityNotEnough() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "2.00")); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "-3.00"); + mockItemSkuAndItem(item, sku); + + // 调用,并断言 + assertServiceException(() -> inventoryService.changeInventory(reqDTO), INVENTORY_QUANTITY_NOT_ENOUGH, + item.getName(), sku.getName(), 100L, new BigDecimal("2.000000"), new BigDecimal("-3.00")); + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, new BigDecimal("2.00").compareTo(inventory.getQuantity())); + assertEquals(0L, inventoryHistoryMapper.selectCount()); + } + + @Test + public void testChangeInventory_quantityNotEnoughWithoutInventory() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "-3.00"); + mockItemSkuAndItem(item, sku); + + // 调用,并断言 + assertServiceException(() -> inventoryService.changeInventory(reqDTO), INVENTORY_QUANTITY_NOT_ENOUGH, + item.getName(), sku.getName(), 100L, BigDecimal.ZERO.setScale(6), new BigDecimal("-3.00")); + assertEquals(0L, inventoryMapper.selectCount()); + assertEquals(0L, inventoryHistoryMapper.selectCount()); + } + + @Test + public void testChangeInventory_quantityNotEnoughRollbackCreatedInventories() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryChangeReqDTO reqDTO = createChangeReq(sku.getId(), 100L, "5.00"); + List items = new ArrayList<>(reqDTO.getItems()); + items.add(new WmsInventoryChangeReqDTO.Item() + .setSkuId(sku.getId()) + .setWarehouseId(200L) + .setQuantity(new BigDecimal("-1.00")) + .setPrice(new BigDecimal("20.00")) + .setTotalPrice(new BigDecimal("-20.00")) + .setRemark("测试出库")); + reqDTO.setItems(items); + mockItemSkuAndItem(item, sku); + + // 调用,并断言:第二条明细库存不足,事务回滚前面补齐的库存行 + assertServiceException(() -> inventoryService.changeInventory(reqDTO), INVENTORY_QUANTITY_NOT_ENOUGH, + item.getName(), sku.getName(), 200L, BigDecimal.ZERO.setScale(6), new BigDecimal("-1.00")); + assertNull(inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L)); + assertNull(inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 200L)); + assertEquals(0L, inventoryHistoryMapper.selectCount()); + } + + @Test + public void testCheckInventory_updateExistingInventory() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryDO inventory = createInventory(sku.getId(), 100L, "10.00"); + inventoryMapper.insert(inventory); + WmsInventoryCheckReqDTO reqDTO = createCheckReq(inventory.getId(), sku.getId(), 100L, + "10.00", "7.00"); + + // 调用 + inventoryService.checkInventory(reqDTO); + + // 断言:库存余额直接调整为实盘数量 + WmsInventoryDO dbInventory = inventoryMapper.selectById(inventory.getId()); + assertNotNull(dbInventory); + assertEquals(0, new BigDecimal("7.00").compareTo(dbInventory.getQuantity())); + // 断言:库存流水记录盘库差异 + List histories = inventoryHistoryMapper.selectList(); + assertEquals(1, histories.size()); + WmsInventoryHistoryDO history = histories.get(0); + assertEquals(sku.getId(), history.getSkuId()); + assertEquals(100L, history.getWarehouseId()); + assertEquals(0, new BigDecimal("10.00").compareTo(history.getBeforeQuantity())); + assertEquals(0, new BigDecimal("7.00").compareTo(history.getAfterQuantity())); + assertEquals(0, new BigDecimal("-3.00").compareTo(history.getQuantity())); + assertEquals(0, new BigDecimal("-300.00").compareTo(history.getTotalPrice())); + assertEquals(reqDTO.getOrderId(), history.getOrderId()); + assertEquals(reqDTO.getOrderNo(), history.getOrderNo()); + assertEquals(reqDTO.getOrderType(), history.getOrderType()); + } + + @Test + public void testCheckInventory_existingInventoryChanged() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryDO inventory = createInventory(sku.getId(), 100L, "12.00"); + inventoryMapper.insert(inventory); + WmsInventoryCheckReqDTO reqDTO = createCheckReq(inventory.getId(), sku.getId(), 100L, + "10.00", "7.00"); + + // 调用,并断言 + assertServiceException(() -> inventoryService.checkInventory(reqDTO), CHECK_ORDER_INVENTORY_CHANGED); + WmsInventoryDO dbInventory = inventoryMapper.selectById(inventory.getId()); + assertNotNull(dbInventory); + assertEquals(0, new BigDecimal("12.00").compareTo(dbInventory.getQuantity())); + assertEquals(0L, inventoryHistoryMapper.selectCount()); + } + + @Test + public void testCheckInventory_createNewInventory() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryCheckReqDTO reqDTO = createCheckReq(null, sku.getId(), 100L, + "0.00", "5.00"); + + // 调用 + inventoryService.checkInventory(reqDTO); + + // 断言:新增库存直接落为实盘数量 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, new BigDecimal("5.00").compareTo(inventory.getQuantity())); + // 断言:库存流水记录从 0 到实盘数量 + List histories = inventoryHistoryMapper.selectList(); + assertEquals(1, histories.size()); + assertEquals(0, BigDecimal.ZERO.compareTo(histories.get(0).getBeforeQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getAfterQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(histories.get(0).getQuantity())); + assertEquals(0, new BigDecimal("500.00").compareTo(histories.get(0).getTotalPrice())); + } + + @Test + public void testCheckInventory_createNewZeroInventory() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsInventoryCheckReqDTO reqDTO = createCheckReq(null, sku.getId(), 100L, + "0.00", "0.00"); + + // 调用 + inventoryService.checkInventory(reqDTO); + + // 断言:新增零库存商品也会生成库存余额行 + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, BigDecimal.ZERO.compareTo(inventory.getQuantity())); + assertEquals(0L, inventoryHistoryMapper.selectCount()); + } + + @Test + public void testCheckInventory_newInventoryAlreadyExists() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "3.00")); + WmsInventoryCheckReqDTO reqDTO = createCheckReq(null, sku.getId(), 100L, + "0.00", "5.00"); + + // 调用,并断言 + assertServiceException(() -> inventoryService.checkInventory(reqDTO), CHECK_ORDER_INVENTORY_CHANGED); + WmsInventoryDO inventory = inventoryMapper.selectBySkuIdAndWarehouseId(sku.getId(), 100L); + assertNotNull(inventory); + assertEquals(0, new BigDecimal("3.00").compareTo(inventory.getQuantity())); + assertEquals(0L, inventoryHistoryMapper.selectCount()); + } + + @Test + public void testGetInventoryPage_filterByWarehouse() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "2.00")); + inventoryMapper.insert(createInventory(sku.getId(), 200L, "5.00")); + // 准备参数 + WmsInventoryPageReqVO reqVO = new WmsInventoryPageReqVO(); + reqVO.setType(WmsInventoryPageReqVO.TYPE_WAREHOUSE); + reqVO.setWarehouseId(100L); + + // 调用 + PageResult pageResult = inventoryService.getInventoryPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + WmsInventoryDO inventory = pageResult.getList().get(0); + assertEquals(100L, inventory.getWarehouseId()); + assertEquals(sku.getId(), inventory.getSkuId()); + assertEquals(0, new BigDecimal("2.00").compareTo(inventory.getQuantity())); + } + + @Test + public void testGetInventoryPage_groupByWarehouse_minQuantity() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "2.00")); + inventoryMapper.insert(createInventory(sku.getId(), 200L, "7.00")); + // 准备参数 + WmsInventoryPageReqVO reqVO = new WmsInventoryPageReqVO(); + reqVO.setType(WmsInventoryPageReqVO.TYPE_WAREHOUSE); + reqVO.setMinQuantity(new BigDecimal("6.00")); + + // 调用 + PageResult pageResult = inventoryService.getInventoryPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertEquals(200L, pageResult.getList().get(0).getWarehouseId()); + } + + @Test + public void testGetInventoryPage_onlyPositiveQuantity() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "0.00")); + inventoryMapper.insert(createInventory(sku.getId(), 200L, "0.01")); + inventoryMapper.insert(createInventory(sku.getId(), 300L, "1.00")); + // 准备参数 + WmsInventoryPageReqVO reqVO = new WmsInventoryPageReqVO(); + reqVO.setType(WmsInventoryPageReqVO.TYPE_WAREHOUSE); + reqVO.setOnlyPositiveQuantity(true); + + // 调用 + PageResult pageResult = inventoryService.getInventoryPage(reqVO); + // 断言 + assertEquals(2, pageResult.getTotal()); + assertEquals(2, pageResult.getList().size()); + assertEquals(200L, pageResult.getList().get(0).getWarehouseId()); + assertEquals(0, new BigDecimal("0.01").compareTo(pageResult.getList().get(0).getQuantity())); + assertEquals(300L, pageResult.getList().get(1).getWarehouseId()); + assertEquals(0, new BigDecimal("1.00").compareTo(pageResult.getList().get(1).getQuantity())); + } + + @Test + public void testGetInventoryPage_sku() { + // mock 数据 + WmsItemDO item = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装"); + WmsItemSkuDO sku2 = createSku(item.getId(), "SKU-002", "5kg 箱装"); + inventoryMapper.insert(createInventory(sku.getId(), 100L, "2.00")); + inventoryMapper.insert(createInventory(sku2.getId(), 100L, "3.00")); + // 准备参数 + WmsInventoryPageReqVO reqVO = new WmsInventoryPageReqVO(); + reqVO.setType(WmsInventoryPageReqVO.TYPE_ITEM); + reqVO.setWarehouseId(100L); + reqVO.setSkuId(sku.getId()); + + // 调用 + PageResult pageResult = inventoryService.getInventoryPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertEquals(sku.getId(), pageResult.getList().get(0).getSkuId()); + } + + @Test + public void testGetInventoryPage_orderByItemDimension() { + // mock 数据 + WmsItemDO item1 = createItem("ITEM-001", "红富士苹果"); + WmsItemSkuDO sku1 = createSku(item1.getId(), "SKU-001", "10kg 箱装"); + WmsItemDO item2 = createItem("ITEM-002", "香蕉"); + WmsItemSkuDO sku2 = createSku(item2.getId(), "SKU-002", "5kg 箱装"); + inventoryMapper.insert(createInventory(sku2.getId(), 100L, "3.00")); + inventoryMapper.insert(createInventory(sku1.getId(), 200L, "2.00")); + inventoryMapper.insert(createInventory(sku1.getId(), 100L, "1.00")); + // 准备参数 + WmsInventoryPageReqVO reqVO = new WmsInventoryPageReqVO(); + reqVO.setType(WmsInventoryPageReqVO.TYPE_ITEM); + + // 调用 + PageResult pageResult = inventoryService.getInventoryPage(reqVO); + // 断言 + assertEquals(3, pageResult.getTotal()); + assertEquals(sku1.getId(), pageResult.getList().get(0).getSkuId()); + assertEquals(100L, pageResult.getList().get(0).getWarehouseId()); + assertEquals(sku1.getId(), pageResult.getList().get(1).getSkuId()); + assertEquals(200L, pageResult.getList().get(1).getWarehouseId()); + assertEquals(sku2.getId(), pageResult.getList().get(2).getSkuId()); + assertEquals(100L, pageResult.getList().get(2).getWarehouseId()); + } + + private WmsItemDO createItem(String code, String name) { + WmsItemDO item = WmsItemDO.builder() + .code(code) + .name(name) + .unit("箱") + .categoryId(1L) + .build(); + itemMapper.insert(item); + return item; + } + + private WmsItemSkuDO createSku(Long itemId, String code, String name) { + WmsItemSkuDO sku = WmsItemSkuDO.builder() + .itemId(itemId) + .code(code) + .name(name) + .barCode("69010001") + .build(); + skuMapper.insert(sku); + return sku; + } + + private void mockItemSkuAndItem(WmsItemDO item, WmsItemSkuDO sku) { + when(itemSkuService.validateItemSkuExists(sku.getId())).thenReturn(sku); + when(itemService.validateItemExists(item.getId())).thenReturn(item); + } + + private static WmsInventoryDO createInventory(Long skuId, Long warehouseId, String quantity) { + return WmsInventoryDO.builder() + .skuId(skuId) + .warehouseId(warehouseId) + .quantity(new BigDecimal(quantity)) + .build(); + } + + private static WmsInventoryChangeReqDTO createChangeReq(Long skuId, Long warehouseId, String quantity) { + return new WmsInventoryChangeReqDTO() + .setOrderId(1L) + .setOrderNo("RK202605120001") + .setOrderType(WmsOrderTypeEnum.RECEIPT.getType()) + .setItems(Collections.singletonList(new WmsInventoryChangeReqDTO.Item() + .setSkuId(skuId) + .setWarehouseId(warehouseId) + .setQuantity(new BigDecimal(quantity)) + .setPrice(new BigDecimal("100.00")) + .setTotalPrice(new BigDecimal(quantity).multiply(new BigDecimal("100.00"))) + .setRemark("测试入库"))); + } + + private static WmsInventoryCheckReqDTO createCheckReq(Long inventoryId, Long skuId, Long warehouseId, + String quantity, String checkQuantity) { + return new WmsInventoryCheckReqDTO() + .setOrderId(2L) + .setOrderNo("PK202605120001") + .setOrderType(WmsOrderTypeEnum.CHECK.getType()) + .setItems(Collections.singletonList(new WmsInventoryCheckReqDTO.Item() + .setInventoryId(inventoryId) + .setSkuId(skuId) + .setWarehouseId(warehouseId) + .setQuantity(new BigDecimal(quantity)) + .setCheckQuantity(new BigDecimal(checkQuantity)) + .setPrice(new BigDecimal("100.00")) + .setRemark("测试盘库"))); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImplTest.java new file mode 100644 index 000000000..9571961a3 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemBrandServiceImplTest.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.wms.service.md.item; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand.WmsItemBrandSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemBrandDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.item.WmsItemBrandMapper; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.ITEM_BRAND_NAME_DUPLICATE; + +@Import(WmsItemBrandServiceImpl.class) +public class WmsItemBrandServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsItemBrandServiceImpl brandService; + + @Resource + private WmsItemBrandMapper brandMapper; + + @MockitoBean + private WmsItemService itemService; + + @Test + public void testCreateItemBrand_nameDuplicate() { + // mock 数据 + brandMapper.insert(createBrand("B001", "华为")); + WmsItemBrandSaveReqVO reqVO = createBrandSaveReqVO(null, "B002", "华为"); + + // 调用,并断言 + assertServiceException(() -> brandService.createItemBrand(reqVO), ITEM_BRAND_NAME_DUPLICATE); + } + + @Test + public void testUpdateItemBrand_nameDuplicate() { + // mock 数据 + brandMapper.insert(createBrand("B001", "华为")); + WmsItemBrandDO brand = createBrand("B002", "小米"); + brandMapper.insert(brand); + WmsItemBrandSaveReqVO reqVO = createBrandSaveReqVO(brand.getId(), "B002", "华为"); + + // 调用,并断言 + assertServiceException(() -> brandService.updateItemBrand(reqVO), ITEM_BRAND_NAME_DUPLICATE); + } + + private static WmsItemBrandDO createBrand(String code, String name) { + return WmsItemBrandDO.builder() + .code(code) + .name(name) + .build(); + } + + private static WmsItemBrandSaveReqVO createBrandSaveReqVO(Long id, String code, String name) { + WmsItemBrandSaveReqVO reqVO = new WmsItemBrandSaveReqVO(); + reqVO.setId(id); + reqVO.setCode(code); + reqVO.setName(name); + return reqVO; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImplTest.java new file mode 100644 index 000000000..ff4ea7019 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/md/warehouse/WmsWarehouseServiceImplTest.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.wms.service.md.warehouse; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo.WmsWarehouseSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.md.warehouse.WmsWarehouseDO; +import cn.iocoder.yudao.module.wms.dal.mysql.md.warehouse.WmsWarehouseMapper; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.order.check.WmsCheckOrderService; +import cn.iocoder.yudao.module.wms.service.order.movement.WmsMovementOrderService; +import cn.iocoder.yudao.module.wms.service.order.receipt.WmsReceiptOrderService; +import cn.iocoder.yudao.module.wms.service.order.shipment.WmsShipmentOrderService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.WAREHOUSE_CODE_DUPLICATE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.WAREHOUSE_HAS_INVENTORY; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@Import(WmsWarehouseServiceImpl.class) +public class WmsWarehouseServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsWarehouseServiceImpl warehouseService; + + @Resource + private WmsWarehouseMapper warehouseMapper; + + @MockitoBean + private WmsInventoryService inventoryService; + @MockitoBean + private WmsReceiptOrderService receiptOrderService; + @MockitoBean + private WmsShipmentOrderService shipmentOrderService; + @MockitoBean + private WmsMovementOrderService movementOrderService; + @MockitoBean + private WmsCheckOrderService checkOrderService; + + @Test + public void testCreateWarehouse_codeDuplicate() { + // mock 数据 + WmsWarehouseDO warehouse = createWarehouse("WH001", "成品仓"); + warehouseMapper.insert(warehouse); + + // 调用,并断言 + WmsWarehouseSaveReqVO reqVO = createWarehouseSaveReqVO(); + assertServiceException(() -> warehouseService.createWarehouse(reqVO), WAREHOUSE_CODE_DUPLICATE); + } + + @Test + public void testUpdateWarehouse_codeDuplicate() { + // mock 数据 + WmsWarehouseDO warehouse = createWarehouse("WH001", "成品仓"); + warehouseMapper.insert(warehouse); + WmsWarehouseDO updateWarehouse = createWarehouse("WH002", "原料仓"); + warehouseMapper.insert(updateWarehouse); + + // 调用,并断言 + WmsWarehouseSaveReqVO reqVO = createWarehouseSaveReqVO(); + reqVO.setId(updateWarehouse.getId()); + assertServiceException(() -> warehouseService.updateWarehouse(reqVO), WAREHOUSE_CODE_DUPLICATE); + } + + @Test + public void testDeleteWarehouse_hasInventory() { + // mock 数据 + WmsWarehouseDO warehouse = createWarehouse(); + warehouseMapper.insert(warehouse); + when(inventoryService.getInventoryCountByWarehouseId(warehouse.getId())).thenReturn(1L); + + // 调用,并断言 + assertServiceException(() -> warehouseService.deleteWarehouse(warehouse.getId()), WAREHOUSE_HAS_INVENTORY); + assertNotNull(warehouseMapper.selectById(warehouse.getId())); + } + + private static WmsWarehouseDO createWarehouse() { + return createWarehouse("WH001", "成品仓"); + } + + private static WmsWarehouseDO createWarehouse(String code, String name) { + return WmsWarehouseDO.builder() + .code(code) + .name(name) + .sort(1) + .build(); + } + + private static WmsWarehouseSaveReqVO createWarehouseSaveReqVO() { + WmsWarehouseSaveReqVO reqVO = new WmsWarehouseSaveReqVO(); + reqVO.setCode("WH001"); + reqVO.setName("半成品仓"); + reqVO.setSort(1); + return reqVO; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImplTest.java new file mode 100644 index 000000000..fe3aae638 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderDetailServiceImplTest.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.wms.service.order.check; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.check.WmsCheckOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.CHECK_ORDER_DETAIL_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Import(WmsCheckOrderDetailServiceImpl.class) +public class WmsCheckOrderDetailServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsCheckOrderDetailServiceImpl checkOrderDetailService; + + @Resource + private WmsCheckOrderDetailMapper checkOrderDetailMapper; + + @MockitoBean + private WmsItemSkuService itemSkuService; + + @Test + public void testCreateCheckOrderDetailList_success() { + // mock 数据 + Long orderId = 10L; + WmsCheckOrderSaveReqVO reqVO = createCheckOrderReqVO( + createCheckOrderDetailReqVO(null, 1001L, "10.00", "9.00"), + createCheckOrderDetailReqVO(null, 1002L, "20.00", "21.00")); + + // 调用 + checkOrderDetailService.createCheckOrderDetailList(orderId, reqVO); + + // 断言 + List details = checkOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(orderId, details.get(0).getOrderId()); + assertEquals(orderId, details.get(1).getOrderId()); + assertEquals(100L, details.get(0).getWarehouseId()); + } + + @Test + public void testCreateCheckOrderDetailList_ignoreId() { + // mock 数据 + WmsCheckOrderSaveReqVO reqVO = createCheckOrderReqVO( + createCheckOrderDetailReqVO(999L, 1001L, "10.00", "9.00")); + + // 调用 + checkOrderDetailService.createCheckOrderDetailList(10L, reqVO); + + // 断言 + List details = checkOrderDetailMapper.selectListByOrderId(10L); + assertEquals(1, details.size()); + assertNotNull(details.get(0).getId()); + assertEquals(1001L, details.get(0).getSkuId()); + } + + @Test + public void testUpdateCheckOrderDetailList_diff() { + // mock 数据 + Long orderId = 10L; + WmsCheckOrderDetailDO detail01 = createCheckOrderDetail(orderId, 1001L, "10.00", "9.00"); + WmsCheckOrderDetailDO detail02 = createCheckOrderDetail(orderId, 1002L, "20.00", "21.00"); + checkOrderDetailMapper.insert(detail01); + checkOrderDetailMapper.insert(detail02); + WmsCheckOrderSaveReqVO reqVO = createCheckOrderReqVO( + createCheckOrderDetailReqVO(detail01.getId(), 2001L, "11.00", "8.00"), + createCheckOrderDetailReqVO(null, 2002L, "22.00", "25.00")); + + // 调用 + checkOrderDetailService.updateCheckOrderDetailList(orderId, reqVO); + + // 断言:修改 + WmsCheckOrderDetailDO dbUpdateDetail = checkOrderDetailMapper.selectById(detail01.getId()); + assertNotNull(dbUpdateDetail); + assertEquals(orderId, dbUpdateDetail.getOrderId()); + assertEquals(2001L, dbUpdateDetail.getSkuId()); + assertEquals(0, new BigDecimal("11.00").compareTo(dbUpdateDetail.getQuantity())); + assertEquals(0, new BigDecimal("8.00").compareTo(dbUpdateDetail.getCheckQuantity())); + assertEquals(0, new BigDecimal("100.00").compareTo(dbUpdateDetail.getPrice())); + assertEquals(100L, dbUpdateDetail.getWarehouseId()); + // 断言:新增 + List details = checkOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + WmsCheckOrderDetailDO dbCreateDetail = details.stream() + .filter(detail -> Long.valueOf(2002L).equals(detail.getSkuId())) + .findFirst().orElse(null); + assertNotNull(dbCreateDetail); + assertEquals(orderId, dbCreateDetail.getOrderId()); + assertEquals(0, new BigDecimal("22.00").compareTo(dbCreateDetail.getQuantity())); + assertEquals(0, new BigDecimal("25.00").compareTo(dbCreateDetail.getCheckQuantity())); + // 断言:删除 + assertNull(checkOrderDetailMapper.selectById(detail02.getId())); + } + + @Test + public void testUpdateCheckOrderDetailList_detailNotExists() { + // mock 数据 + WmsCheckOrderSaveReqVO reqVO = createCheckOrderReqVO( + createCheckOrderDetailReqVO(999L, 1001L, "10.00", "9.00")); + + // 调用,并断言 + assertServiceException(() -> checkOrderDetailService.updateCheckOrderDetailList(10L, reqVO), + CHECK_ORDER_DETAIL_NOT_EXISTS); + } + + private static WmsCheckOrderSaveReqVO createCheckOrderReqVO(WmsCheckOrderDetailSaveReqVO... details) { + WmsCheckOrderSaveReqVO reqVO = new WmsCheckOrderSaveReqVO(); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setWarehouseId(100L); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsCheckOrderDetailSaveReqVO createCheckOrderDetailReqVO(Long id, Long skuId, + String quantity, String checkQuantity) { + WmsCheckOrderDetailSaveReqVO reqVO = new WmsCheckOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setInventoryId(300L); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setCheckQuantity(new BigDecimal(checkQuantity)); + reqVO.setPrice(new BigDecimal("100.00")); + return reqVO; + } + + private static WmsCheckOrderDetailDO createCheckOrderDetail(Long orderId, Long skuId, + String quantity, String checkQuantity) { + return WmsCheckOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .warehouseId(100L) + .inventoryId(300L) + .quantity(new BigDecimal(quantity)) + .checkQuantity(new BigDecimal(checkQuantity)) + .price(new BigDecimal("100.00")) + .build(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImplTest.java new file mode 100644 index 000000000..d1233f12a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/check/WmsCheckOrderServiceImplTest.java @@ -0,0 +1,347 @@ +package cn.iocoder.yudao.module.wms.service.order.check; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order.WmsCheckOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.check.WmsCheckOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.check.WmsCheckOrderDetailMapper; +import cn.iocoder.yudao.module.wms.dal.mysql.order.check.WmsCheckOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryCheckReqDTO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.CHECK_ORDER_DETAIL_REQUIRED; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.CHECK_ORDER_INVENTORY_CHANGED; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.CHECK_ORDER_STATUS_NOT_DELETABLE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.CHECK_ORDER_STATUS_NOT_PREPARE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@Import({WmsCheckOrderServiceImpl.class, WmsCheckOrderDetailServiceImpl.class}) +public class WmsCheckOrderServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsCheckOrderServiceImpl checkOrderService; + + @Resource + private WmsCheckOrderMapper checkOrderMapper; + @Resource + private WmsCheckOrderDetailMapper checkOrderDetailMapper; + + @MockitoBean + private WmsWarehouseService warehouseService; + @MockitoBean + private WmsItemSkuService itemSkuService; + @MockitoBean + private WmsInventoryService inventoryService; + + @Test + public void testCreateCheckOrder_calculateTotals() { + // mock 数据 + WmsCheckOrderSaveReqVO reqVO = createCheckOrderReqVO(100L, + createCheckOrderDetailReqVO(null, 200L, "10.00", "7.00", "5.00"), + createCheckOrderDetailReqVO(null, 201L, "20.00", "25.00", "2.00")); + + // 调用 + Long orderId = checkOrderService.createCheckOrder(reqVO); + + // 断言:盘库单汇总由后端计算 + WmsCheckOrderDO order = checkOrderMapper.selectById(orderId); + assertNotNull(order); + assertEquals(0, new BigDecimal("2.00").compareTo(order.getTotalQuantity())); + assertEquals(0, new BigDecimal("90.00").compareTo(order.getTotalPrice())); + assertEquals(0, new BigDecimal("85.00").compareTo(order.getActualPrice())); + // 断言:盘库明细保存单价 + List details = checkOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(0, new BigDecimal("5.00").compareTo(details.get(0).getPrice())); + } + + @Test + public void testUpdateCheckOrder_calculateTotals() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L); + checkOrderMapper.insert(order); + WmsCheckOrderDetailDO oldDetail = createCheckOrderDetail(order.getId(), 200L, 100L, "10.00", "7.00"); + checkOrderDetailMapper.insert(oldDetail); + WmsCheckOrderSaveReqVO reqVO = createCheckOrderReqVO(100L, + createCheckOrderDetailReqVO(oldDetail.getId(), 200L, "8.00", "10.00", "6.00"), + createCheckOrderDetailReqVO(null, 201L, "2.00", "1.00", "4.00")); + reqVO.setId(order.getId()); + reqVO.setNo(order.getNo()); + + // 调用 + checkOrderService.updateCheckOrder(reqVO); + + // 断言:盘库单汇总由后端重新计算 + WmsCheckOrderDO dbOrder = checkOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(0, new BigDecimal("1.00").compareTo(dbOrder.getTotalQuantity())); + assertEquals(0, new BigDecimal("56.00").compareTo(dbOrder.getTotalPrice())); + assertEquals(0, new BigDecimal("64.00").compareTo(dbOrder.getActualPrice())); + List details = checkOrderDetailMapper.selectListByOrderId(order.getId()); + assertEquals(2, details.size()); + } + + @Test + public void testCompleteCheckOrder_success() { + // mock 数据 + Long warehouseId = 100L; + Long skuId = 200L; + WmsCheckOrderDO order = createCheckOrder(warehouseId); + checkOrderMapper.insert(order); + checkOrderDetailMapper.insert(createCheckOrderDetail(order.getId(), skuId, warehouseId, + "10.00", "7.00")); + + // 调用 + checkOrderService.completeCheckOrder(order.getId()); + + // 断言:盘库单 + WmsCheckOrderDO dbOrder = checkOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.FINISHED.getStatus(), dbOrder.getStatus()); + // 断言:库存盘点 + ArgumentCaptor captor = ArgumentCaptor.forClass(WmsInventoryCheckReqDTO.class); + verify(inventoryService).checkInventory(captor.capture()); + WmsInventoryCheckReqDTO inventoryReqDTO = captor.getValue(); + assertEquals(order.getId(), inventoryReqDTO.getOrderId()); + assertEquals(order.getNo(), inventoryReqDTO.getOrderNo()); + assertEquals(WmsOrderTypeEnum.CHECK.getType(), inventoryReqDTO.getOrderType()); + assertEquals(1, inventoryReqDTO.getItems().size()); + assertEquals(skuId, inventoryReqDTO.getItems().get(0).getSkuId()); + assertEquals(warehouseId, inventoryReqDTO.getItems().get(0).getWarehouseId()); + assertEquals(300L, inventoryReqDTO.getItems().get(0).getInventoryId()); + assertEquals(0, new BigDecimal("10.00").compareTo(inventoryReqDTO.getItems().get(0).getQuantity())); + assertEquals(0, new BigDecimal("7.00").compareTo(inventoryReqDTO.getItems().get(0).getCheckQuantity())); + assertEquals(0, new BigDecimal("30.00").compareTo(inventoryReqDTO.getItems().get(0).getPrice())); + } + + @Test + public void testCompleteCheckOrder_zeroDifference() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L); + checkOrderMapper.insert(order); + checkOrderDetailMapper.insert(createCheckOrderDetail(order.getId(), 200L, 100L, + "10.00", "10.00")); + + // 调用 + checkOrderService.completeCheckOrder(order.getId()); + + // 断言 + WmsCheckOrderDO dbOrder = checkOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.FINISHED.getStatus(), dbOrder.getStatus()); + verify(inventoryService).checkInventory(any()); + } + + @Test + public void testCompleteCheckOrder_newInventoryCurrentMissing() { + // mock 数据 + Long warehouseId = 100L; + Long skuId = 200L; + WmsCheckOrderDO order = createCheckOrder(warehouseId); + checkOrderMapper.insert(order); + checkOrderDetailMapper.insert(createNewInventoryCheckOrderDetail(order.getId(), skuId, warehouseId, + "0.00", "5.00")); + // 调用 + checkOrderService.completeCheckOrder(order.getId()); + + // 断言 + ArgumentCaptor captor = ArgumentCaptor.forClass(WmsInventoryCheckReqDTO.class); + verify(inventoryService).checkInventory(captor.capture()); + WmsInventoryCheckReqDTO inventoryReqDTO = captor.getValue(); + assertEquals(1, inventoryReqDTO.getItems().size()); + assertEquals(skuId, inventoryReqDTO.getItems().get(0).getSkuId()); + assertEquals(warehouseId, inventoryReqDTO.getItems().get(0).getWarehouseId()); + assertNull(inventoryReqDTO.getItems().get(0).getInventoryId()); + assertEquals(0, new BigDecimal("0.00").compareTo(inventoryReqDTO.getItems().get(0).getQuantity())); + assertEquals(0, new BigDecimal("5.00").compareTo(inventoryReqDTO.getItems().get(0).getCheckQuantity())); + } + + @Test + public void testCompleteCheckOrder_checkInventoryChanged() { + // mock 数据 + Long warehouseId = 100L; + Long skuId = 200L; + WmsCheckOrderDO order = createCheckOrder(warehouseId); + checkOrderMapper.insert(order); + checkOrderDetailMapper.insert(createNewInventoryCheckOrderDetail(order.getId(), skuId, warehouseId, + "0.00", "5.00")); + doThrow(exception(CHECK_ORDER_INVENTORY_CHANGED)).when(inventoryService).checkInventory(any()); + + // 调用,并断言 + assertServiceException(() -> checkOrderService.completeCheckOrder(order.getId()), + CHECK_ORDER_INVENTORY_CHANGED); + assertEquals(WmsOrderStatusEnum.PREPARE.getStatus(), + checkOrderMapper.selectById(order.getId()).getStatus()); + } + + @Test + public void testCompleteCheckOrder_detailRequired() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L); + checkOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> checkOrderService.completeCheckOrder(order.getId()), + CHECK_ORDER_DETAIL_REQUIRED); + verify(inventoryService, never()).checkInventory(any()); + } + + @Test + public void testCompleteCheckOrder_duplicateComplete() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L); + checkOrderMapper.insert(order); + checkOrderDetailMapper.insert(createCheckOrderDetail(order.getId(), 200L, 100L, + "10.00", "7.00")); + + // 调用 + checkOrderService.completeCheckOrder(order.getId()); + + // 调用,并断言:二次完成不能再次写库存 + assertServiceException(() -> checkOrderService.completeCheckOrder(order.getId()), + CHECK_ORDER_STATUS_NOT_PREPARE); + verify(inventoryService).checkInventory(any()); + } + + @Test + public void testCancelCheckOrder_success() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L); + checkOrderMapper.insert(order); + + // 调用 + checkOrderService.cancelCheckOrder(order.getId()); + + // 断言 + WmsCheckOrderDO dbOrder = checkOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.CANCELED.getStatus(), dbOrder.getStatus()); + verify(inventoryService, never()).checkInventory(any()); + } + + @Test + public void testUpdateByIdAndStatus_statusNotMatch() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L); + checkOrderMapper.insert(order); + + // 调用 + int updateCount = checkOrderMapper.updateByIdAndStatus(order.getId(), + WmsOrderStatusEnum.FINISHED.getStatus(), + new WmsCheckOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())); + + // 断言 + assertEquals(0, updateCount); + assertEquals(WmsOrderStatusEnum.PREPARE.getStatus(), + checkOrderMapper.selectById(order.getId()).getStatus()); + } + + @Test + public void testDeleteCheckOrder_canceled() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L).setStatus(WmsOrderStatusEnum.CANCELED.getStatus()); + checkOrderMapper.insert(order); + checkOrderDetailMapper.insert(createCheckOrderDetail(order.getId(), 200L, 100L, + "10.00", "8.00")); + + // 调用 + checkOrderService.deleteCheckOrder(order.getId()); + + // 断言 + assertNull(checkOrderMapper.selectById(order.getId())); + assertEquals(0, checkOrderDetailMapper.selectListByOrderId(order.getId()).size()); + } + + @Test + public void testDeleteCheckOrder_finished() { + // mock 数据 + WmsCheckOrderDO order = createCheckOrder(100L).setStatus(WmsOrderStatusEnum.FINISHED.getStatus()); + checkOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> checkOrderService.deleteCheckOrder(order.getId()), + CHECK_ORDER_STATUS_NOT_DELETABLE); + } + + private static WmsCheckOrderDO createCheckOrder(Long warehouseId) { + return new WmsCheckOrderDO() + .setNo("PK202605120001") + .setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()) + .setWarehouseId(warehouseId) + .setTotalQuantity(new BigDecimal("-3.00")) + .setTotalPrice(new BigDecimal("300.00")) + .setActualPrice(new BigDecimal("210.00")); + } + + private static WmsCheckOrderDetailDO createCheckOrderDetail(Long orderId, Long skuId, Long warehouseId, + String quantity, String checkQuantity) { + return WmsCheckOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .warehouseId(warehouseId) + .inventoryId(300L) + .quantity(new BigDecimal(quantity)) + .checkQuantity(new BigDecimal(checkQuantity)) + .price(new BigDecimal("30.00")) + .build(); + } + + private static WmsCheckOrderDetailDO createNewInventoryCheckOrderDetail(Long orderId, Long skuId, Long warehouseId, + String quantity, String checkQuantity) { + return WmsCheckOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .warehouseId(warehouseId) + .quantity(new BigDecimal(quantity)) + .checkQuantity(new BigDecimal(checkQuantity)) + .price(new BigDecimal("30.00")) + .build(); + } + + private static WmsCheckOrderSaveReqVO createCheckOrderReqVO(Long warehouseId, WmsCheckOrderDetailSaveReqVO... details) { + WmsCheckOrderSaveReqVO reqVO = new WmsCheckOrderSaveReqVO(); + reqVO.setNo("PK202605120002"); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setWarehouseId(warehouseId); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsCheckOrderDetailSaveReqVO createCheckOrderDetailReqVO(Long id, Long skuId, + String quantity, String checkQuantity, + String price) { + WmsCheckOrderDetailSaveReqVO reqVO = new WmsCheckOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setInventoryId(300L); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setCheckQuantity(new BigDecimal(checkQuantity)); + reqVO.setPrice(new BigDecimal(price)); + return reqVO; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImplTest.java new file mode 100644 index 000000000..39d278189 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderDetailServiceImplTest.java @@ -0,0 +1,155 @@ +package cn.iocoder.yudao.module.wms.service.order.movement; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.movement.WmsMovementOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.MOVEMENT_ORDER_DETAIL_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Import(WmsMovementOrderDetailServiceImpl.class) +public class WmsMovementOrderDetailServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsMovementOrderDetailServiceImpl movementOrderDetailService; + + @Resource + private WmsMovementOrderDetailMapper movementOrderDetailMapper; + + @MockitoBean + private WmsItemSkuService itemSkuService; + + @Test + public void testCreateMovementOrderDetailList_success() { + // mock 数据 + Long orderId = 10L; + WmsMovementOrderSaveReqVO reqVO = createMovementOrderReqVO( + createMovementOrderDetailReqVO(null, 1001L, "1.00"), + createMovementOrderDetailReqVO(null, 1002L, "2.00")); + + // 调用 + movementOrderDetailService.createMovementOrderDetailList(orderId, reqVO); + + // 断言 + List details = movementOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(orderId, details.get(0).getOrderId()); + assertEquals(orderId, details.get(1).getOrderId()); + assertEquals(100L, details.get(0).getSourceWarehouseId()); + assertEquals(200L, details.get(0).getTargetWarehouseId()); + assertEquals(0, new BigDecimal("100.00").compareTo(details.get(0).getTotalPrice())); + assertEquals(0, new BigDecimal("200.00").compareTo(details.get(1).getTotalPrice())); + } + + @Test + public void testCreateMovementOrderDetailList_ignoreId() { + // mock 数据 + WmsMovementOrderSaveReqVO reqVO = createMovementOrderReqVO( + createMovementOrderDetailReqVO(999L, 1001L, "1.00")); + + // 调用 + movementOrderDetailService.createMovementOrderDetailList(10L, reqVO); + + // 断言 + List details = movementOrderDetailMapper.selectListByOrderId(10L); + assertEquals(1, details.size()); + assertNotNull(details.get(0).getId()); + assertEquals(1001L, details.get(0).getSkuId()); + } + + @Test + public void testUpdateMovementOrderDetailList_diff() { + // mock 数据 + Long orderId = 10L; + WmsMovementOrderDetailDO detail01 = createMovementOrderDetail(orderId, 1001L, "1.00"); + WmsMovementOrderDetailDO detail02 = createMovementOrderDetail(orderId, 1002L, "2.00"); + movementOrderDetailMapper.insert(detail01); + movementOrderDetailMapper.insert(detail02); + WmsMovementOrderSaveReqVO reqVO = createMovementOrderReqVO( + createMovementOrderDetailReqVO(detail01.getId(), 2001L, "11.00"), + createMovementOrderDetailReqVO(null, 2002L, "22.00")); + + // 调用 + movementOrderDetailService.updateMovementOrderDetailList(orderId, reqVO); + + // 断言:修改 + WmsMovementOrderDetailDO dbUpdateDetail = movementOrderDetailMapper.selectById(detail01.getId()); + assertNotNull(dbUpdateDetail); + assertEquals(orderId, dbUpdateDetail.getOrderId()); + assertEquals(2001L, dbUpdateDetail.getSkuId()); + assertEquals(0, new BigDecimal("11.00").compareTo(dbUpdateDetail.getQuantity())); + assertEquals(0, new BigDecimal("1100.00").compareTo(dbUpdateDetail.getTotalPrice())); + assertEquals(100L, dbUpdateDetail.getSourceWarehouseId()); + assertEquals(200L, dbUpdateDetail.getTargetWarehouseId()); + // 断言:新增 + List details = movementOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + WmsMovementOrderDetailDO dbCreateDetail = details.stream() + .filter(detail -> Long.valueOf(2002L).equals(detail.getSkuId())) + .findFirst().orElse(null); + assertNotNull(dbCreateDetail); + assertEquals(orderId, dbCreateDetail.getOrderId()); + assertEquals(0, new BigDecimal("22.00").compareTo(dbCreateDetail.getQuantity())); + assertEquals(0, new BigDecimal("2200.00").compareTo(dbCreateDetail.getTotalPrice())); + // 断言:删除 + assertNull(movementOrderDetailMapper.selectById(detail02.getId())); + } + + @Test + public void testUpdateMovementOrderDetailList_detailNotExists() { + // mock 数据 + WmsMovementOrderSaveReqVO reqVO = createMovementOrderReqVO( + createMovementOrderDetailReqVO(999L, 1001L, "1.00")); + + // 调用,并断言 + assertServiceException(() -> movementOrderDetailService.updateMovementOrderDetailList(10L, reqVO), + MOVEMENT_ORDER_DETAIL_NOT_EXISTS); + } + + private static WmsMovementOrderSaveReqVO createMovementOrderReqVO(WmsMovementOrderDetailSaveReqVO... details) { + WmsMovementOrderSaveReqVO reqVO = new WmsMovementOrderSaveReqVO(); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setSourceWarehouseId(100L); + reqVO.setTargetWarehouseId(200L); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsMovementOrderDetailSaveReqVO createMovementOrderDetailReqVO(Long id, Long skuId, String quantity) { + WmsMovementOrderDetailSaveReqVO reqVO = new WmsMovementOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setPrice(new BigDecimal("100.00")); + reqVO.setTotalPrice(new BigDecimal(quantity).multiply(new BigDecimal("100.00"))); + return reqVO; + } + + private static WmsMovementOrderDetailDO createMovementOrderDetail(Long orderId, Long skuId, String quantity) { + return WmsMovementOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .sourceWarehouseId(100L) + .targetWarehouseId(200L) + .quantity(new BigDecimal(quantity)) + .price(new BigDecimal("100.00")) + .totalPrice(new BigDecimal(quantity).multiply(new BigDecimal("100.00"))) + .build(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImplTest.java new file mode 100644 index 000000000..a7222f3f5 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/movement/WmsMovementOrderServiceImplTest.java @@ -0,0 +1,291 @@ +package cn.iocoder.yudao.module.wms.service.order.movement; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order.WmsMovementOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.movement.WmsMovementOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.movement.WmsMovementOrderDetailMapper; +import cn.iocoder.yudao.module.wms.dal.mysql.order.movement.WmsMovementOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.MOVEMENT_ORDER_DETAIL_REQUIRED; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.MOVEMENT_ORDER_STATUS_NOT_DELETABLE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.MOVEMENT_ORDER_STATUS_NOT_PREPARE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.MOVEMENT_ORDER_WAREHOUSE_SAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@Import({WmsMovementOrderServiceImpl.class, WmsMovementOrderDetailServiceImpl.class}) +public class WmsMovementOrderServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsMovementOrderServiceImpl movementOrderService; + + @Resource + private WmsMovementOrderMapper movementOrderMapper; + @Resource + private WmsMovementOrderDetailMapper movementOrderDetailMapper; + + @MockitoBean + private WmsWarehouseService warehouseService; + @MockitoBean + private WmsItemSkuService itemSkuService; + @MockitoBean + private WmsInventoryService inventoryService; + + @Test + public void testCreateMovementOrder_calculateTotal() { + // mock 数据 + WmsMovementOrderSaveReqVO reqVO = createMovementOrderReqVO(null, 100L, 200L, + createMovementOrderDetailReqVO(null, 3001L, "1.50", "10.00", "16.00"), + createMovementOrderDetailReqVO(null, 3002L, "2.50", "20.00", "51.00")); + + // 调用 + Long orderId = movementOrderService.createMovementOrder(reqVO); + + // 断言 + WmsMovementOrderDO dbOrder = movementOrderMapper.selectById(orderId); + assertNotNull(dbOrder); + assertEquals(0, new BigDecimal("4.00").compareTo(dbOrder.getTotalQuantity())); + assertEquals(0, new BigDecimal("67.00").compareTo(dbOrder.getTotalPrice())); + List details = movementOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(0, new BigDecimal("16.00").compareTo(details.get(0).getTotalPrice())); + } + + @Test + public void testUpdateMovementOrder_calculateTotal() { + // mock 数据 + WmsMovementOrderDO order = createMovementOrder(100L, 200L); + movementOrderMapper.insert(order); + WmsMovementOrderSaveReqVO reqVO = createMovementOrderReqVO(order.getId(), 100L, 200L, + createMovementOrderDetailReqVO(null, 3001L, "3.00", "30.00", "88.00")); + + // 调用 + movementOrderService.updateMovementOrder(reqVO); + + // 断言 + WmsMovementOrderDO dbOrder = movementOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(0, new BigDecimal("3.00").compareTo(dbOrder.getTotalQuantity())); + assertEquals(0, new BigDecimal("88.00").compareTo(dbOrder.getTotalPrice())); + List details = movementOrderDetailMapper.selectListByOrderId(order.getId()); + assertEquals(1, details.size()); + assertEquals(0, new BigDecimal("88.00").compareTo(details.get(0).getTotalPrice())); + } + + @Test + public void testCompleteMovementOrder_success() { + // mock 数据 + Long sourceWarehouseId = 100L; + Long targetWarehouseId = 200L; + Long skuId = 300L; + WmsMovementOrderDO order = createMovementOrder(sourceWarehouseId, targetWarehouseId); + movementOrderMapper.insert(order); + movementOrderDetailMapper.insert(createMovementOrderDetail(order.getId(), skuId, + sourceWarehouseId, targetWarehouseId)); + + // 调用 + movementOrderService.completeMovementOrder(order.getId()); + + // 断言:移库单 + WmsMovementOrderDO dbOrder = movementOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.FINISHED.getStatus(), dbOrder.getStatus()); + // 断言:库存变更 + ArgumentCaptor captor = ArgumentCaptor.forClass(WmsInventoryChangeReqDTO.class); + verify(inventoryService).changeInventory(captor.capture()); + WmsInventoryChangeReqDTO inventoryReqDTO = captor.getValue(); + assertEquals(order.getId(), inventoryReqDTO.getOrderId()); + assertEquals(order.getNo(), inventoryReqDTO.getOrderNo()); + assertEquals(WmsOrderTypeEnum.MOVEMENT.getType(), inventoryReqDTO.getOrderType()); + assertEquals(2, inventoryReqDTO.getItems().size()); + assertEquals(skuId, inventoryReqDTO.getItems().get(0).getSkuId()); + assertEquals(sourceWarehouseId, inventoryReqDTO.getItems().get(0).getWarehouseId()); + assertEquals(0, new BigDecimal("-2.00").compareTo(inventoryReqDTO.getItems().get(0).getQuantity())); + assertEquals(0, new BigDecimal("-40.00").compareTo(inventoryReqDTO.getItems().get(0).getTotalPrice())); + assertEquals(skuId, inventoryReqDTO.getItems().get(1).getSkuId()); + assertEquals(targetWarehouseId, inventoryReqDTO.getItems().get(1).getWarehouseId()); + assertEquals(0, new BigDecimal("2.00").compareTo(inventoryReqDTO.getItems().get(1).getQuantity())); + assertEquals(0, new BigDecimal("40.00").compareTo(inventoryReqDTO.getItems().get(1).getTotalPrice())); + } + + @Test + public void testCompleteMovementOrder_detailRequired() { + // mock 数据 + WmsMovementOrderDO order = createMovementOrder(100L, 200L); + movementOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> movementOrderService.completeMovementOrder(order.getId()), + MOVEMENT_ORDER_DETAIL_REQUIRED); + verify(inventoryService, never()).changeInventory(any()); + } + + @Test + public void testCompleteMovementOrder_duplicateComplete() { + // mock 数据 + WmsMovementOrderDO order = createMovementOrder(100L, 200L); + movementOrderMapper.insert(order); + movementOrderDetailMapper.insert(createMovementOrderDetail(order.getId(), 300L, 100L, 200L)); + + // 调用 + movementOrderService.completeMovementOrder(order.getId()); + + // 调用,并断言:二次完成不能再次写库存 + assertServiceException(() -> movementOrderService.completeMovementOrder(order.getId()), + MOVEMENT_ORDER_STATUS_NOT_PREPARE); + verify(inventoryService).changeInventory(any()); + } + + @Test + public void testCreateMovementOrder_sameWarehouse() { + // 准备参数 + WmsMovementOrderSaveReqVO reqVO = createMovementOrderReqVO(100L, 100L); + + // 调用,并断言 + assertServiceException(() -> movementOrderService.createMovementOrder(reqVO), MOVEMENT_ORDER_WAREHOUSE_SAME); + verify(inventoryService, never()).changeInventory(any()); + } + + @Test + public void testCancelMovementOrder_success() { + // mock 数据 + WmsMovementOrderDO order = createMovementOrder(100L, 200L); + movementOrderMapper.insert(order); + + // 调用 + movementOrderService.cancelMovementOrder(order.getId()); + + // 断言 + WmsMovementOrderDO dbOrder = movementOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.CANCELED.getStatus(), dbOrder.getStatus()); + verify(inventoryService, never()).changeInventory(any()); + } + + @Test + public void testUpdateByIdAndStatus_statusNotMatch() { + // mock 数据 + WmsMovementOrderDO order = createMovementOrder(100L, 200L); + movementOrderMapper.insert(order); + + // 调用 + int updateCount = movementOrderMapper.updateByIdAndStatus(order.getId(), + WmsOrderStatusEnum.FINISHED.getStatus(), + new WmsMovementOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())); + + // 断言 + assertEquals(0, updateCount); + assertEquals(WmsOrderStatusEnum.PREPARE.getStatus(), + movementOrderMapper.selectById(order.getId()).getStatus()); + } + + @Test + public void testDeleteMovementOrder_canceled() { + // mock 数据 + WmsMovementOrderDO order = createMovementOrder(100L, 200L).setStatus(WmsOrderStatusEnum.CANCELED.getStatus()); + movementOrderMapper.insert(order); + movementOrderDetailMapper.insert(createMovementOrderDetail(order.getId(), 300L, 100L, 200L)); + + // 调用 + movementOrderService.deleteMovementOrder(order.getId()); + + // 断言 + assertNull(movementOrderMapper.selectById(order.getId())); + assertEquals(0, movementOrderDetailMapper.selectListByOrderId(order.getId()).size()); + } + + @Test + public void testDeleteMovementOrder_finished() { + // mock 数据 + WmsMovementOrderDO order = createMovementOrder(100L, 200L).setStatus(WmsOrderStatusEnum.FINISHED.getStatus()); + movementOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> movementOrderService.deleteMovementOrder(order.getId()), + MOVEMENT_ORDER_STATUS_NOT_DELETABLE); + } + + private static WmsMovementOrderDO createMovementOrder(Long sourceWarehouseId, Long targetWarehouseId) { + return new WmsMovementOrderDO() + .setNo("YK202605120001") + .setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()) + .setSourceWarehouseId(sourceWarehouseId) + .setTargetWarehouseId(targetWarehouseId) + .setTotalQuantity(new BigDecimal("2.00")) + .setTotalPrice(new BigDecimal("20.00")); + } + + private static WmsMovementOrderDetailDO createMovementOrderDetail(Long orderId, Long skuId, + Long sourceWarehouseId, Long targetWarehouseId) { + return WmsMovementOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .sourceWarehouseId(sourceWarehouseId) + .targetWarehouseId(targetWarehouseId) + .quantity(new BigDecimal("2.00")) + .price(new BigDecimal("20.00")) + .totalPrice(new BigDecimal("40.00")) + .build(); + } + + private static WmsMovementOrderSaveReqVO createMovementOrderReqVO(Long sourceWarehouseId, Long targetWarehouseId) { + return createMovementOrderReqVO(null, sourceWarehouseId, targetWarehouseId); + } + + private static WmsMovementOrderSaveReqVO createMovementOrderReqVO(Long id, Long sourceWarehouseId, + Long targetWarehouseId, + WmsMovementOrderDetailSaveReqVO... details) { + WmsMovementOrderSaveReqVO reqVO = new WmsMovementOrderSaveReqVO(); + reqVO.setId(id); + reqVO.setNo("YK202605120001"); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setSourceWarehouseId(sourceWarehouseId); + reqVO.setTargetWarehouseId(targetWarehouseId); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsMovementOrderDetailSaveReqVO createMovementOrderDetailReqVO(Long id, Long skuId, + String quantity, String price) { + return createMovementOrderDetailReqVO(id, skuId, quantity, price, null); + } + + private static WmsMovementOrderDetailSaveReqVO createMovementOrderDetailReqVO(Long id, Long skuId, + String quantity, String price, + String totalPrice) { + WmsMovementOrderDetailSaveReqVO reqVO = new WmsMovementOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setPrice(new BigDecimal(price)); + if (totalPrice != null) { + reqVO.setTotalPrice(new BigDecimal(totalPrice)); + } + return reqVO; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImplTest.java new file mode 100644 index 000000000..e8d93aa24 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderDetailServiceImplTest.java @@ -0,0 +1,149 @@ +package cn.iocoder.yudao.module.wms.service.order.receipt; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.receipt.WmsReceiptOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.RECEIPT_ORDER_DETAIL_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Import(WmsReceiptOrderDetailServiceImpl.class) +public class WmsReceiptOrderDetailServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsReceiptOrderDetailServiceImpl receiptOrderDetailService; + + @Resource + private WmsReceiptOrderDetailMapper receiptOrderDetailMapper; + + @MockitoBean + private WmsItemSkuService itemSkuService; + + @Test + public void testCreateReceiptOrderDetailList_success() { + // mock 数据 + Long orderId = 10L; + WmsReceiptOrderSaveReqVO reqVO = createReceiptOrderReqVO( + createReceiptOrderDetailReqVO(null, 1001L, "1.00"), + createReceiptOrderDetailReqVO(null, 1002L, "2.00")); + + // 调用 + receiptOrderDetailService.createReceiptOrderDetailList(orderId, reqVO); + + // 断言 + List details = receiptOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(orderId, details.get(0).getOrderId()); + assertEquals(orderId, details.get(1).getOrderId()); + assertEquals(0, new BigDecimal("100.00").compareTo(details.get(0).getTotalPrice())); + assertEquals(0, new BigDecimal("200.00").compareTo(details.get(1).getTotalPrice())); + } + + @Test + public void testCreateReceiptOrderDetailList_ignoreId() { + // mock 数据 + WmsReceiptOrderSaveReqVO reqVO = createReceiptOrderReqVO( + createReceiptOrderDetailReqVO(999L, 1001L, "1.00")); + + // 调用 + receiptOrderDetailService.createReceiptOrderDetailList(10L, reqVO); + + // 断言 + List details = receiptOrderDetailMapper.selectListByOrderId(10L); + assertEquals(1, details.size()); + assertNotNull(details.get(0).getId()); + assertEquals(1001L, details.get(0).getSkuId()); + } + + @Test + public void testUpdateReceiptOrderDetailList_diff() { + // mock 数据 + Long orderId = 10L; + WmsReceiptOrderDetailDO detail01 = createReceiptOrderDetail(orderId, 1001L, "1.00"); + WmsReceiptOrderDetailDO detail02 = createReceiptOrderDetail(orderId, 1002L, "2.00"); + receiptOrderDetailMapper.insert(detail01); + receiptOrderDetailMapper.insert(detail02); + WmsReceiptOrderSaveReqVO reqVO = createReceiptOrderReqVO( + createReceiptOrderDetailReqVO(detail01.getId(), 2001L, "11.00"), + createReceiptOrderDetailReqVO(null, 2002L, "22.00")); + + // 调用 + receiptOrderDetailService.updateReceiptOrderDetailList(orderId, reqVO); + + // 断言:修改 + WmsReceiptOrderDetailDO dbUpdateDetail = receiptOrderDetailMapper.selectById(detail01.getId()); + assertNotNull(dbUpdateDetail); + assertEquals(orderId, dbUpdateDetail.getOrderId()); + assertEquals(2001L, dbUpdateDetail.getSkuId()); + assertEquals(0, new BigDecimal("11.00").compareTo(dbUpdateDetail.getQuantity())); + assertEquals(0, new BigDecimal("1100.00").compareTo(dbUpdateDetail.getTotalPrice())); + // 断言:新增 + List details = receiptOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + WmsReceiptOrderDetailDO dbCreateDetail = details.stream() + .filter(detail -> Long.valueOf(2002L).equals(detail.getSkuId())) + .findFirst().orElse(null); + assertNotNull(dbCreateDetail); + assertEquals(orderId, dbCreateDetail.getOrderId()); + assertEquals(0, new BigDecimal("22.00").compareTo(dbCreateDetail.getQuantity())); + assertEquals(0, new BigDecimal("2200.00").compareTo(dbCreateDetail.getTotalPrice())); + // 断言:删除 + assertNull(receiptOrderDetailMapper.selectById(detail02.getId())); + } + + @Test + public void testUpdateReceiptOrderDetailList_detailNotExists() { + // mock 数据 + WmsReceiptOrderSaveReqVO reqVO = createReceiptOrderReqVO( + createReceiptOrderDetailReqVO(999L, 1001L, "1.00")); + + // 调用,并断言 + assertServiceException(() -> receiptOrderDetailService.updateReceiptOrderDetailList(10L, reqVO), + RECEIPT_ORDER_DETAIL_NOT_EXISTS); + } + + private static WmsReceiptOrderSaveReqVO createReceiptOrderReqVO(WmsReceiptOrderDetailSaveReqVO... details) { + WmsReceiptOrderSaveReqVO reqVO = new WmsReceiptOrderSaveReqVO(); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setWarehouseId(100L); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsReceiptOrderDetailSaveReqVO createReceiptOrderDetailReqVO(Long id, Long skuId, String quantity) { + WmsReceiptOrderDetailSaveReqVO reqVO = new WmsReceiptOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setPrice(new BigDecimal("100.00")); + reqVO.setTotalPrice(new BigDecimal(quantity).multiply(new BigDecimal("100.00"))); + return reqVO; + } + + private static WmsReceiptOrderDetailDO createReceiptOrderDetail(Long orderId, Long skuId, String quantity) { + return WmsReceiptOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .warehouseId(100L) + .quantity(new BigDecimal(quantity)) + .price(new BigDecimal("100.00")) + .totalPrice(new BigDecimal(quantity).multiply(new BigDecimal("100.00"))) + .build(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImplTest.java new file mode 100644 index 000000000..36f029c5c --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/receipt/WmsReceiptOrderServiceImplTest.java @@ -0,0 +1,271 @@ +package cn.iocoder.yudao.module.wms.service.order.receipt; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order.WmsReceiptOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.receipt.WmsReceiptOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.receipt.WmsReceiptOrderDetailMapper; +import cn.iocoder.yudao.module.wms.dal.mysql.order.receipt.WmsReceiptOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsReceiptOrderTypeEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.merchant.WmsMerchantService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.RECEIPT_ORDER_DETAIL_REQUIRED; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.RECEIPT_ORDER_STATUS_NOT_PREPARE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.RECEIPT_ORDER_STATUS_NOT_DELETABLE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@Import({WmsReceiptOrderServiceImpl.class, WmsReceiptOrderDetailServiceImpl.class}) +public class WmsReceiptOrderServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsReceiptOrderServiceImpl receiptOrderService; + + @Resource + private WmsReceiptOrderMapper receiptOrderMapper; + @Resource + private WmsReceiptOrderDetailMapper receiptOrderDetailMapper; + + @MockitoBean + private WmsWarehouseService warehouseService; + @MockitoBean + private WmsMerchantService merchantService; + @MockitoBean + private WmsItemSkuService itemSkuService; + @MockitoBean + private WmsInventoryService inventoryService; + + @Test + public void testCreateReceiptOrder_calculateTotal() { + // mock 数据 + WmsReceiptOrderSaveReqVO reqVO = createReceiptOrderSaveReqVO(null, + createReceiptOrderDetailReqVO(null, 2001L, "1.50", "10.00", "16.00"), + createReceiptOrderDetailReqVO(null, 2002L, "2.50", "20.00", "51.00")); + + // 调用 + Long orderId = receiptOrderService.createReceiptOrder(reqVO); + + // 断言 + WmsReceiptOrderDO dbOrder = receiptOrderMapper.selectById(orderId); + assertNotNull(dbOrder); + assertEquals(0, new BigDecimal("4.00").compareTo(dbOrder.getTotalQuantity())); + assertEquals(0, new BigDecimal("67.00").compareTo(dbOrder.getTotalPrice())); + List details = receiptOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(0, new BigDecimal("16.00").compareTo(details.get(0).getTotalPrice())); + } + + @Test + public void testUpdateReceiptOrder_calculateTotal() { + // mock 数据 + WmsReceiptOrderDO order = createReceiptOrder(100L); + receiptOrderMapper.insert(order); + WmsReceiptOrderSaveReqVO reqVO = createReceiptOrderSaveReqVO(order.getId(), + createReceiptOrderDetailReqVO(null, 2001L, "3.00", "30.00", "88.00")); + + // 调用 + receiptOrderService.updateReceiptOrder(reqVO); + + // 断言 + WmsReceiptOrderDO dbOrder = receiptOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(0, new BigDecimal("3.00").compareTo(dbOrder.getTotalQuantity())); + assertEquals(0, new BigDecimal("88.00").compareTo(dbOrder.getTotalPrice())); + List details = receiptOrderDetailMapper.selectListByOrderId(order.getId()); + assertEquals(1, details.size()); + assertEquals(0, new BigDecimal("88.00").compareTo(details.get(0).getTotalPrice())); + } + + @Test + public void testCompleteReceiptOrder_success() { + // mock 数据 + Long warehouseId = 100L; + Long skuId = 200L; + WmsReceiptOrderDO order = createReceiptOrder(warehouseId); + receiptOrderMapper.insert(order); + receiptOrderDetailMapper.insert(createReceiptOrderDetail(order.getId(), skuId, warehouseId)); + + // 调用 + receiptOrderService.completeReceiptOrder(order.getId()); + + // 断言:入库单 + WmsReceiptOrderDO dbOrder = receiptOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.FINISHED.getStatus(), dbOrder.getStatus()); + // 断言:库存变更 + ArgumentCaptor captor = ArgumentCaptor.forClass(WmsInventoryChangeReqDTO.class); + verify(inventoryService).changeInventory(captor.capture()); + WmsInventoryChangeReqDTO inventoryReqDTO = captor.getValue(); + assertEquals(order.getId(), inventoryReqDTO.getOrderId()); + assertEquals(order.getNo(), inventoryReqDTO.getOrderNo()); + assertEquals(WmsOrderTypeEnum.RECEIPT.getType(), inventoryReqDTO.getOrderType()); + assertEquals(1, inventoryReqDTO.getItems().size()); + assertEquals(skuId, inventoryReqDTO.getItems().get(0).getSkuId()); + assertEquals(warehouseId, inventoryReqDTO.getItems().get(0).getWarehouseId()); + assertEquals(0, new BigDecimal("2.00").compareTo(inventoryReqDTO.getItems().get(0).getQuantity())); + assertEquals(0, new BigDecimal("40.00").compareTo(inventoryReqDTO.getItems().get(0).getTotalPrice())); + } + + @Test + public void testCompleteReceiptOrder_detailRequired() { + // mock 数据 + WmsReceiptOrderDO order = createReceiptOrder(100L); + receiptOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> receiptOrderService.completeReceiptOrder(order.getId()), + RECEIPT_ORDER_DETAIL_REQUIRED); + verify(inventoryService, never()).changeInventory(any()); + } + + @Test + public void testCompleteReceiptOrder_duplicateComplete() { + // mock 数据 + WmsReceiptOrderDO order = createReceiptOrder(100L); + receiptOrderMapper.insert(order); + receiptOrderDetailMapper.insert(createReceiptOrderDetail(order.getId(), 200L, 100L)); + + // 调用 + receiptOrderService.completeReceiptOrder(order.getId()); + + // 调用,并断言:二次完成不能再次写库存 + assertServiceException(() -> receiptOrderService.completeReceiptOrder(order.getId()), + RECEIPT_ORDER_STATUS_NOT_PREPARE); + verify(inventoryService).changeInventory(any()); + } + + @Test + public void testCancelReceiptOrder_success() { + // mock 数据 + WmsReceiptOrderDO order = createReceiptOrder(100L); + receiptOrderMapper.insert(order); + + // 调用 + receiptOrderService.cancelReceiptOrder(order.getId()); + + // 断言 + WmsReceiptOrderDO dbOrder = receiptOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.CANCELED.getStatus(), dbOrder.getStatus()); + verify(inventoryService, never()).changeInventory(any()); + } + + @Test + public void testUpdateByIdAndStatus_statusNotMatch() { + // mock 数据 + WmsReceiptOrderDO order = createReceiptOrder(100L); + receiptOrderMapper.insert(order); + + // 调用 + int updateCount = receiptOrderMapper.updateByIdAndStatus(order.getId(), + WmsOrderStatusEnum.FINISHED.getStatus(), + new WmsReceiptOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())); + + // 断言 + assertEquals(0, updateCount); + assertEquals(WmsOrderStatusEnum.PREPARE.getStatus(), + receiptOrderMapper.selectById(order.getId()).getStatus()); + } + + @Test + public void testDeleteReceiptOrder_canceled() { + // mock 数据 + WmsReceiptOrderDO order = createReceiptOrder(100L).setStatus(WmsOrderStatusEnum.CANCELED.getStatus()); + receiptOrderMapper.insert(order); + receiptOrderDetailMapper.insert(createReceiptOrderDetail(order.getId(), 200L, 100L)); + + // 调用 + receiptOrderService.deleteReceiptOrder(order.getId()); + + // 断言 + assertNull(receiptOrderMapper.selectById(order.getId())); + assertEquals(0, receiptOrderDetailMapper.selectListByOrderId(order.getId()).size()); + } + + @Test + public void testDeleteReceiptOrder_finished() { + // mock 数据 + WmsReceiptOrderDO order = createReceiptOrder(100L).setStatus(WmsOrderStatusEnum.FINISHED.getStatus()); + receiptOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> receiptOrderService.deleteReceiptOrder(order.getId()), + RECEIPT_ORDER_STATUS_NOT_DELETABLE); + } + + private static WmsReceiptOrderDO createReceiptOrder(Long warehouseId) { + return new WmsReceiptOrderDO() + .setNo("RK202605120001") + .setType(WmsReceiptOrderTypeEnum.PURCHASE.getType()) + .setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()) + .setWarehouseId(warehouseId) + .setTotalQuantity(new BigDecimal("2.00")) + .setTotalPrice(new BigDecimal("20.00")); + } + + private static WmsReceiptOrderSaveReqVO createReceiptOrderSaveReqVO(Long id, + WmsReceiptOrderDetailSaveReqVO... details) { + WmsReceiptOrderSaveReqVO reqVO = new WmsReceiptOrderSaveReqVO(); + reqVO.setId(id); + reqVO.setNo("RK202605120001"); + reqVO.setType(WmsReceiptOrderTypeEnum.PURCHASE.getType()); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setWarehouseId(100L); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsReceiptOrderDetailSaveReqVO createReceiptOrderDetailReqVO(Long id, Long skuId, + String quantity, String price) { + return createReceiptOrderDetailReqVO(id, skuId, quantity, price, null); + } + + private static WmsReceiptOrderDetailSaveReqVO createReceiptOrderDetailReqVO(Long id, Long skuId, + String quantity, String price, + String totalPrice) { + WmsReceiptOrderDetailSaveReqVO reqVO = new WmsReceiptOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setPrice(new BigDecimal(price)); + if (totalPrice != null) { + reqVO.setTotalPrice(new BigDecimal(totalPrice)); + } + return reqVO; + } + + private static WmsReceiptOrderDetailDO createReceiptOrderDetail(Long orderId, Long skuId, Long warehouseId) { + return WmsReceiptOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .warehouseId(warehouseId) + .quantity(new BigDecimal("2.00")) + .price(new BigDecimal("20.00")) + .totalPrice(new BigDecimal("40.00")) + .build(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImplTest.java new file mode 100644 index 000000000..2ea341c0b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderDetailServiceImplTest.java @@ -0,0 +1,150 @@ +package cn.iocoder.yudao.module.wms.service.order.shipment; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.shipment.WmsShipmentOrderDetailMapper; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.SHIPMENT_ORDER_DETAIL_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Import(WmsShipmentOrderDetailServiceImpl.class) +public class WmsShipmentOrderDetailServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsShipmentOrderDetailServiceImpl shipmentOrderDetailService; + + @Resource + private WmsShipmentOrderDetailMapper shipmentOrderDetailMapper; + + @MockitoBean + private WmsItemSkuService itemSkuService; + + @Test + public void testCreateShipmentOrderDetailList_success() { + // mock 数据 + Long orderId = 10L; + WmsShipmentOrderSaveReqVO reqVO = createShipmentOrderReqVO( + createShipmentOrderDetailReqVO(null, 1001L, "1.00"), + createShipmentOrderDetailReqVO(null, 1002L, "2.00")); + + // 调用 + shipmentOrderDetailService.createShipmentOrderDetailList(orderId, reqVO); + + // 断言 + List details = shipmentOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(orderId, details.get(0).getOrderId()); + assertEquals(orderId, details.get(1).getOrderId()); + assertEquals(100L, details.get(0).getWarehouseId()); + assertEquals(0, new BigDecimal("100.00").compareTo(details.get(0).getTotalPrice())); + assertEquals(0, new BigDecimal("200.00").compareTo(details.get(1).getTotalPrice())); + } + + @Test + public void testCreateShipmentOrderDetailList_ignoreId() { + // mock 数据 + WmsShipmentOrderSaveReqVO reqVO = createShipmentOrderReqVO( + createShipmentOrderDetailReqVO(999L, 1001L, "1.00")); + + // 调用 + shipmentOrderDetailService.createShipmentOrderDetailList(10L, reqVO); + + // 断言 + List details = shipmentOrderDetailMapper.selectListByOrderId(10L); + assertEquals(1, details.size()); + assertNotNull(details.get(0).getId()); + assertEquals(1001L, details.get(0).getSkuId()); + } + + @Test + public void testUpdateShipmentOrderDetailList_diff() { + // mock 数据 + Long orderId = 10L; + WmsShipmentOrderDetailDO detail01 = createShipmentOrderDetail(orderId, 1001L, "1.00"); + WmsShipmentOrderDetailDO detail02 = createShipmentOrderDetail(orderId, 1002L, "2.00"); + shipmentOrderDetailMapper.insert(detail01); + shipmentOrderDetailMapper.insert(detail02); + WmsShipmentOrderSaveReqVO reqVO = createShipmentOrderReqVO( + createShipmentOrderDetailReqVO(detail01.getId(), 2001L, "11.00"), + createShipmentOrderDetailReqVO(null, 2002L, "22.00")); + + // 调用 + shipmentOrderDetailService.updateShipmentOrderDetailList(orderId, reqVO); + + // 断言:修改 + WmsShipmentOrderDetailDO dbUpdateDetail = shipmentOrderDetailMapper.selectById(detail01.getId()); + assertNotNull(dbUpdateDetail); + assertEquals(orderId, dbUpdateDetail.getOrderId()); + assertEquals(2001L, dbUpdateDetail.getSkuId()); + assertEquals(0, new BigDecimal("11.00").compareTo(dbUpdateDetail.getQuantity())); + assertEquals(0, new BigDecimal("1100.00").compareTo(dbUpdateDetail.getTotalPrice())); + // 断言:新增 + List details = shipmentOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + WmsShipmentOrderDetailDO dbCreateDetail = details.stream() + .filter(detail -> Long.valueOf(2002L).equals(detail.getSkuId())) + .findFirst().orElse(null); + assertNotNull(dbCreateDetail); + assertEquals(orderId, dbCreateDetail.getOrderId()); + assertEquals(0, new BigDecimal("22.00").compareTo(dbCreateDetail.getQuantity())); + assertEquals(0, new BigDecimal("2200.00").compareTo(dbCreateDetail.getTotalPrice())); + // 断言:删除 + assertNull(shipmentOrderDetailMapper.selectById(detail02.getId())); + } + + @Test + public void testUpdateShipmentOrderDetailList_detailNotExists() { + // mock 数据 + WmsShipmentOrderSaveReqVO reqVO = createShipmentOrderReqVO( + createShipmentOrderDetailReqVO(999L, 1001L, "1.00")); + + // 调用,并断言 + assertServiceException(() -> shipmentOrderDetailService.updateShipmentOrderDetailList(10L, reqVO), + SHIPMENT_ORDER_DETAIL_NOT_EXISTS); + } + + private static WmsShipmentOrderSaveReqVO createShipmentOrderReqVO(WmsShipmentOrderDetailSaveReqVO... details) { + WmsShipmentOrderSaveReqVO reqVO = new WmsShipmentOrderSaveReqVO(); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setWarehouseId(100L); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsShipmentOrderDetailSaveReqVO createShipmentOrderDetailReqVO(Long id, Long skuId, String quantity) { + WmsShipmentOrderDetailSaveReqVO reqVO = new WmsShipmentOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setPrice(new BigDecimal("100.00")); + reqVO.setTotalPrice(new BigDecimal(quantity).multiply(new BigDecimal("100.00"))); + return reqVO; + } + + private static WmsShipmentOrderDetailDO createShipmentOrderDetail(Long orderId, Long skuId, String quantity) { + return WmsShipmentOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .warehouseId(100L) + .quantity(new BigDecimal(quantity)) + .price(new BigDecimal("100.00")) + .totalPrice(new BigDecimal(quantity).multiply(new BigDecimal("100.00"))) + .build(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImplTest.java b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImplTest.java new file mode 100644 index 000000000..6a4e86cb7 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/java/cn/iocoder/yudao/module/wms/service/order/shipment/WmsShipmentOrderServiceImplTest.java @@ -0,0 +1,271 @@ +package cn.iocoder.yudao.module.wms.service.order.shipment; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailSaveReqVO; +import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order.WmsShipmentOrderSaveReqVO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDO; +import cn.iocoder.yudao.module.wms.dal.dataobject.order.shipment.WmsShipmentOrderDetailDO; +import cn.iocoder.yudao.module.wms.dal.mysql.order.shipment.WmsShipmentOrderDetailMapper; +import cn.iocoder.yudao.module.wms.dal.mysql.order.shipment.WmsShipmentOrderMapper; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsOrderStatusEnum; +import cn.iocoder.yudao.module.wms.enums.order.WmsShipmentOrderTypeEnum; +import cn.iocoder.yudao.module.wms.service.inventory.WmsInventoryService; +import cn.iocoder.yudao.module.wms.service.inventory.dto.WmsInventoryChangeReqDTO; +import cn.iocoder.yudao.module.wms.service.md.item.WmsItemSkuService; +import cn.iocoder.yudao.module.wms.service.md.merchant.WmsMerchantService; +import cn.iocoder.yudao.module.wms.service.md.warehouse.WmsWarehouseService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.SHIPMENT_ORDER_DETAIL_REQUIRED; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.SHIPMENT_ORDER_STATUS_NOT_DELETABLE; +import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.SHIPMENT_ORDER_STATUS_NOT_PREPARE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@Import({WmsShipmentOrderServiceImpl.class, WmsShipmentOrderDetailServiceImpl.class}) +public class WmsShipmentOrderServiceImplTest extends BaseDbUnitTest { + + @Resource + private WmsShipmentOrderServiceImpl shipmentOrderService; + + @Resource + private WmsShipmentOrderMapper shipmentOrderMapper; + @Resource + private WmsShipmentOrderDetailMapper shipmentOrderDetailMapper; + + @MockitoBean + private WmsWarehouseService warehouseService; + @MockitoBean + private WmsMerchantService merchantService; + @MockitoBean + private WmsItemSkuService itemSkuService; + @MockitoBean + private WmsInventoryService inventoryService; + + @Test + public void testCreateShipmentOrder_calculateTotal() { + // mock 数据 + WmsShipmentOrderSaveReqVO reqVO = createShipmentOrderSaveReqVO(null, + createShipmentOrderDetailReqVO(null, 2001L, "1.50", "10.00", "16.00"), + createShipmentOrderDetailReqVO(null, 2002L, "2.50", "20.00", "51.00")); + + // 调用 + Long orderId = shipmentOrderService.createShipmentOrder(reqVO); + + // 断言 + WmsShipmentOrderDO dbOrder = shipmentOrderMapper.selectById(orderId); + assertNotNull(dbOrder); + assertEquals(0, new BigDecimal("4.00").compareTo(dbOrder.getTotalQuantity())); + assertEquals(0, new BigDecimal("67.00").compareTo(dbOrder.getTotalPrice())); + List details = shipmentOrderDetailMapper.selectListByOrderId(orderId); + assertEquals(2, details.size()); + assertEquals(0, new BigDecimal("16.00").compareTo(details.get(0).getTotalPrice())); + } + + @Test + public void testUpdateShipmentOrder_calculateTotal() { + // mock 数据 + WmsShipmentOrderDO order = createShipmentOrder(100L); + shipmentOrderMapper.insert(order); + WmsShipmentOrderSaveReqVO reqVO = createShipmentOrderSaveReqVO(order.getId(), + createShipmentOrderDetailReqVO(null, 2001L, "3.00", "30.00", "88.00")); + + // 调用 + shipmentOrderService.updateShipmentOrder(reqVO); + + // 断言 + WmsShipmentOrderDO dbOrder = shipmentOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(0, new BigDecimal("3.00").compareTo(dbOrder.getTotalQuantity())); + assertEquals(0, new BigDecimal("88.00").compareTo(dbOrder.getTotalPrice())); + List details = shipmentOrderDetailMapper.selectListByOrderId(order.getId()); + assertEquals(1, details.size()); + assertEquals(0, new BigDecimal("88.00").compareTo(details.get(0).getTotalPrice())); + } + + @Test + public void testCompleteShipmentOrder_success() { + // mock 数据 + Long warehouseId = 100L; + Long skuId = 200L; + WmsShipmentOrderDO order = createShipmentOrder(warehouseId); + shipmentOrderMapper.insert(order); + shipmentOrderDetailMapper.insert(createShipmentOrderDetail(order.getId(), skuId, warehouseId)); + + // 调用 + shipmentOrderService.completeShipmentOrder(order.getId()); + + // 断言:出库单 + WmsShipmentOrderDO dbOrder = shipmentOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.FINISHED.getStatus(), dbOrder.getStatus()); + // 断言:库存变更 + ArgumentCaptor captor = ArgumentCaptor.forClass(WmsInventoryChangeReqDTO.class); + verify(inventoryService).changeInventory(captor.capture()); + WmsInventoryChangeReqDTO inventoryReqDTO = captor.getValue(); + assertEquals(order.getId(), inventoryReqDTO.getOrderId()); + assertEquals(order.getNo(), inventoryReqDTO.getOrderNo()); + assertEquals(WmsOrderTypeEnum.SHIPMENT.getType(), inventoryReqDTO.getOrderType()); + assertEquals(1, inventoryReqDTO.getItems().size()); + assertEquals(skuId, inventoryReqDTO.getItems().get(0).getSkuId()); + assertEquals(warehouseId, inventoryReqDTO.getItems().get(0).getWarehouseId()); + assertEquals(0, new BigDecimal("-2.00").compareTo(inventoryReqDTO.getItems().get(0).getQuantity())); + assertEquals(0, new BigDecimal("-40.00").compareTo(inventoryReqDTO.getItems().get(0).getTotalPrice())); + } + + @Test + public void testCompleteShipmentOrder_detailRequired() { + // mock 数据 + WmsShipmentOrderDO order = createShipmentOrder(100L); + shipmentOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> shipmentOrderService.completeShipmentOrder(order.getId()), + SHIPMENT_ORDER_DETAIL_REQUIRED); + verify(inventoryService, never()).changeInventory(any()); + } + + @Test + public void testCompleteShipmentOrder_duplicateComplete() { + // mock 数据 + WmsShipmentOrderDO order = createShipmentOrder(100L); + shipmentOrderMapper.insert(order); + shipmentOrderDetailMapper.insert(createShipmentOrderDetail(order.getId(), 200L, 100L)); + + // 调用 + shipmentOrderService.completeShipmentOrder(order.getId()); + + // 调用,并断言:二次完成不能再次写库存 + assertServiceException(() -> shipmentOrderService.completeShipmentOrder(order.getId()), + SHIPMENT_ORDER_STATUS_NOT_PREPARE); + verify(inventoryService).changeInventory(any()); + } + + @Test + public void testCancelShipmentOrder_success() { + // mock 数据 + WmsShipmentOrderDO order = createShipmentOrder(100L); + shipmentOrderMapper.insert(order); + + // 调用 + shipmentOrderService.cancelShipmentOrder(order.getId()); + + // 断言 + WmsShipmentOrderDO dbOrder = shipmentOrderMapper.selectById(order.getId()); + assertNotNull(dbOrder); + assertEquals(WmsOrderStatusEnum.CANCELED.getStatus(), dbOrder.getStatus()); + verify(inventoryService, never()).changeInventory(any()); + } + + @Test + public void testUpdateByIdAndStatus_statusNotMatch() { + // mock 数据 + WmsShipmentOrderDO order = createShipmentOrder(100L); + shipmentOrderMapper.insert(order); + + // 调用 + int updateCount = shipmentOrderMapper.updateByIdAndStatus(order.getId(), + WmsOrderStatusEnum.FINISHED.getStatus(), + new WmsShipmentOrderDO().setStatus(WmsOrderStatusEnum.CANCELED.getStatus())); + + // 断言 + assertEquals(0, updateCount); + assertEquals(WmsOrderStatusEnum.PREPARE.getStatus(), + shipmentOrderMapper.selectById(order.getId()).getStatus()); + } + + @Test + public void testDeleteShipmentOrder_canceled() { + // mock 数据 + WmsShipmentOrderDO order = createShipmentOrder(100L).setStatus(WmsOrderStatusEnum.CANCELED.getStatus()); + shipmentOrderMapper.insert(order); + shipmentOrderDetailMapper.insert(createShipmentOrderDetail(order.getId(), 200L, 100L)); + + // 调用 + shipmentOrderService.deleteShipmentOrder(order.getId()); + + // 断言 + assertNull(shipmentOrderMapper.selectById(order.getId())); + assertEquals(0, shipmentOrderDetailMapper.selectListByOrderId(order.getId()).size()); + } + + @Test + public void testDeleteShipmentOrder_finished() { + // mock 数据 + WmsShipmentOrderDO order = createShipmentOrder(100L).setStatus(WmsOrderStatusEnum.FINISHED.getStatus()); + shipmentOrderMapper.insert(order); + + // 调用,并断言 + assertServiceException(() -> shipmentOrderService.deleteShipmentOrder(order.getId()), + SHIPMENT_ORDER_STATUS_NOT_DELETABLE); + } + + private static WmsShipmentOrderDO createShipmentOrder(Long warehouseId) { + return new WmsShipmentOrderDO() + .setNo("CK202605120001") + .setType(WmsShipmentOrderTypeEnum.SALE.getType()) + .setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)) + .setStatus(WmsOrderStatusEnum.PREPARE.getStatus()) + .setWarehouseId(warehouseId) + .setTotalQuantity(new BigDecimal("2.00")) + .setTotalPrice(new BigDecimal("20.00")); + } + + private static WmsShipmentOrderSaveReqVO createShipmentOrderSaveReqVO(Long id, + WmsShipmentOrderDetailSaveReqVO... details) { + WmsShipmentOrderSaveReqVO reqVO = new WmsShipmentOrderSaveReqVO(); + reqVO.setId(id); + reqVO.setNo("CK202605120001"); + reqVO.setType(WmsShipmentOrderTypeEnum.SALE.getType()); + reqVO.setOrderTime(LocalDateTime.of(2026, 5, 12, 0, 0)); + reqVO.setWarehouseId(100L); + reqVO.setDetails(Arrays.asList(details)); + return reqVO; + } + + private static WmsShipmentOrderDetailSaveReqVO createShipmentOrderDetailReqVO(Long id, Long skuId, + String quantity, String price) { + return createShipmentOrderDetailReqVO(id, skuId, quantity, price, null); + } + + private static WmsShipmentOrderDetailSaveReqVO createShipmentOrderDetailReqVO(Long id, Long skuId, + String quantity, String price, + String totalPrice) { + WmsShipmentOrderDetailSaveReqVO reqVO = new WmsShipmentOrderDetailSaveReqVO(); + reqVO.setId(id); + reqVO.setSkuId(skuId); + reqVO.setQuantity(new BigDecimal(quantity)); + reqVO.setPrice(new BigDecimal(price)); + if (totalPrice != null) { + reqVO.setTotalPrice(new BigDecimal(totalPrice)); + } + return reqVO; + } + + private static WmsShipmentOrderDetailDO createShipmentOrderDetail(Long orderId, Long skuId, Long warehouseId) { + return WmsShipmentOrderDetailDO.builder() + .orderId(orderId) + .skuId(skuId) + .warehouseId(warehouseId) + .quantity(new BigDecimal("2.00")) + .price(new BigDecimal("20.00")) + .totalPrice(new BigDecimal("40.00")) + .build(); + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/resources/application-unit-test.yaml b/yudao-module-wms/yudao-module-wms-server/src/test/resources/application-unit-test.yaml new file mode 100644 index 000000000..29615501a --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/resources/application-unit-test.yaml @@ -0,0 +1,35 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + data: + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 芋道相关配置 #################### + +yudao: + info: + base-package: cn.iocoder.yudao.module diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/clean.sql b/yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/clean.sql new file mode 100644 index 000000000..2ca91cb4b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/clean.sql @@ -0,0 +1,16 @@ +DELETE FROM "wms_check_order_detail"; +DELETE FROM "wms_check_order"; +DELETE FROM "wms_movement_order_detail"; +DELETE FROM "wms_movement_order"; +DELETE FROM "wms_shipment_order_detail"; +DELETE FROM "wms_shipment_order"; +DELETE FROM "wms_receipt_order_detail"; +DELETE FROM "wms_receipt_order"; +DELETE FROM "wms_inventory_history"; +DELETE FROM "wms_inventory"; +DELETE FROM "wms_item_sku"; +DELETE FROM "wms_item"; +DELETE FROM "wms_item_category"; +DELETE FROM "wms_item_brand"; +DELETE FROM "wms_warehouse"; +DELETE FROM "wms_merchant"; diff --git a/yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/create_tables.sql b/yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/create_tables.sql new file mode 100644 index 000000000..811ae5e4b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/test/resources/sql/create_tables.sql @@ -0,0 +1,320 @@ +CREATE TABLE IF NOT EXISTS "wms_warehouse" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(20) NOT NULL, + "name" varchar(50) NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "sort" int DEFAULT '0', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 仓库表'; + +CREATE TABLE IF NOT EXISTS "wms_merchant" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(20) NOT NULL, + "name" varchar(60) NOT NULL, + "type" tinyint NOT NULL, + "level" varchar(10) DEFAULT NULL, + "bank_name" varchar(255) DEFAULT NULL, + "bank_account" varchar(40) DEFAULT NULL, + "address" varchar(200) DEFAULT NULL, + "mobile" varchar(13) DEFAULT NULL, + "telephone" varchar(13) DEFAULT NULL, + "contact" varchar(30) DEFAULT NULL, + "email" varchar(50) DEFAULT NULL, + "remark" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 往来企业表'; + +CREATE TABLE IF NOT EXISTS "wms_item_brand" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(20) NOT NULL, + "name" varchar(30) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 商品品牌表'; + +CREATE TABLE IF NOT EXISTS "wms_item_category" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "parent_id" bigint NOT NULL DEFAULT '0', + "code" varchar(20) NOT NULL, + "name" varchar(30) NOT NULL, + "sort" int DEFAULT '0', + "status" tinyint NOT NULL DEFAULT '1', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 商品分类表'; + +CREATE TABLE IF NOT EXISTS "wms_item" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(20) DEFAULT NULL, + "name" varchar(60) NOT NULL, + "category_id" bigint NOT NULL, + "unit" varchar(20) DEFAULT NULL, + "brand_id" bigint DEFAULT NULL, + "remark" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 商品表'; + +CREATE TABLE IF NOT EXISTS "wms_item_sku" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL, + "item_id" bigint NOT NULL, + "bar_code" varchar(64) DEFAULT NULL, + "code" varchar(64) DEFAULT NULL, + "length" decimal(10, 1) DEFAULT NULL, + "width" decimal(10, 1) DEFAULT NULL, + "height" decimal(10, 1) DEFAULT NULL, + "gross_weight" decimal(10, 3) DEFAULT NULL, + "net_weight" decimal(10, 3) DEFAULT NULL, + "cost_price" decimal(16, 2) DEFAULT NULL, + "selling_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 商品 SKU 表'; + +CREATE TABLE IF NOT EXISTS "wms_inventory" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "sku_id" bigint NOT NULL, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "remark" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id"), + CONSTRAINT "uk_sku_id_warehouse_id" UNIQUE ("sku_id", "warehouse_id") +) COMMENT 'WMS 库存表'; + +CREATE TABLE IF NOT EXISTS "wms_inventory_history" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "sku_id" bigint NOT NULL, + "quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "before_quantity" decimal(20, 2) DEFAULT NULL, + "after_quantity" decimal(20, 2) DEFAULT NULL, + "batch_no" varchar(64) DEFAULT NULL, + "production_date" timestamp DEFAULT NULL, + "expiration_date" timestamp DEFAULT NULL, + "price" decimal(16, 2) DEFAULT NULL, + "total_price" decimal(16, 2) DEFAULT NULL, + "remark" varchar(255) DEFAULT NULL, + "order_id" bigint DEFAULT NULL, + "order_no" varchar(64) DEFAULT NULL, + "order_type" int DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 库存流水表'; + +CREATE TABLE IF NOT EXISTS "wms_receipt_order" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar(64) NOT NULL, + "type" int NOT NULL, + "order_time" timestamp NOT NULL, + "status" int NOT NULL DEFAULT '0', + "biz_order_no" varchar(64) DEFAULT NULL, + "merchant_id" bigint DEFAULT NULL, + "remark" varchar(255) DEFAULT NULL, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "total_quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "total_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 入库单表'; + +CREATE TABLE IF NOT EXISTS "wms_receipt_order_detail" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "order_id" bigint NOT NULL, + "sku_id" bigint NOT NULL, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "batch_no" varchar(64) DEFAULT NULL, + "production_date" timestamp DEFAULT NULL, + "expiration_date" timestamp DEFAULT NULL, + "quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "price" decimal(16, 2) DEFAULT NULL, + "total_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 入库单明细表'; + +CREATE TABLE IF NOT EXISTS "wms_shipment_order" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar(64) NOT NULL, + "type" int NOT NULL, + "order_time" timestamp NOT NULL, + "status" int NOT NULL DEFAULT '0', + "biz_order_no" varchar(64) DEFAULT NULL, + "merchant_id" bigint DEFAULT NULL, + "remark" varchar(255) DEFAULT NULL, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "total_quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "total_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 出库单表'; + +CREATE TABLE IF NOT EXISTS "wms_shipment_order_detail" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "order_id" bigint NOT NULL, + "sku_id" bigint NOT NULL, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "inventory_detail_id" bigint DEFAULT NULL, + "batch_no" varchar(64) DEFAULT NULL, + "production_date" timestamp DEFAULT NULL, + "expiration_date" timestamp DEFAULT NULL, + "quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "price" decimal(16, 2) DEFAULT NULL, + "total_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 出库单明细表'; + +CREATE TABLE IF NOT EXISTS "wms_movement_order" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar(64) NOT NULL, + "order_time" timestamp NOT NULL, + "status" int NOT NULL DEFAULT '0', + "remark" varchar(255) DEFAULT NULL, + "source_warehouse_id" bigint NOT NULL, + "target_warehouse_id" bigint NOT NULL, + "total_quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "total_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 移库单表'; + +CREATE TABLE IF NOT EXISTS "wms_movement_order_detail" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "order_id" bigint NOT NULL, + "sku_id" bigint NOT NULL, + "source_warehouse_id" bigint NOT NULL, + "target_warehouse_id" bigint NOT NULL, + "inventory_detail_id" bigint DEFAULT NULL, + "batch_no" varchar(64) DEFAULT NULL, + "production_date" timestamp DEFAULT NULL, + "expiration_date" timestamp DEFAULT NULL, + "quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "price" decimal(16, 2) DEFAULT NULL, + "total_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 移库单明细表'; + +CREATE TABLE IF NOT EXISTS "wms_check_order" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar(64) NOT NULL, + "order_time" timestamp NOT NULL, + "status" int NOT NULL DEFAULT '0', + "remark" varchar(255) DEFAULT NULL, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "total_quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "total_price" decimal(16, 2) DEFAULT NULL, + "actual_price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 盘库单表'; + +CREATE TABLE IF NOT EXISTS "wms_check_order_detail" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "order_id" bigint NOT NULL, + "sku_id" bigint NOT NULL, + "warehouse_id" bigint NOT NULL, + "area_id" bigint NOT NULL DEFAULT '0', + "inventory_id" bigint DEFAULT NULL, + "inventory_detail_id" bigint DEFAULT NULL, + "batch_no" varchar(64) DEFAULT NULL, + "production_date" timestamp DEFAULT NULL, + "expiration_date" timestamp DEFAULT NULL, + "receipt_time" timestamp DEFAULT NULL, + "quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "check_quantity" decimal(20, 2) NOT NULL DEFAULT '0', + "price" decimal(16, 2) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT '0', + PRIMARY KEY ("id") +) COMMENT 'WMS 盘库单明细表'; From 9d1dd25bc70f83b9db0c0b7a6d5faa8c00d1970f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 06:39:43 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=88wms?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .image/common/wms-feature.png | Bin 0 -> 221167 bytes .image/common/wms-preview.png | Bin 0 -> 55711 bytes README.md | 15 ++++++++++++--- .../yudao-module-wms-server/pom.xml | 5 +++++ .../home/WmsHomeStatisticsServiceImpl.java | 4 ++-- .../inventory/WmsInventoryServiceImpl.java | 3 ++- 6 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .image/common/wms-feature.png create mode 100644 .image/common/wms-preview.png diff --git a/.image/common/wms-feature.png b/.image/common/wms-feature.png new file mode 100644 index 0000000000000000000000000000000000000000..0d7790b18567142f2070b98c071d6503d6e66cc9 GIT binary patch literal 221167 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 z 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͵YUb9p!wRHW~23S6nwk?4ieij?o&7 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 literal 0 HcmV?d00001 diff --git a/.image/common/wms-preview.png b/.image/common/wms-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..31c5187d926dc12db6e0d10cff924117bbbcd98c GIT binary patch literal 55711 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 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 > @@ -287,6 +287,14 @@ ![功能图](/.image/common/mes-preview.png) +### WMS 系统 + +演示地址: + +![功能图](/.image/common/wms-feature.png) + +![功能图](/.image/common/wms-preview.png) + ### AI 大模型 演示地址: @@ -321,6 +329,7 @@ | `yudao-module-erp` | ERP 系统的 Module 模块 | | `yudao-module-crm` | CRM 系统的 Module 模块 | | `yudao-module-mes` | MES 系统的 Module 模块 | +| `yudao-module-wms` | WMS 系统的 Module 模块 | | `yudao-module-ai` | AI 大模型的 Module 模块 | | `yudao-module-iot` | IoT 物联网的 Module 模块 | | `yudao-module-mp` | 微信公众号的 Module 模块 | diff --git a/yudao-module-wms/yudao-module-wms-server/pom.xml b/yudao-module-wms/yudao-module-wms-server/pom.xml index 0461e0340..07590d70e 100644 --- a/yudao-module-wms/yudao-module-wms-server/pom.xml +++ b/yudao-module-wms/yudao-module-wms-server/pom.xml @@ -88,6 +88,11 @@ yudao-spring-boot-starter-monitor + + org.projectlombok + lombok + + cn.iocoder.cloud diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java index 8aa2926a2..ba5f4410f 100644 --- a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java @@ -22,11 +22,11 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.IntStream; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue; import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.getBigDecimal; -import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.getDateList; /** * WMS 首页统计 Service 实现类 @@ -74,7 +74,7 @@ public class WmsHomeStatisticsServiceImpl implements WmsHomeStatisticsService { dateMap.computeIfAbsent(date, key -> new HashMap<>()).put(orderType, count); } // 构造结果,保证每天都有数据 - return convertList(getDateList(startDate, days), d -> { + return convertList(IntStream.range(0, days).mapToObj(startDate::plusDays).toList(), d -> { String dateStr = DatePattern.NORM_DATE_FORMATTER.format(d); Map row = dateMap.getOrDefault(dateStr, Collections.emptyMap()); return new WmsHomeOrderTrendRespVO().setTime(d.atStartOfDay()) diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java index 3ff81ad6d..250870eee 100644 --- a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java @@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; @@ -291,7 +292,7 @@ public class WmsInventoryServiceImpl implements WmsInventoryService { WmsItemSkuDO skuDO = itemSkuService.validateItemSkuExists(item.getSkuId()); WmsItemDO itemDO = itemService.validateItemExists(skuDO.getItemId()); return exception(INVENTORY_QUANTITY_NOT_ENOUGH, itemDO.getName(), skuDO.getName(), - item.getWarehouseId(), beforeQuantity, item.getQuantity()); + item.getWarehouseId(), beforeQuantity.setScale(6, RoundingMode.HALF_UP), item.getQuantity()); } } From 5cf473d48e2bc356e3ef3050a889ffa3e912d52a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 14:37:14 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=88wms?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .image/common/ruoyi-vue-pro-biz.png | Bin 65743 -> 42017 bytes README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.image/common/ruoyi-vue-pro-biz.png b/.image/common/ruoyi-vue-pro-biz.png index 42dc849b509a6e103d128eb2be4a282b99225dc3..355491963f6ac7958a528475fb0d7fe61eaef9fd 100644 GIT binary patch literal 42017 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 z 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 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 > From d5ab0b06a70f89165c13e37f03f36dfde8b9fbdd Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 14:38:29 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=88wms?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yaml | 4 ++-- .../src/main/resources/application-local.yaml | 4 ++-- .../src/main/resources/application.yaml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml index c819a254d..522c0145e 100644 --- a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-dev.yaml @@ -7,12 +7,12 @@ spring: username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 - nawmspace: dev # 命名空间。这里使用 dev 开发环境 + namespace: dev # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 config: # 【注册中心】配置项 - nawmspace: dev # 命名空间。这里使用 dev 开发环境 + namespace: dev # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP --- #################### 数据库相关配置 #################### diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml index fdd75ad7b..c7fd54dfc 100644 --- a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application-local.yaml @@ -7,12 +7,12 @@ spring: username: # Nacos 账号 password: # Nacos 密码 discovery: # 【配置中心】配置项 - nawmspace: dev # 命名空间。这里使用 dev 开发环境 + namespace: dev # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP metadata: version: 1.0.0 # 服务实例的版本号,可用于灰度发布 config: # 【注册中心】配置项 - nawmspace: dev # 命名空间。这里使用 dev 开发环境 + namespace: dev # 命名空间。这里使用 dev 开发环境 group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP --- #################### 数据库相关配置 #################### diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml index 7dfc4055d..82dfb4e4a 100644 --- a/yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml +++ b/yudao-module-wms/yudao-module-wms-server/src/main/resources/application.yaml @@ -24,9 +24,9 @@ spring: # Jackson 配置项 jackson: serialization: - write-dates-as-tiwmstamps: true # 设置 LocalDateTime 的格式,使用时间戳 - write-date-tiwmstamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 - write-durations-as-tiwmstamps: true # 设置 Duration 的格式,使用时间戳 + write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 fail-on-empty-beans: false # 允许序列化无属性的 Bean # Cache 配置项 From 1702fc1acb70ce8e1646fc7e30feab953b93e1de Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 14:55:40 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat(wms)=EF=BC=9A=E8=B0=83=E6=95=B4=20READ?= =?UTF-8?q?ME.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7889607e1..693fa9c2b 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ | 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 | | 【精简版】[yudao-cloud-mini](https://gitee.com/yudaocode/yudao-cloud-mini) | [`master`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk17/) 分支 | -* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、WMS、AI 大模型、IoT 物联网 等功能 -* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、WMS、AI 大模型、IoT 物联网 等功能 +* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、AI 大模型、IoT 物联网 等功能 +* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、AI 大模型、IoT 物联网 等功能 可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】 @@ -115,7 +115,7 @@ * 通用模块(必选):系统功能、基础设施 * 通用模块(可选):工作流程、支付系统、数据报表、会员中心 -* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏、WMS 仓库管理系统 +* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏 > 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 > From 8637b2a28f9833b6094637da164b2c122919c512 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 15:08:55 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat(wms)=EF=BC=9A=E8=B0=83=E6=95=B4=20READ?= =?UTF-8?q?ME.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 693fa9c2b..f28367052 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,14 @@ ![功能图](/.image/common/erp-feature.png) +### WMS 系统 + +演示地址: + +![功能图](/.image/common/wms-feature.png) + +![功能图](/.image/common/wms-preview.png) + ### CRM 系统 演示地址: @@ -287,14 +295,6 @@ ![功能图](/.image/common/mes-preview.png) -### WMS 系统 - -演示地址: - -![功能图](/.image/common/wms-feature.png) - -![功能图](/.image/common/wms-preview.png) - ### AI 大模型 演示地址: From 5088b8c2e24ec1112a5c583e8fe6cedbb75e74b3 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 17:33:15 +0800 Subject: [PATCH 7/8] fix(config): add import-enable option for Excel import interface --- yudao-server/src/main/resources/application.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 92766be20..f02cbf706 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -331,6 +331,7 @@ yudao: vo-type: 10 # VO 的类型,参见 CodegenVOTypeEnum 枚举类 delete-batch-enable: true # 是否生成批量删除接口 unit-test-enable: false # 是否生成单元测试 + import-enable: false # 是否生成 Excel 导入接口 tenant: # 多租户相关配置项 enable: true ignore-urls: From 050edb2db74ba9ba4b31688b0b1d874e2eff84d0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 May 2026 17:51:08 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=20wms=20?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=A4=B1=E8=B4=A5=20feat=EF=BC=9A=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=96=87=E6=A1=A3=E8=AF=B4=E6=98=8E=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../banner/core/BannerApplicationRunner.java | 6 +++ .../core/handler/GlobalExceptionHandler.java | 26 +++++++++++-- .../gateway/util/BannerApplicationRunner.java | 6 +++ .../config/SecurityConfiguration.java | 39 +++++++++++++++++++ .../framework/security/core/package-info.java | 4 ++ yudao-server/pom.xml | 11 +++++- .../server/controller/DefaultController.java | 18 +++++++++ 7 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/config/SecurityConfiguration.java create mode 100644 yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/core/package-info.java diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java index 6c01b9497..517fd603e 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java @@ -37,8 +37,14 @@ public class BannerApplicationRunner implements ApplicationRunner { System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); // ERP 系统 System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); + // WMS 仓库管理系统 + System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]"); // CRM 系统 System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); + // MES 系统 + System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]"); + // IM 即时通讯 + System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]"); // 微信公众号 System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); // 支付平台 diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index b99925e65..91dc38967 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -423,25 +423,43 @@ public class GlobalExceptionHandler { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); } - // 6. CRM 系统 + // 6. WMS 仓库管理系统 + if (message.contains("wms_")) { + log.error("[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]"); + } + // 7. CRM 系统 if (message.contains("crm_")) { log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); } - // 7. 支付平台 + // 8. MES 系统 + if (message.contains("mes_")) { + log.error("[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]"); + } + // 9. IM 即时通讯 + if (message.contains("im_")) { + log.error("[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]"); + } + // 10. 支付平台 if (message.contains("pay_")) { log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); } - // 8. AI 大模型 + // 11. AI 大模型 if (message.contains("ai_")) { log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); } - // 9. IoT 物联网 + // 12. IoT 物联网 if (message.contains("iot_")) { log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), diff --git a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/BannerApplicationRunner.java b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/BannerApplicationRunner.java index 3933ef262..a9ecd0bdc 100644 --- a/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/BannerApplicationRunner.java +++ b/yudao-gateway/src/main/java/cn/iocoder/yudao/gateway/util/BannerApplicationRunner.java @@ -39,8 +39,14 @@ public class BannerApplicationRunner implements ApplicationRunner { System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); // ERP 系统 System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); + // WMS 仓库管理系统 + System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]"); // CRM 系统 System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); + // MES 系统 + System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]"); + // IM 即时通讯 + System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]"); // 微信公众号 System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); // 支付平台 diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/config/SecurityConfiguration.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..1798c0b5b --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.wms.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.module.wms.enums.ApiConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * WMS 模块的 Security 配置 + */ +@Configuration("wmsSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("wmsAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // Swagger 接口文档 + registry.requestMatchers("/v3/api-docs/**").permitAll() + .requestMatchers("/webjars/**").permitAll() + .requestMatchers("/swagger-ui").permitAll() + .requestMatchers("/swagger-ui/**").permitAll(); + // Spring Boot Actuator 的安全配置 + registry.requestMatchers("/actuator").permitAll() + .requestMatchers("/actuator/**").permitAll(); + // Druid 监控 + registry.requestMatchers("/druid/**").permitAll(); + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/core/package-info.java b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/core/package-info.java new file mode 100644 index 000000000..0c573aa64 --- /dev/null +++ b/yudao-module-wms/yudao-module-wms-server/src/main/java/cn/iocoder/yudao/module/wms/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.wms.framework.security.core; diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index c06cab00d..f6834031e 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -117,8 +117,15 @@ - - + + + + + + + + + diff --git a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java index a9ed8fbf4..f8ba5ece2 100644 --- a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java +++ b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java @@ -46,12 +46,30 @@ public class DefaultController { "[ERP 模块 yudao-module-erp - 已禁用][参考 https://doc.iocoder.cn/erp/build/ 开启]"); } + @RequestMapping(value = { "/admin-api/wms/**"}) + public CommonResult wms404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[WMS 仓库管理系统 yudao-module-wms - 已禁用][参考 https://doc.iocoder.cn/wms/build/ 开启]"); + } + @RequestMapping("/admin-api/crm/**") public CommonResult crm404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[CRM 模块 yudao-module-crm - 已禁用][参考 https://doc.iocoder.cn/crm/build/ 开启]"); } + @RequestMapping(value = { "/admin-api/mes/**"}) + public CommonResult mes404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[MES 系统 yudao-module-mes - 已禁用][参考 https://doc.iocoder.cn/mes/build/ 开启]"); + } + + @RequestMapping(value = { "/admin-api/im/**"}) + public CommonResult im404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[IM 即时通讯 yudao-module-im - 已禁用][参考 https://doc.iocoder.cn/im/build/ 开启]"); + } + @RequestMapping(value = { "/admin-api/report/**"}) public CommonResult report404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(),