完成成品入库单。

This commit is contained in:
c
2026-03-10 18:07:12 +08:00
parent c15c5c9a09
commit 742d7750f9
10 changed files with 209 additions and 133 deletions

View File

@@ -23,7 +23,6 @@ public class CustomerTenantHandler implements TenantLineHandler {
customerTables.add("storage_list");
customerTables.add("bom_list");
customerTables.add("vendor");
customerTables.add("productionform");
}
@Override

View File

@@ -10,6 +10,7 @@ import com.niuan.erp.common.base.BaseResult;
import com.niuan.erp.common.base.CommonValidateGroup.*;
import com.niuan.erp.common.base.OperationType;
import com.niuan.erp.module.common.entity.Document;
import com.niuan.erp.module.production.controller.dto.DeviceOutstockRequest;
import com.niuan.erp.module.production.controller.dto.FinishedProductReceiptAddDto;
import com.niuan.erp.module.production.controller.dto.FinishedProductReceiptDto;
import com.niuan.erp.module.production.service.FinishedProductReceiptService;
@@ -65,7 +66,7 @@ public class FinishedProductReceiptController {
@ApiLog(type = OperationType.DELETE, remark = "删除一条FinishedProductReceipt记录")
@Operation(summary = "删除FinishedProductReceipt", operationId = "deleteFinishedProductReceipt")
@PostMapping("/deleteFinishedProductReceipt")
@PreAuthorize("hasAuthority('finished_product_receipt:delete')")
@PreAuthorize("hasAuthority('finished_product_receipt:remove')")
public BaseResult<?> deleteFinishedProductReceipt(@Validated(DeleteOne.class) @RequestBody BaseDeleteBody req) {
finishedProductReceiptService.deleteFinishedProductReceipt(req.id());
return BaseResult.success();
@@ -74,45 +75,37 @@ public class FinishedProductReceiptController {
@ApiLog(type = OperationType.DELETE, remark = "批量删除FinishedProductReceipt记录")
@Operation(summary = "批量删除FinishedProductReceipt", operationId = "deleteFinishedProductReceiptBatch")
@PostMapping("/deleteFinishedProductReceiptBatch")
@PreAuthorize("hasAuthority('finished_product_receipt:deleteBatch')")
@PreAuthorize("hasAuthority('finished_product_receipt:remove')")
public BaseResult<?> deleteFinishedProductReceiptBatch(@Validated(DeleteBatch.class) @RequestBody BaseDeleteBody req) {
finishedProductReceiptService.deleteBatch(req.ids());
return BaseResult.success();
}
@ApiLog(type = OperationType.UPDATE, remark = "审核FinishedProductReceipt")
@Operation(summary = "审核FinishedProductReceipt", operationId = "approveFinishedProductReceipt")
@PostMapping("/approveFinishedProductReceipt")
@PreAuthorize("hasAuthority('finished_product_receipt:approve')")
public BaseResult<?> approveFinishedProductReceipt(
@Parameter(description = "成品入库单ID") @RequestParam Long id) {
finishedProductReceiptService.approve(id);
return BaseResult.success();
}
@ApiLog(type = OperationType.UPDATE, remark = "反审核FinishedProductReceipt")
@Operation(summary = "反审核FinishedProductReceipt", operationId = "unapproveFinishedProductReceipt")
@PostMapping("/unapproveFinishedProductReceipt")
@PreAuthorize("hasAuthority('finished_product_receipt:unapprove')")
public BaseResult<?> unapproveFinishedProductReceipt(
@Parameter(description = "成品入库单ID") @RequestParam Long id) {
finishedProductReceiptService.unapprove(id);
return BaseResult.success();
}
@Operation(summary = "获取FinishedProductReceipt明细", operationId = "getFinishedProductReceiptDetail")
@GetMapping("/getFinishedProductReceiptDetail")
@PreAuthorize("hasAuthority('finishedproductreceipt:index')")
@PreAuthorize("hasAuthority('finished_product_receipt:index')")
public BaseResult<List<DeviceDto>> getFinishedProductReceiptDetail(
@Parameter(description = "成品入库单ID") @RequestParam Long id) {
return BaseResult.successWithData(finishedProductReceiptService.getDetail(id));
}
@Operation(summary = "导入FinishedProductReceipt明细", operationId = "importFinishedProductReceiptItems")
@PostMapping("/importFinishedProductReceiptItems")
@PreAuthorize("hasAuthority('finished_product_receipt:import')")
public BaseResult<List<DeviceDto>> importFinishedProductReceiptItems(
@Parameter(description = "Excel文件") @RequestParam("file") MultipartFile file) {
return BaseResult.successWithData(finishedProductReceiptService.importItems(file));
@ApiLog(type = OperationType.UPDATE, remark = "获取未出货设备列表")
@Operation(summary = "获取未出货设备列表", operationId = "getOutstockData")
@GetMapping("/getOutstockData")
@PreAuthorize("hasAuthority('finished_product_receipt:outstock')")
public BaseResult<List<DeviceDto>> getOutstockData(
@Parameter(description = "成品入库单ID") @RequestParam Long id) {
return BaseResult.successWithData(finishedProductReceiptService.getOutstockData(id));
}
@ApiLog(type = OperationType.UPDATE, remark = "执行设备出货操作")
@Operation(summary = "执行设备出货操作", operationId = "saveOutstock")
@PostMapping("/saveOutstock")
@PreAuthorize("hasAuthority('finished_product_receipt:outstock')")
public BaseResult<?> saveOutstock(@Validated @RequestBody DeviceOutstockRequest request) {
finishedProductReceiptService.saveOutstock(request);
return BaseResult.success();
}
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.niuan.erp.common.base.BasePageReqParams;
import com.niuan.erp.module.common.entity.Document;
import com.niuan.erp.module.production.controller.dto.DeviceOutstockRequest;
import com.niuan.erp.module.production.controller.dto.FinishedProductReceiptAddDto;
import com.niuan.erp.module.production.controller.dto.FinishedProductReceiptDto;
import com.niuan.erp.module.sale.controller.dto.DeviceDto;
@@ -23,11 +24,15 @@ public interface FinishedProductReceiptService {
void deleteBatch(List<Long> ids);
void approve(long id);
void unapprove(long id);
List<DeviceDto> getDetail(long id);
List<DeviceDto> importItems(MultipartFile file);
/**
* 获取未出货的设备列表
*/
List<DeviceDto> getOutstockData(long id);
/**
* 执行设备出货操作
*/
void saveOutstock(DeviceOutstockRequest request);
}

View File

@@ -12,6 +12,8 @@ import com.niuan.erp.module.common.entity.Document;
import com.niuan.erp.module.common.enums.DocumentType;
import com.niuan.erp.module.common.enums.FormStatus;
import com.niuan.erp.module.common.mapper.DocumentMapper;
import com.niuan.erp.module.production.controller.dto.DeviceOutstockItem;
import com.niuan.erp.module.production.controller.dto.DeviceOutstockRequest;
import com.niuan.erp.module.production.controller.dto.FinishedProductReceiptAddDto;
import com.niuan.erp.module.production.controller.dto.FinishedProductReceiptDto;
import com.niuan.erp.module.production.converter.FinishedProductReceiptConverter;
@@ -26,9 +28,7 @@ import org.apache.poi.ss.usermodel.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@@ -49,7 +49,8 @@ public class FinishedProductReceiptServiceImpl extends ServiceImpl<DocumentMappe
@Override
public IPage<FinishedProductReceiptDto> getFinishedProductReceiptPage(BasePageReqParams pageParams, LambdaQueryWrapper<Document> wrapper) {
wrapper.eq(Document::getFormType, DocumentType.FINISHED_PRODUCT_RECEIPT);
wrapper.eq(Document::getFormType, DocumentType.FINISHED_PRODUCT_RECEIPT)
.orderByDesc(Document::getCreateDate);
IPage<Document> result = this.baseMapper.selectPage(new Page<>(pageParams.page(), pageParams.pageSize()), wrapper);
return result.convert(finishedProductReceiptConverter::toDto);
}
@@ -71,6 +72,30 @@ public class FinishedProductReceiptServiceImpl extends ServiceImpl<DocumentMappe
throw new BusinessException("production.finished_product_receipt.exception.duplicate_sn_in_request");
}
List<String> macList = deviceItems.stream()
.map(DeviceAddDto::mac)
.filter(mac -> mac != null && !mac.isEmpty())
.toList();
if (!macList.isEmpty()) {
Set<String> uniqueMacSet = new HashSet<>(macList);
if (uniqueMacSet.size() != macList.size()) {
throw new BusinessException("production.finished_product_receipt.exception.duplicate_mac_in_request");
}
}
List<String> serialNumList = deviceItems.stream()
.map(DeviceAddDto::serialNum)
.filter(serialNum -> serialNum != null && !serialNum.isEmpty())
.toList();
if (!serialNumList.isEmpty()) {
Set<String> uniqueSerialNumSet = new HashSet<>(serialNumList);
if (uniqueSerialNumSet.size() != serialNumList.size()) {
throw new BusinessException("production.finished_product_receipt.exception.duplicate_serial_num_in_request");
}
}
List<String> invalidStatusSn = deviceItems.stream()
.filter(item -> !"已激活".equals(item.alTxt()))
.map(item -> "SN号" + item.productSn())
@@ -96,13 +121,51 @@ public class FinishedProductReceiptServiceImpl extends ServiceImpl<DocumentMappe
throw new BusinessException("production.finished_product_receipt.exception.sn_already_exists");
}
if (!macList.isEmpty()) {
LambdaQueryWrapper<Device> macWrapper = new LambdaQueryWrapper<>();
macWrapper.in(Device::getMac, macList);
List<Device> existingMacList = deviceMapper.selectList(macWrapper);
Set<String> existingMacSet = existingMacList.stream()
.map(Device::getMac)
.filter(mac -> mac != null)
.collect(Collectors.toSet());
List<String> duplicateMacInDb = macList.stream()
.filter(existingMacSet::contains)
.map(mac -> "MAC地址" + mac)
.toList();
if (!duplicateMacInDb.isEmpty()) {
throw new BusinessException("production.finished_product_receipt.exception.mac_already_exists");
}
}
if (!serialNumList.isEmpty()) {
LambdaQueryWrapper<Device> serialNumWrapper = new LambdaQueryWrapper<>();
serialNumWrapper.in(Device::getSerialNum, serialNumList);
List<Device> existingSerialNumList = deviceMapper.selectList(serialNumWrapper);
Set<String> existingSerialNumSet = existingSerialNumList.stream()
.map(Device::getSerialNum)
.filter(serialNum -> serialNum != null)
.collect(Collectors.toSet());
List<String> duplicateSerialNumInDb = serialNumList.stream()
.filter(existingSerialNumSet::contains)
.map(serialNum -> "序列号:" + serialNum)
.toList();
if (!duplicateSerialNumInDb.isEmpty()) {
throw new BusinessException("production.finished_product_receipt.exception.serial_num_already_exists");
}
}
Document entity = finishedProductReceiptConverter.toEntity(dto);
entity.setCreateUserId(SecurityUtils.getUserId());
entity.setCreateUserName(SecurityUtils.getUserName());
entity.setCreateDate(LocalDateTime.now());
entity.setStatus(0);
entity.setFormType(DocumentType.FINISHED_PRODUCT_RECEIPT);
entity.setFormStatus(FormStatus.NO_APPROVE);
entity.setFormStatus(FormStatus.APPROVE);
entity.setTotalValue(Double.valueOf(deviceItems.size()));
entity.setCustomerId(SecurityUtils.getCustomerId());
this.baseMapper.insert(entity);
@@ -179,70 +242,7 @@ public class FinishedProductReceiptServiceImpl extends ServiceImpl<DocumentMappe
this.baseMapper.deleteBatchIds(ids);
}
@Override
public void approve(long id) {
Document entity = this.baseMapper.selectById(id);
if (entity == null) {
throw new BusinessException("production.finished_product_receipt.exception.not_found");
}
if (entity.getFormStatus() == FormStatus.APPROVE) {
throw new BusinessException("production.finished_product_receipt.exception.already_approved");
}
LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Device::getDocumentNo, id);
List<Device> items = deviceMapper.selectList(wrapper);
if (items == null || items.isEmpty()) {
throw new BusinessException("production.finished_product_receipt.exception.no_device_items");
}
entity.setFormStatus(FormStatus.APPROVE);
entity.setUpdateDate(LocalDateTime.now());
entity.setUpdateUserId(SecurityUtils.getUserId());
entity.setUpdateUserName(SecurityUtils.getUserName());
this.baseMapper.updateById(entity);
LambdaUpdateWrapper<Device> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Device::getDocumentNo, id)
.set(Device::getUpdateDate, LocalDateTime.now())
.set(Device::getUpdateUserId, SecurityUtils.getUserId())
.set(Device::getUpdateUserName, SecurityUtils.getUserName());
deviceMapper.update(null, updateWrapper);
}
@Override
public void unapprove(long id) {
Document entity = this.baseMapper.selectById(id);
if (entity == null) {
throw new BusinessException("production.finished_product_receipt.exception.not_found");
}
if (entity.getFormStatus() != FormStatus.APPROVE) {
throw new BusinessException("production.finished_product_receipt.exception.not_approved");
}
LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Device::getDocumentNo, id);
List<Device> items = deviceMapper.selectList(wrapper);
boolean hasShipped = items.stream()
.anyMatch(item -> item.getOutStatus() != null && item.getOutStatus());
if (hasShipped) {
throw new BusinessException("production.finished_product_receipt.exception.cannot_unapprove_with_shipped");
}
entity.setFormStatus(FormStatus.NO_APPROVE);
entity.setUpdateDate(LocalDateTime.now());
entity.setUpdateUserId(SecurityUtils.getUserId());
entity.setUpdateUserName(SecurityUtils.getUserName());
this.baseMapper.updateById(entity);
LambdaUpdateWrapper<Device> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Device::getDocumentNo, id)
.set(Device::getUpdateDate, LocalDateTime.now())
.set(Device::getUpdateUserId, SecurityUtils.getUserId())
.set(Device::getUpdateUserName, SecurityUtils.getUserName());
deviceMapper.update(null, updateWrapper);
}
@Override
public List<DeviceDto> getDetail(long id) {
@@ -253,46 +253,91 @@ public class FinishedProductReceiptServiceImpl extends ServiceImpl<DocumentMappe
}
@Override
public List<DeviceDto> importItems(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException("production.finished_product_receipt.exception.file_empty");
public List<DeviceDto> getOutstockData(long id) {
LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Device::getDocumentNo, id)
.eq(Device::getOutStatus, false)
.eq(Device::getCustomerId, SecurityUtils.getCustomerId());
List<Device> devices = deviceMapper.selectList(wrapper);
return deviceConverter.toDtoList(devices);
}
@Override
public void saveOutstock(DeviceOutstockRequest request) {
if (request == null) {
throw new BusinessException("production.finished_product_receipt.exception.request_null");
}
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0);
List<DeviceDto> items = new ArrayList<>();
if (request.keyAccountId() <= 0) {
throw new BusinessException("production.finished_product_receipt.exception.invalid_key_account");
}
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
if (request.outstockList() == null || request.outstockList().isEmpty()) {
throw new BusinessException("production.finished_product_receipt.exception.empty_outstock_list");
}
String productType = getCellValueAsString(row.getCell(0));
String productSn = getCellValueAsString(row.getCell(1));
String mac = getCellValueAsString(row.getCell(2));
String serialNum = getCellValueAsString(row.getCell(3));
String softVersion = getCellValueAsString(row.getCell(4));
String alVersion = getCellValueAsString(row.getCell(5));
String alNum = getCellValueAsString(row.getCell(6));
String alTxt = getCellValueAsString(row.getCell(7));
String manufacturingDate = getCellValueAsString(row.getCell(8));
String mark = getCellValueAsString(row.getCell(9));
// 提取有效SN列表
List<String> validSnList = request.outstockList().stream()
.map(DeviceOutstockItem::productSn)
.filter(sn -> sn != null && !sn.trim().isEmpty())
.distinct()
.toList();
if (validSnList.isEmpty()) {
throw new BusinessException("production.finished_product_receipt.exception.invalid_sn_list");
}
// 检查单据是否存在
Document document = this.baseMapper.selectById(request.id());
if (document == null) {
throw new BusinessException("production.finished_product_receipt.exception.document_not_found");
}
// 检查单据状态
if (document.getFormStatus() != FormStatus.APPROVE) {
throw new BusinessException("production.finished_product_receipt.exception.document_not_approved");
}
// 使用事务处理出货操作
try {
// 更新设备出货状态
LambdaUpdateWrapper<Device> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.in(Device::getProductSn, validSnList)
.eq(Device::getDocumentNo, request.id())
.set(Device::getOutStatus, true)
.set(Device::getKeyAccountId, request.keyAccountId())
.set(Device::getOutProductDate, LocalDateTime.now())
.set(Device::getUpdateDate, LocalDateTime.now())
.set(Device::getUpdateUserId, SecurityUtils.getUserId())
.set(Device::getUpdateUserName, SecurityUtils.getUserName());
int updatedCount = deviceMapper.update(null, updateWrapper);
if (updatedCount == 0) {
throw new BusinessException("production.finished_product_receipt.exception.no_device_updated");
}
return items;
} catch (IOException e) {
throw new BusinessException("production.finished_product_receipt.exception.import_failed");
}
}
// 检查是否所有设备都已出货
LambdaQueryWrapper<Device> countWrapper = new LambdaQueryWrapper<>();
countWrapper.eq(Device::getDocumentNo, request.id())
.eq(Device::getOutStatus, false);
long remainingCount = deviceMapper.selectCount(countWrapper);
private String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
// 更新单据状态
if (remainingCount == 0) {
// 所有设备都已出货
document.setFormStatus(FormStatus.COMPLETE);
} else {
// 部分设备已出货
document.setFormStatus(FormStatus.NO_COMPLETE);
}
document.setUpdateDate(LocalDateTime.now());
document.setUpdateUserId(SecurityUtils.getUserId());
document.setUpdateUserName(SecurityUtils.getUserName());
this.baseMapper.updateById(document);
} catch (Exception e) {
throw new BusinessException("production.finished_product_receipt.exception.outstock_failed");
}
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue().trim();
case NUMERIC -> String.valueOf((long) cell.getNumericCellValue());
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
default -> "";
};
}
}

View File

@@ -7,15 +7,20 @@ import com.niuan.erp.common.base.BasePageReqParams;
import com.niuan.erp.common.base.BaseResult;
import com.niuan.erp.common.base.CommonValidateGroup.DeleteBatch;
import com.niuan.erp.common.base.CommonValidateGroup.DeleteOne;
import com.niuan.erp.common.base.BaseSelectDto;
import com.niuan.erp.module.sys.controller.dto.KeyAccountDto;
import com.niuan.erp.module.sys.entity.KeyAccount;
import com.niuan.erp.module.sys.service.KeyAccountService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/sys/keyaccount")
@RequiredArgsConstructor
@@ -33,6 +38,13 @@ public class KeyAccountController {
return BaseResult.successWithData(keyAccountService.getKeyAccountPage(dto, wrapper));
}
@Operation(summary = "查询客户下拉列表", operationId = "getKeyAccountSelectList")
@GetMapping("/getKeyAccountSelectList")
@PreAuthorize("hasAnyAuthority('finished_product_receipt:add')")
public BaseResult<List<BaseSelectDto>> getKeyAccountSelectList() {
return BaseResult.successWithData(keyAccountService.getKeyAccountSelectList());
}
@PostMapping("/addKeyAccount")
public BaseResult<?> addKeyAccount(@Valid @RequestBody KeyAccountDto dto) {
keyAccountService.addKeyAccount(dto);

View File

@@ -1,8 +1,11 @@
package com.niuan.erp.module.sys.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.niuan.erp.common.base.BaseSelectDto;
import com.niuan.erp.module.sys.entity.KeyAccount;
import java.util.List;
/**
* <p>
* Mapper 接口
@@ -13,4 +16,6 @@ import com.niuan.erp.module.sys.entity.KeyAccount;
*/
public interface KeyAccountMapper extends BaseMapper<KeyAccount> {
List<BaseSelectDto> getKeyAccountSelectList();
}

View File

@@ -3,6 +3,7 @@ package com.niuan.erp.module.sys.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.niuan.erp.common.base.BasePageReqParams;
import com.niuan.erp.common.base.BaseSelectDto;
import com.niuan.erp.module.sys.controller.dto.KeyAccountDto;
import com.niuan.erp.module.sys.entity.KeyAccount;
@@ -12,6 +13,8 @@ public interface KeyAccountService {
IPage<KeyAccountDto> getKeyAccountPage(BasePageReqParams dto, LambdaQueryWrapper<KeyAccount> wrapper);
List<BaseSelectDto> getKeyAccountSelectList();
void addKeyAccount(KeyAccountDto dto);
void updateKeyAccount(KeyAccountDto dto);

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.niuan.erp.common.base.BasePageReqParams;
import com.niuan.erp.common.base.BaseSelectDto;
import com.niuan.erp.common.utils.SecurityUtils;
import com.niuan.erp.module.sys.controller.dto.KeyAccountDto;
import com.niuan.erp.module.sys.converter.KeyAccountConverter;
@@ -33,6 +34,11 @@ public class KeyAccountServiceImpl extends ServiceImpl<KeyAccountMapper, KeyAcco
return result.convert(keyAccountConverter::toDto);
}
@Override
public List<BaseSelectDto> getKeyAccountSelectList() {
return this.baseMapper.getKeyAccountSelectList();
}
@Override
public void addKeyAccount(KeyAccountDto dto) {
KeyAccount entity = keyAccountConverter.toEntity(dto);