新增采购

This commit is contained in:
2026-06-27 10:40:54 +08:00
parent ce3998db74
commit 66d2b33db5
25 changed files with 1261 additions and 227 deletions

View File

@@ -13,6 +13,7 @@ import com.klp.common.utils.poi.ExcelUtil;
import com.klp.erp.domain.bo.ErpPurchasePlanAuditBo;
import com.klp.erp.domain.bo.ErpPurchasePlanBo;
import com.klp.erp.domain.vo.ErpContractOptionVo;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryBatchVo;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryVo;
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
@@ -131,7 +132,7 @@ public class ErpPurchasePlanController extends BaseController {
@PostMapping(value = "/{planId}/importDelivery", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Map<String, Object>> importDelivery(@PathVariable Long planId,
@RequestPart("file") MultipartFile file) throws Exception {
Map<String, Object> result = iErpPurchasePlanService.importDelivery(planId, file.getInputStream(), getUsername());
Map<String, Object> result = iErpPurchasePlanService.importDelivery(planId, file.getInputStream(), file.getOriginalFilename(), getUsername());
return R.ok(String.valueOf(result.get("message")), result);
}
@@ -141,6 +142,25 @@ public class ErpPurchasePlanController extends BaseController {
return R.ok(iErpPurchasePlanService.queryDeliveryList(planId));
}
/** 某计划的到货上传批次(每次上传一条,可回看) */
@GetMapping("/{planId}/deliveryBatches")
public R<List<ErpPurchasePlanDeliveryBatchVo>> deliveryBatches(@PathVariable Long planId) {
return R.ok(iErpPurchasePlanService.queryDeliveryBatches(planId));
}
/** 某批次的到货明细 */
@GetMapping("/deliveryBatch/{batchId}")
public R<List<ErpPurchasePlanDeliveryVo>> deliveryByBatch(@PathVariable Long batchId) {
return R.ok(iErpPurchasePlanService.queryDeliveryListByBatch(batchId));
}
/** 到货记录页左侧:审核通过的计划分页 */
@GetMapping("/deliveryPlans")
public TableDataInfo<ErpPurchasePlanVo> deliveryPlans(@RequestParam(value = "keyword", required = false) String keyword,
PageQuery pageQuery) {
return iErpPurchasePlanService.queryDeliveryPlanPage(keyword, pageQuery);
}
/** 删除到货明细 */
@Log(title = "采购计划-到货", businessType = BusinessType.DELETE)
@DeleteMapping("/delivery/{deliveryId}")

View File

@@ -0,0 +1,67 @@
package com.klp.erp.controller;
import com.klp.common.annotation.Log;
import com.klp.common.annotation.RepeatSubmit;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.R;
import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.erp.domain.bo.ErpSupplierTransactionBo;
import com.klp.erp.domain.vo.ErpSupplierTransactionVo;
import com.klp.erp.service.IErpSupplierTransactionService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 供应商往来流水
*
* @author klp
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/erp/supplierTxn")
public class ErpSupplierTransactionController extends BaseController {
private final IErpSupplierTransactionService txnService;
/** 某供应商的往来流水(含逐笔余额) */
@GetMapping("/list")
public R<List<ErpSupplierTransactionVo>> list(@RequestParam("supplierId") Long supplierId) {
return R.ok(txnService.queryBySupplier(supplierId));
}
/** 某供应商的往来汇总 */
@GetMapping("/summary")
public R<Map<String, Object>> summary(@RequestParam("supplierId") Long supplierId) {
return R.ok(txnService.summary(supplierId));
}
@Log(title = "供应商往来", businessType = BusinessType.INSERT)
@RepeatSubmit
@PostMapping
public R<Void> add(@Validated(AddGroup.class) @RequestBody ErpSupplierTransactionBo bo) {
return toAjax(txnService.insertByBo(bo));
}
@Log(title = "供应商往来", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PutMapping
public R<Void> edit(@Validated(EditGroup.class) @RequestBody ErpSupplierTransactionBo bo) {
return toAjax(txnService.updateByBo(bo));
}
@Log(title = "供应商往来", businessType = BusinessType.DELETE)
@DeleteMapping("/{txnIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] txnIds) {
return toAjax(txnService.deleteByIds(Arrays.asList(txnIds)));
}
}

View File

@@ -54,9 +54,15 @@ public class ErpPurchasePlan extends BaseEntity {
/** 计划总重量(T) */
private BigDecimal planWeight;
/** 已到货重量(T) */
/** 已到货重量(T) = Σ到货卷(WMS已确认)单卷重量 */
private BigDecimal arrivedWeight;
/** 计划要求总数量(卷/件) = Σ明细数量 */
private Integer planQty;
/** 已到货卷数 = WMS确认到货的上传卷数 */
private Integer arrivedCount;
/** 删除标志 */
@TableLogic
private String delFlag;

View File

@@ -30,6 +30,12 @@ public class ErpPurchasePlanDelivery extends BaseEntity {
/** 关联计划ID */
private Long planId;
/** 关联上传批次ID */
private Long batchId;
/** 是否WMS已确认到货: 1-是 0-否(卷号在钢卷表查不到或为占位类型) */
private Integer arrived;
/** 日期 */
private Date arrivalDate;

View File

@@ -0,0 +1,50 @@
package com.klp.erp.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 到货上传批次对象 erp_purchase_plan_delivery_batch
* 每上传一份到货 Excel 记一条批次,便于随时回看每一次上传。
*
* @author klp
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("erp_purchase_plan_delivery_batch")
public class ErpPurchasePlanDeliveryBatch extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 批次ID */
@TableId(value = "batch_id")
private Long batchId;
/** 关联计划ID */
private Long planId;
/** 文件名 */
private String fileName;
/** 本次导入到货行数 */
private Integer rowCount;
/** 本次与明细卷号匹配上的卷数 */
private Integer matchedCount;
/** 上传后计划到货百分比快照(0-100) */
private BigDecimal arrivedPercent;
/** 删除标志 */
@TableLogic
private String delFlag;
/** 备注 */
private String remark;
}

View File

@@ -0,0 +1,50 @@
package com.klp.erp.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.klp.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
/**
* 供应商往来流水对象 erp_supplier_transaction
*
* @author klp
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("erp_supplier_transaction")
public class ErpSupplierTransaction extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 往来流水ID */
@TableId(value = "txn_id")
private Long txnId;
/** 关联供应商ID */
private Long supplierId;
/** 发生日期 */
private Date txnDate;
/** 类型: 1-采购应付 2-付款 3-退货 4-其他 */
private String txnType;
/** 金额 */
private BigDecimal amount;
/** 单据号 */
private String docNo;
/** 删除标志 */
@TableLogic
private String delFlag;
/** 备注 */
private String remark;
}

View File

@@ -0,0 +1,39 @@
package com.klp.erp.domain.bo;
import com.klp.common.core.domain.BaseEntity;
import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.Date;
/**
* 供应商往来流水业务对象
*
* @author klp
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ErpSupplierTransactionBo extends BaseEntity {
@NotNull(message = "主键不能为空", groups = {EditGroup.class})
private Long txnId;
@NotNull(message = "供应商不能为空", groups = {AddGroup.class, EditGroup.class})
private Long supplierId;
private Date txnDate;
@NotNull(message = "类型不能为空", groups = {AddGroup.class, EditGroup.class})
private String txnType;
@NotNull(message = "金额不能为空", groups = {AddGroup.class, EditGroup.class})
private BigDecimal amount;
private String docNo;
private String remark;
}

View File

@@ -0,0 +1,38 @@
package com.klp.erp.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 到货上传批次视图对象
*
* @author klp
*/
@Data
public class ErpPurchasePlanDeliveryBatchVo implements Serializable {
private static final long serialVersionUID = 1L;
private Long batchId;
private Long planId;
private String fileName;
private Integer rowCount;
private Integer matchedCount;
private BigDecimal arrivedPercent;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String remark;
}

View File

@@ -26,6 +26,11 @@ public class ErpPurchasePlanDeliveryVo implements Serializable {
private Long planId;
private Long batchId;
/** 是否WMS已确认到货: 1-是 0-否 */
private Integer arrived;
@ExcelProperty(value = "日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date arrivalDate;

View File

@@ -59,12 +59,21 @@ public class ErpPurchasePlanVo implements Serializable {
@ExcelProperty(value = "已到货重量(T)")
private BigDecimal arrivedWeight;
/** 计划要求总数量(卷/件) */
private Integer planQty;
/** 已到货卷数 */
private Integer arrivedCount;
@ExcelProperty(value = "备注")
private String remark;
/** 到货进度百分比(0-100) */
/** 到货进度-按重量(0-100) = 已到货重量/计划要求总重量 */
private BigDecimal progress;
/** 到货进度-按卷数(0-100) = 已到货卷数/计划要求总数量 */
private BigDecimal progressQty;
/** 明细行 */
private List<ErpPurchasePlanItemVo> items;

View File

@@ -0,0 +1,43 @@
package com.klp.erp.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 供应商往来流水视图对象
*
* @author klp
*/
@Data
public class ErpSupplierTransactionVo implements Serializable {
private static final long serialVersionUID = 1L;
private Long txnId;
private Long supplierId;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date txnDate;
/** 类型: 1-采购应付 2-付款 3-退货 4-其他 */
private String txnType;
private BigDecimal amount;
private String docNo;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String remark;
/** 计算字段:截至该笔的应付余额(正数=欠供应商) */
private BigDecimal balance;
}

View File

@@ -0,0 +1,13 @@
package com.klp.erp.mapper;
import com.klp.common.core.mapper.BaseMapperPlus;
import com.klp.erp.domain.ErpPurchasePlanDeliveryBatch;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryBatchVo;
/**
* 到货上传批次Mapper接口
*
* @author klp
*/
public interface ErpPurchasePlanDeliveryBatchMapper extends BaseMapperPlus<ErpPurchasePlanDeliveryBatchMapper, ErpPurchasePlanDeliveryBatch, ErpPurchasePlanDeliveryBatchVo> {
}

View File

@@ -9,6 +9,7 @@ import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/**
@@ -44,4 +45,10 @@ public interface ErpPurchasePlanMapper extends BaseMapperPlus<ErpPurchasePlanMap
* 按合同关键字(订单编号/合同号/合同名称查出关联的采购计划ID用于综合搜索。
*/
List<Long> selectPlanIdsByContractKeyword(@Param("kw") String kw);
/**
* 在 WMS 钢卷表(wms_material_coil)中查出已存在且非占位(data_type<>10)的厂家卷号集合,
* 用于判定上传到货行的卷号是否已实际到货入库。
*/
List<String> selectExistingSupplierCoilNos(@Param("coilNos") Collection<String> coilNos);
}

View File

@@ -0,0 +1,13 @@
package com.klp.erp.mapper;
import com.klp.common.core.mapper.BaseMapperPlus;
import com.klp.erp.domain.ErpSupplierTransaction;
import com.klp.erp.domain.vo.ErpSupplierTransactionVo;
/**
* 供应商往来流水Mapper接口
*
* @author klp
*/
public interface ErpSupplierTransactionMapper extends BaseMapperPlus<ErpSupplierTransactionMapper, ErpSupplierTransaction, ErpSupplierTransactionVo> {
}

View File

@@ -6,6 +6,7 @@ import com.klp.erp.domain.bo.ErpPurchasePlanAuditBo;
import com.klp.erp.domain.bo.ErpPurchasePlanBo;
import com.klp.erp.domain.vo.ErpContractOptionVo;
import com.klp.erp.domain.vo.ErpPurchasePlanAuditLogVo;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryBatchVo;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryVo;
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
@@ -63,11 +64,20 @@ public interface IErpPurchasePlanService {
* 导入到货 Excel校验列/数值、kg→t 单位纠正,写入并刷新进度归档。
* 返回 {count: 入库条数, message: 回执文案, kgConverted: 是否做了单位换算}
*/
Map<String, Object> importDelivery(Long planId, InputStream inputStream, String operator);
Map<String, Object> importDelivery(Long planId, InputStream inputStream, String fileName, String operator);
/** 查询某计划的到货明细 */
/** 查询某计划的到货明细matched 标记是否与明细卷号匹配) */
List<ErpPurchasePlanDeliveryVo> queryDeliveryList(Long planId);
/** 某计划下指定批次的到货明细 */
List<ErpPurchasePlanDeliveryVo> queryDeliveryListByBatch(Long batchId);
/** 某计划的到货上传批次(最新在前) */
List<ErpPurchasePlanDeliveryBatchVo> queryDeliveryBatches(Long planId);
/** 到货记录页左侧:审核通过的计划分页(含合同号、明细数、进度) */
TableDataInfo<ErpPurchasePlanVo> queryDeliveryPlanPage(String keyword, PageQuery pageQuery);
/** 删除到货明细,并刷新进度 */
Boolean deleteDelivery(Long deliveryId);

View File

@@ -0,0 +1,28 @@
package com.klp.erp.service;
import com.klp.erp.domain.bo.ErpSupplierTransactionBo;
import com.klp.erp.domain.vo.ErpSupplierTransactionVo;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 供应商往来流水Service接口
*
* @author klp
*/
public interface IErpSupplierTransactionService {
/** 某供应商的往来流水(含逐笔累计余额,最新在前) */
List<ErpSupplierTransactionVo> queryBySupplier(Long supplierId);
/** 某供应商的往来汇总:应付合计/付款合计/退货合计/当前余额 */
Map<String, Object> summary(Long supplierId);
Boolean insertByBo(ErpSupplierTransactionBo bo);
Boolean updateByBo(ErpSupplierTransactionBo bo);
Boolean deleteByIds(Collection<Long> ids);
}

View File

@@ -13,6 +13,7 @@ import com.klp.common.utils.StringUtils;
import com.klp.erp.domain.ErpPurchasePlan;
import com.klp.erp.domain.ErpPurchasePlanContractRel;
import com.klp.erp.domain.ErpPurchasePlanDelivery;
import com.klp.erp.domain.ErpPurchasePlanDeliveryBatch;
import com.klp.erp.domain.ErpPurchasePlanItem;
import com.klp.erp.domain.ErpPurchasePlanAuditLog;
import com.klp.erp.domain.bo.ErpPurchasePlanAuditBo;
@@ -20,11 +21,13 @@ import com.klp.erp.domain.bo.ErpPurchasePlanBo;
import com.klp.erp.domain.bo.ErpPurchasePlanItemBo;
import com.klp.erp.domain.vo.ErpContractOptionVo;
import com.klp.erp.domain.vo.ErpPurchasePlanAuditLogVo;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryBatchVo;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryImportVo;
import com.klp.erp.domain.vo.ErpPurchasePlanDeliveryVo;
import com.klp.erp.domain.vo.ErpPurchasePlanItemVo;
import com.klp.erp.domain.vo.ErpPurchasePlanVo;
import com.klp.erp.mapper.ErpPurchasePlanContractRelMapper;
import com.klp.erp.mapper.ErpPurchasePlanDeliveryBatchMapper;
import com.klp.erp.mapper.ErpPurchasePlanDeliveryMapper;
import com.klp.erp.mapper.ErpPurchasePlanItemMapper;
import com.klp.erp.mapper.ErpPurchasePlanAuditLogMapper;
@@ -42,9 +45,10 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -63,14 +67,12 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
private static final String AUDIT_PASS = "1";
private static final String AUDIT_REJECT = "2";
private static final String AUDIT_DRAFT = "3";
private static final String ITEM_NOT_ARRIVED = "0";
private static final String ITEM_PARTIAL = "1";
private static final String ITEM_ARRIVED = "2";
private final ErpPurchasePlanMapper baseMapper;
private final ErpPurchasePlanItemMapper itemMapper;
private final ErpPurchasePlanContractRelMapper relMapper;
private final ErpPurchasePlanDeliveryMapper deliveryMapper;
private final ErpPurchasePlanDeliveryBatchMapper deliveryBatchMapper;
private final ErpPurchasePlanAuditLogMapper auditLogMapper;
@Override
@@ -90,7 +92,7 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
if (!orderIds.isEmpty()) {
vo.setContractCodes(baseMapper.selectOrderCodes(orderIds));
}
vo.setProgress(calcProgress(vo.getArrivedWeight(), vo.getPlanWeight()));
fillProgress(vo);
// 审核历史(含每次驳回理由)
vo.setAuditLogs(queryAuditLogs(planId));
return vo;
@@ -107,14 +109,14 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
@Override
public TableDataInfo<ErpPurchasePlanVo> queryPageList(ErpPurchasePlanBo bo, PageQuery pageQuery) {
Page<ErpPurchasePlanVo> result = baseMapper.selectVoPage(pageQuery.build(), buildQueryWrapper(bo));
result.getRecords().forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight())));
result.getRecords().forEach(this::fillProgress);
return TableDataInfo.build(result);
}
@Override
public List<ErpPurchasePlanVo> queryList(ErpPurchasePlanBo bo) {
List<ErpPurchasePlanVo> list = baseMapper.selectVoList(buildQueryWrapper(bo));
list.forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight())));
list.forEach(this::fillProgress);
return list;
}
@@ -127,7 +129,7 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
@Override
public List<ErpPurchasePlanVo> queryPlansByContract(Long orderId) {
List<ErpPurchasePlanVo> list = baseMapper.selectPlansByContract(orderId);
list.forEach(v -> v.setProgress(calcProgress(v.getArrivedWeight(), v.getPlanWeight())));
list.forEach(this::fillProgress);
return list;
}
@@ -164,6 +166,8 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
add.setAuditStatus(AUDIT_DRAFT); // 新建为「待送审」,需手动送审后才进入审核池
add.setArrivedWeight(BigDecimal.ZERO);
add.setPlanWeight(sumItemWeight(bo.getItems()));
add.setPlanQty(sumItemQty(bo.getItems()));
add.setArrivedCount(0);
if (baseMapper.insert(add) <= 0) {
return false;
}
@@ -187,6 +191,7 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
update.setPlanNo(bo.getPlanNo());
}
update.setPlanWeight(sumItemWeight(bo.getItems()));
update.setPlanQty(sumItemQty(bo.getItems()));
baseMapper.updateById(update);
// 覆盖式重写明细与合同关联
itemMapper.delete(Wrappers.lambdaQuery(ErpPurchasePlanItem.class)
@@ -195,6 +200,8 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
relMapper.delete(Wrappers.lambdaQuery(ErpPurchasePlanContractRel.class)
.eq(ErpPurchasePlanContractRel::getPlanId, bo.getPlanId()));
saveContractRels(bo.getPlanId(), bo.getOrderIds());
// 卷号匹配可能变化,刷新到货进度
refreshProgress(bo.getPlanId());
return true;
}
@@ -234,6 +241,13 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private int sumItemQty(List<ErpPurchasePlanItemBo> items) {
if (items == null) {
return 0;
}
return items.stream().mapToInt(i -> i.getQuantity() == null ? 0 : i.getQuantity()).sum();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
@@ -298,7 +312,7 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> importDelivery(Long planId, InputStream inputStream, String operator) {
public Map<String, Object> importDelivery(Long planId, InputStream inputStream, String fileName, String operator) {
ErpPurchasePlan plan = baseMapper.selectById(planId);
if (plan == null) {
throw new ServiceException("采购计划不存在");
@@ -334,11 +348,28 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
.orElse(BigDecimal.ZERO);
boolean kgConverted = maxCoil.compareTo(KG_THRESHOLD) > 0;
// 3) 合并单元格向下填充 + 单位换算 + 落
// 3) 卷号去 WMS 钢卷表校验:存在且 data_type<>10 即视为已实际到货入
List<String> uploadCoilNos = valid.stream()
.map(ErpPurchasePlanDeliveryImportVo::getCoilNo)
.filter(StringUtils::isNotBlank)
.map(String::trim)
.collect(Collectors.toList());
Set<String> wmsArrivedSet = uploadCoilNos.isEmpty() ? new HashSet<>()
: baseMapper.selectExistingSupplierCoilNos(uploadCoilNos).stream()
.map(this::normCoil).collect(Collectors.toSet());
// 4) 先建批次(每次上传存档,可随时回看)
ErpPurchasePlanDeliveryBatch batch = new ErpPurchasePlanDeliveryBatch();
batch.setPlanId(planId);
batch.setFileName(StringUtils.isNotBlank(fileName) ? fileName : "到货表");
deliveryBatchMapper.insert(batch);
// 5) 合并单元格向下填充 + 单位换算 + 落库 + WMS到货标记
String lastTruckNo = null;
BigDecimal lastTruckWeight = null;
Integer lastPieceCount = null;
int count = 0;
int matched = 0;
for (ErpPurchasePlanDeliveryImportVo row : rows) {
if (StringUtils.isNotBlank(row.getTruckNo())) {
lastTruckNo = row.getTruckNo();
@@ -348,9 +379,12 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
if (StringUtils.isBlank(row.getCoilNo()) && row.getCoilWeight() == null) {
continue;
}
boolean arrived = StringUtils.isNotBlank(row.getCoilNo()) && wmsArrivedSet.contains(normCoil(row.getCoilNo()));
BigDecimal truckWeight = row.getTruckWeight() != null ? row.getTruckWeight() : lastTruckWeight;
ErpPurchasePlanDelivery d = new ErpPurchasePlanDelivery();
d.setPlanId(planId);
d.setBatchId(batch.getBatchId());
d.setArrived(arrived ? 1 : 0);
d.setArrivalDate(parseDate(row.getArrivalDate()));
d.setGrade(row.getGrade());
d.setSpec(row.getSpec());
@@ -363,17 +397,29 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
d.setArrivalStation(row.getArrivalStation());
deliveryMapper.insert(d);
count++;
if (arrived) {
matched++;
}
}
// 6) 汇总刷新进度
refreshProgress(planId);
StringBuilder msg = new StringBuilder("成功导入 " + count + " 条到货记录");
// 7) 回填批次统计行数、WMS确认到货卷数、上传后进度快照
ErpPurchasePlan after = baseMapper.selectById(planId);
batch.setRowCount(count);
batch.setMatchedCount(matched);
batch.setArrivedPercent(calcPct(after.getArrivedWeight(), after.getPlanWeight()));
deliveryBatchMapper.updateById(batch);
StringBuilder msg = new StringBuilder("成功导入 " + count + " 条到货记录,其中 " + matched + " 卷在钢卷表确认到货");
if (kgConverted) {
msg.append(";检测到重量疑似按 kg 录入(最大单卷 ")
.append(maxCoil.stripTrailingZeros().toPlainString())
.append("),已自动 ÷1000 换算为吨");
}
Map<String, Object> result = new HashMap<>(4);
Map<String, Object> result = new HashMap<>(6);
result.put("count", count);
result.put("matched", matched);
result.put("kgConverted", kgConverted);
result.put("message", msg.toString());
return result;
@@ -391,7 +437,40 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
public List<ErpPurchasePlanDeliveryVo> queryDeliveryList(Long planId) {
return deliveryMapper.selectVoList(Wrappers.lambdaQuery(ErpPurchasePlanDelivery.class)
.eq(ErpPurchasePlanDelivery::getPlanId, planId)
.orderByAsc(ErpPurchasePlanDelivery::getTruckNo, ErpPurchasePlanDelivery::getDeliveryId));
.orderByDesc(ErpPurchasePlanDelivery::getDeliveryId));
}
@Override
public List<ErpPurchasePlanDeliveryVo> queryDeliveryListByBatch(Long batchId) {
return deliveryMapper.selectVoList(Wrappers.lambdaQuery(ErpPurchasePlanDelivery.class)
.eq(ErpPurchasePlanDelivery::getBatchId, batchId)
.orderByDesc(ErpPurchasePlanDelivery::getDeliveryId));
}
@Override
public List<ErpPurchasePlanDeliveryBatchVo> queryDeliveryBatches(Long planId) {
return deliveryBatchMapper.selectVoList(Wrappers.lambdaQuery(ErpPurchasePlanDeliveryBatch.class)
.eq(ErpPurchasePlanDeliveryBatch::getPlanId, planId)
.orderByDesc(ErpPurchasePlanDeliveryBatch::getBatchId));
}
@Override
public TableDataInfo<ErpPurchasePlanVo> queryDeliveryPlanPage(String keyword, PageQuery pageQuery) {
ErpPurchasePlanBo bo = new ErpPurchasePlanBo();
bo.setKeyword(keyword);
LambdaQueryWrapper<ErpPurchasePlan> lqw = buildQueryWrapper(bo)
.eq(ErpPurchasePlan::getAuditStatus, AUDIT_PASS);
Page<ErpPurchasePlanVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
for (ErpPurchasePlanVo v : result.getRecords()) {
fillProgress(v);
List<Long> orderIds = relMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanContractRel.class)
.eq(ErpPurchasePlanContractRel::getPlanId, v.getPlanId())).stream()
.map(ErpPurchasePlanContractRel::getOrderId).collect(Collectors.toList());
if (!orderIds.isEmpty()) {
v.setContractCodes(baseMapper.selectOrderCodes(orderIds));
}
}
return TableDataInfo.build(result);
}
@Override
@@ -413,124 +492,29 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
if (plan == null) {
return;
}
// 1. 计划总到货 = Σ单卷重量
BigDecimal arrived = deliveryMapper.sumCoilWeightByPlan(planId);
if (arrived == null) {
arrived = BigDecimal.ZERO;
}
plan.setArrivedWeight(arrived);
// 2. 明细级回填:到货行按 牌号+规格(厚×宽) 聚合,再顺序分配到匹配明细
List<ErpPurchasePlanItem> items = itemMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanItem.class)
.eq(ErpPurchasePlanItem::getPlanId, planId));
// 到货 = 累计所有上传批次中 WMS 已确认到货(arrived=1)的卷
List<ErpPurchasePlanDelivery> deliveries = deliveryMapper.selectList(Wrappers.lambdaQuery(ErpPurchasePlanDelivery.class)
.eq(ErpPurchasePlanDelivery::getPlanId, planId));
Map<String, BigDecimal> arrivedByKey = new HashMap<>();
int arrivedCount = 0;
BigDecimal arrivedWeight = BigDecimal.ZERO;
for (ErpPurchasePlanDelivery d : deliveries) {
String key = specKey(d.getGrade(), d.getSpec());
if (key == null) {
continue;
}
BigDecimal w = d.getCoilWeight() == null ? BigDecimal.ZERO : d.getCoilWeight();
arrivedByKey.merge(key, w, BigDecimal::add);
}
// 明细按 key 分组(保持原顺序),便于同规格多行顺序分配
Map<String, List<ErpPurchasePlanItem>> itemsByKey = new LinkedHashMap<>();
for (ErpPurchasePlanItem it : items) {
it.setArrivedWeight(BigDecimal.ZERO);
it.setItemStatus(ITEM_NOT_ARRIVED);
String key = itemSpecKey(it.getGrade(), it.getThickness(), it.getWidth());
if (key != null) {
itemsByKey.computeIfAbsent(key, k -> new ArrayList<>()).add(it);
if (d.getArrived() != null && d.getArrived() == 1) {
arrivedCount++;
arrivedWeight = arrivedWeight.add(d.getCoilWeight() == null ? BigDecimal.ZERO : d.getCoilWeight());
}
}
for (Map.Entry<String, List<ErpPurchasePlanItem>> e : itemsByKey.entrySet()) {
BigDecimal remaining = arrivedByKey.getOrDefault(e.getKey(), BigDecimal.ZERO);
List<ErpPurchasePlanItem> group = e.getValue();
for (int i = 0; i < group.size(); i++) {
ErpPurchasePlanItem it = group.get(i);
BigDecimal planned = it.getWeight() == null ? BigDecimal.ZERO : it.getWeight();
// 末项吃掉剩余(含富余),其余按计划量封顶
BigDecimal give = (i == group.size() - 1) ? remaining : remaining.min(planned);
if (give.compareTo(BigDecimal.ZERO) < 0) {
give = BigDecimal.ZERO;
}
it.setArrivedWeight(give);
remaining = remaining.subtract(give);
if (remaining.compareTo(BigDecimal.ZERO) < 0) {
remaining = BigDecimal.ZERO;
}
}
}
// 3. 明细状态 + 是否全部到货
boolean allArrived = !items.isEmpty();
for (ErpPurchasePlanItem it : items) {
BigDecimal planned = it.getWeight() == null ? BigDecimal.ZERO : it.getWeight();
BigDecimal aw = it.getArrivedWeight() == null ? BigDecimal.ZERO : it.getArrivedWeight();
if (aw.compareTo(BigDecimal.ZERO) <= 0) {
it.setItemStatus(ITEM_NOT_ARRIVED);
} else if (planned.compareTo(BigDecimal.ZERO) > 0 && aw.compareTo(planned) >= 0) {
it.setItemStatus(ITEM_ARRIVED);
} else {
it.setItemStatus(ITEM_PARTIAL);
}
if (!ITEM_ARRIVED.equals(it.getItemStatus())) {
allArrived = false;
}
itemMapper.updateById(it);
}
// 4. 计划状态:所有明细到货 或 总量达标 → 自动归档
plan.setArrivedCount(arrivedCount);
plan.setArrivedWeight(arrivedWeight);
// 到货重量达到计划要求总重量 → 自动归档
BigDecimal planWeight = plan.getPlanWeight() == null ? BigDecimal.ZERO : plan.getPlanWeight();
boolean weightDone = planWeight.compareTo(BigDecimal.ZERO) > 0 && arrived.compareTo(planWeight) >= 0;
plan.setPlanStatus((allArrived || weightDone) ? PLAN_STATUS_ARCHIVED : PLAN_STATUS_ONGOING);
boolean done = planWeight.compareTo(BigDecimal.ZERO) > 0 && arrivedWeight.compareTo(planWeight) >= 0;
plan.setPlanStatus(done ? PLAN_STATUS_ARCHIVED : PLAN_STATUS_ONGOING);
baseMapper.updateById(plan);
}
/** 到货行规格 key牌号 + 厚×宽(规格形如 "3.00×1230" */
private String specKey(String grade, String spec) {
if (StringUtils.isBlank(spec)) {
return null;
}
String[] parts = spec.split("[×xX*]");
if (parts.length < 2) {
return null;
}
String t = normNum(parts[0]);
String w = normNum(parts[1]);
if (t == null || w == null) {
return null;
}
return normGrade(grade) + "|" + t + "×" + w;
}
/** 明细规格 key牌号 + 厚×宽 */
private String itemSpecKey(String grade, String thickness, String width) {
String t = normNum(thickness);
String w = normNum(width);
if (t == null || w == null) {
return null;
}
return normGrade(grade) + "|" + t + "×" + w;
}
private String normGrade(String g) {
return g == null ? "" : g.trim().toUpperCase();
}
/** 数字归一:去尾零,无法解析(区间文本等)返回 null 不参与匹配 */
private String normNum(String s) {
if (s == null) {
return null;
}
try {
return new BigDecimal(s.trim()).stripTrailingZeros().toPlainString();
} catch (Exception e) {
return null;
}
/** 卷号归一去空白转大写便于匹配DB 校对集已大小写不敏感,这里再兜底 */
private String normCoil(String s) {
return s == null ? "" : s.trim().toUpperCase();
}
@Override
@@ -545,13 +529,21 @@ public class ErpPurchasePlanServiceImpl implements IErpPurchasePlanService {
return map;
}
/** 进度百分比(0-100保留2位) */
private BigDecimal calcProgress(BigDecimal arrived, BigDecimal planWeight) {
if (planWeight == null || planWeight.compareTo(BigDecimal.ZERO) <= 0) {
/** 同时填充按重量、按卷数两个到货百分比 */
private void fillProgress(ErpPurchasePlanVo v) {
v.setProgress(calcPct(v.getArrivedWeight(), v.getPlanWeight()));
v.setProgressQty(calcPct(
v.getArrivedCount() == null ? null : BigDecimal.valueOf(v.getArrivedCount()),
v.getPlanQty() == null ? null : BigDecimal.valueOf(v.getPlanQty())));
}
/** 百分比(0-100保留2位) = num/den封顶100 */
private BigDecimal calcPct(BigDecimal num, BigDecimal den) {
if (den == null || den.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
BigDecimal a = arrived == null ? BigDecimal.ZERO : arrived;
BigDecimal pct = a.multiply(BigDecimal.valueOf(100)).divide(planWeight, 2, RoundingMode.HALF_UP);
BigDecimal n = num == null ? BigDecimal.ZERO : num;
BigDecimal pct = n.multiply(BigDecimal.valueOf(100)).divide(den, 2, RoundingMode.HALF_UP);
return pct.min(BigDecimal.valueOf(100));
}

View File

@@ -0,0 +1,118 @@
package com.klp.erp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.klp.erp.domain.ErpSupplierTransaction;
import com.klp.erp.domain.bo.ErpSupplierTransactionBo;
import com.klp.erp.domain.vo.ErpSupplierTransactionVo;
import com.klp.erp.mapper.ErpSupplierTransactionMapper;
import com.klp.erp.service.IErpSupplierTransactionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 供应商往来流水Service业务层处理
*
* @author klp
*/
@RequiredArgsConstructor
@Service
public class ErpSupplierTransactionServiceImpl implements IErpSupplierTransactionService {
/** 类型常量 */
private static final String TYPE_PAYABLE = "1"; // 采购应付
private static final String TYPE_PAY = "2"; // 付款
private static final String TYPE_RETURN = "3"; // 退货
private static final String TYPE_OTHER = "4"; // 其他
private final ErpSupplierTransactionMapper baseMapper;
@Override
public List<ErpSupplierTransactionVo> queryBySupplier(Long supplierId) {
if (supplierId == null) {
return new ArrayList<>();
}
// 按发生时间升序计算逐笔累计余额,再倒序返回(最新在前)
List<ErpSupplierTransactionVo> asc = baseMapper.selectVoList(Wrappers.lambdaQuery(ErpSupplierTransaction.class)
.eq(ErpSupplierTransaction::getSupplierId, supplierId)
.orderByAsc(ErpSupplierTransaction::getTxnDate)
.orderByAsc(ErpSupplierTransaction::getTxnId));
BigDecimal running = BigDecimal.ZERO;
for (ErpSupplierTransactionVo v : asc) {
running = running.add(signedAmount(v.getTxnType(), v.getAmount()));
v.setBalance(running);
}
Collections.reverse(asc);
return asc;
}
@Override
public Map<String, Object> summary(Long supplierId) {
Map<String, Object> map = new HashMap<>(8);
BigDecimal payable = BigDecimal.ZERO;
BigDecimal paid = BigDecimal.ZERO;
BigDecimal returned = BigDecimal.ZERO;
BigDecimal other = BigDecimal.ZERO;
BigDecimal balance = BigDecimal.ZERO;
if (supplierId != null) {
List<ErpSupplierTransactionVo> list = baseMapper.selectVoList(Wrappers.lambdaQuery(ErpSupplierTransaction.class)
.eq(ErpSupplierTransaction::getSupplierId, supplierId));
for (ErpSupplierTransactionVo v : list) {
BigDecimal amt = v.getAmount() == null ? BigDecimal.ZERO : v.getAmount();
if (TYPE_PAYABLE.equals(v.getTxnType())) {
payable = payable.add(amt);
} else if (TYPE_PAY.equals(v.getTxnType())) {
paid = paid.add(amt);
} else if (TYPE_RETURN.equals(v.getTxnType())) {
returned = returned.add(amt);
} else {
other = other.add(amt);
}
balance = balance.add(signedAmount(v.getTxnType(), amt));
}
}
map.put("payable", payable);
map.put("paid", paid);
map.put("returned", returned);
map.put("other", other);
map.put("balance", balance);
return map;
}
/** 付款/退货为负方向,其余为正方向 */
private BigDecimal signedAmount(String type, BigDecimal amount) {
BigDecimal amt = amount == null ? BigDecimal.ZERO : amount;
if (TYPE_PAY.equals(type) || TYPE_RETURN.equals(type)) {
return amt.negate();
}
return amt;
}
@Override
public Boolean insertByBo(ErpSupplierTransactionBo bo) {
ErpSupplierTransaction add = BeanUtil.toBean(bo, ErpSupplierTransaction.class);
return baseMapper.insert(add) > 0;
}
@Override
public Boolean updateByBo(ErpSupplierTransactionBo bo) {
ErpSupplierTransaction update = BeanUtil.toBean(bo, ErpSupplierTransaction.class);
return baseMapper.updateById(update) > 0;
}
@Override
public Boolean deleteByIds(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return false;
}
return baseMapper.deleteBatchIds(ids) > 0;
}
}

View File

@@ -66,6 +66,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ORDER BY p.plan_id DESC
</select>
<!-- WMS 钢卷表中已存在(非占位 data_type<>10)的厂家卷号,用于到货卷号校验 -->
<select id="selectExistingSupplierCoilNos" resultType="java.lang.String">
SELECT DISTINCT supplier_coil_no
FROM wms_material_coil
WHERE del_flag = 0 AND data_type &lt;&gt; 10
AND supplier_coil_no IN
<foreach collection="coilNos" item="c" open="(" separator="," close=")">
#{c}
</foreach>
</select>
<!-- 按合同关键字查关联的采购计划ID订单编号/合同号/合同名称) -->
<select id="selectPlanIdsByContractKeyword" resultType="java.lang.Long">
SELECT DISTINCT r.plan_id