feat(contract): 新增合同发票管理功能

实现合同发票的增删改查功能,包括:
1. 添加ContractInvoiceVo实体类及相关ViewModel
2. 创建合同发票数据库表CONTRACT_INVOICE
3. 实现前后端发票管理服务ContractInvoiceService
4. 开发发票管理界面及标签页
5. 添加发票表格单元格组件
6. 完善销售订单表结构,增加客户联系人字段
7. 更新pom.xml版本至0.0.122-SNAPSHOT

修复销售订单界面搜索字段命名不一致问题
This commit is contained in:
2025-10-16 15:47:33 +08:00
parent cf0a7e18ea
commit 71a358fa77
28 changed files with 1274 additions and 40 deletions

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>com.ecep.contract</groupId>
<artifactId>Contract-Manager</artifactId>
<version>0.0.102-SNAPSHOT</version>
<version>0.0.122-SNAPSHOT</version>
</parent>
<groupId>com.ecep.contract</groupId>
<artifactId>server</artifactId>
<version>0.0.102-SNAPSHOT</version>
<version>0.0.122-SNAPSHOT</version>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -22,7 +22,7 @@
<dependency>
<groupId>com.ecep.contract</groupId>
<artifactId>common</artifactId>
<version>0.0.102-SNAPSHOT</version>
<version>0.0.122-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -0,0 +1,139 @@
package com.ecep.contract.ds.contract.model;
import java.time.LocalDate;
import java.util.Objects;
import com.ecep.contract.ds.other.model.Invoice;
import com.ecep.contract.model.Employee;
import com.ecep.contract.model.IdentityEntity;
import com.ecep.contract.model.NamedEntity;
import com.ecep.contract.model.Voable;
import com.ecep.contract.vo.ContractInvoiceVo;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 合同发票实体类
*/
@Getter
@Setter
@Entity
@Table(name = "CONTRACT_INVOICE", schema = "supplier_ms")
@ToString
public class ContractInvoice implements IdentityEntity, NamedEntity, Voable<ContractInvoiceVo> {
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "CODE", length = 50)
private String code;
@Column(name = "NAME", length = 200)
private String name;
/**
* 关联的合同对象
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CONTRACT_ID", foreignKey = @jakarta.persistence.ForeignKey(name = "FK_CONTRACT_INVOICE_CONTRACT"))
@OnDelete(action = OnDeleteAction.CASCADE)
@ToString.Exclude
private Contract contract;
/**
* 关联的发票对象
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "INVOICE_ID", foreignKey = @jakarta.persistence.ForeignKey(name = "FK_CONTRACT_INVOICE_INVOICE"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Invoice invoice;
/**
* 发票金额
*/
@Column(name = "AMOUNT")
private Double amount;
/**
* 创建人
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "SETUP_PERSON_ID", foreignKey = @jakarta.persistence.ForeignKey(name = "FK_CONTRACT_INVOICE_SETUP_PERSON"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee setupPerson;
/**
* 提交日期
*/
@Column(name = "SETUP_DATE")
private LocalDate setupDate;
/**
* 更新人
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "UPDATE_PERSON_ID", foreignKey = @jakarta.persistence.ForeignKey(name = "FK_CONTRACT_INVOICE_UPDATE_PERSON"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee updatePerson;
/**
* 更新日期
*/
@Column(name = "UPDATE_DATE")
private LocalDate updateDate;
/**
* 备注
*/
@Column(name = "REMARK", length = 500)
private String remark;
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ContractInvoice that = (ContractInvoice) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public ContractInvoiceVo toVo() {
ContractInvoiceVo vo = new ContractInvoiceVo();
vo.setId(id);
vo.setCode(code);
vo.setName(name);
vo.setContractId(contract != null ? contract.getId() : null);
vo.setInvoiceId(invoice != null ? invoice.getId() : null);
vo.setAmount(amount);
vo.setRemark(remark);
vo.setSetupPersonId(setupPerson != null ? setupPerson.getId() : null);
vo.setUpdatePersonId(updatePerson != null ? updatePerson.getId() : null);
vo.setSetupDate(setupDate);
vo.setUpdateDate(updateDate);
return vo;
}
}

View File

@@ -0,0 +1,35 @@
package com.ecep.contract.ds.contract.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import com.ecep.contract.ds.MyRepository;
import com.ecep.contract.ds.contract.model.ContractInvoice;
@Repository
public interface ContractInvoiceRepository extends MyRepository<ContractInvoice, Integer> {
/**
* 根据GUID查找发票的方法
*
* @param code 发票代码
* @return 返回一个Optional<ContractInvoice>对象,可能包含找到的发票,也可能为空
*/
Optional<ContractInvoice> findByCode(String code);
/**
* 根据合同ID查找发票列表
*
* @param contractId 合同ID
* @return 发票列表
*/
List<ContractInvoice> findByContractId(Integer contractId);
/**
* 根据发票ID查找合同发票
*
* @param invoiceId 发票ID
* @return 合同发票实体
*/
Optional<ContractInvoice> findByInvoiceId(Integer invoiceId);
}

View File

@@ -0,0 +1,200 @@
package com.ecep.contract.ds.contract.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import com.ecep.contract.IEntityService;
import com.ecep.contract.QueryService;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.company.service.InvoiceService;
import com.ecep.contract.ds.contract.model.ContractInvoice;
import com.ecep.contract.ds.contract.repository.ContractInvoiceRepository;
import com.ecep.contract.ds.other.service.EmployeeService;
import com.ecep.contract.service.VoableService;
import com.ecep.contract.util.SpecificationUtils;
import com.ecep.contract.vo.ContractInvoiceVo;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@CacheConfig(cacheNames = "contract-invoice")
public class ContractInvoiceService implements IEntityService<ContractInvoice>, QueryService<ContractInvoiceVo>,
VoableService<ContractInvoice, ContractInvoiceVo> {
@Autowired
private ContractInvoiceRepository repository;
@Cacheable(key = "#p0")
public ContractInvoiceVo findById(Integer id) {
return repository.findById(id).map(ContractInvoice::toVo).orElse(null);
}
@Cacheable(key = "'code-'+#p0")
public ContractInvoiceVo findByCode(String code) {
return repository.findByCode(code).map(ContractInvoice::toVo).orElse(null);
}
/**
* 根据合同ID查找发票列表
*
* @param contractId 合同ID
* @return 发票VO列表
*/
public List<ContractInvoiceVo> findByContractId(Integer contractId) {
return repository.findByContractId(contractId)
.stream()
.map(ContractInvoice::toVo)
.toList();
}
/**
* 根据发票ID查找合同发票
*
* @param invoiceId 发票ID
* @return 合同发票VO
*/
public ContractInvoiceVo findByInvoiceId(Integer invoiceId) {
return repository.findByInvoiceId(invoiceId)
.map(ContractInvoice::toVo)
.orElse(null);
}
/**
* 保存发票
*
* @param invoice 发票实体
* @return 保存后的发票VO
*/
@Override
public ContractInvoice save(ContractInvoice invoice) {
try {
ContractInvoice saved = repository.save(invoice);
return saved;
} catch (Exception e) {
log.error("保存发票失败", e);
throw new RuntimeException("保存发票失败", e);
}
}
@Override
public void updateByVo(ContractInvoice entity, ContractInvoiceVo vo) {
if (entity == null || vo == null) {
return;
}
entity.setCode(vo.getCode());
entity.setName(vo.getName());
entity.setAmount(vo.getAmount());
entity.setRemark(vo.getRemark());
// 处理关联实体 - 合同
if (vo.getContractId() == null) {
entity.setContract(null);
} else {
ContractService contractService = SpringApp.getBean(ContractService.class);
if (entity.getContract() == null || !entity.getContract().getId().equals(vo.getContractId())) {
entity.setContract(contractService.getById(vo.getContractId()));
}
}
// 处理关联实体 - 发票
if (vo.getInvoiceId() == null) {
entity.setInvoice(null);
} else {
InvoiceService invoiceService = SpringApp.getBean(InvoiceService.class);
if (entity.getInvoice() == null || !entity.getInvoice().getId().equals(vo.getInvoiceId())) {
entity.setInvoice(invoiceService.getById(vo.getInvoiceId()));
}
}
// 处理关联实体 - 发票
EmployeeService personService = SpringApp.getBean(EmployeeService.class);
if (vo.getSetupPersonId() == null) {
entity.setSetupPerson(null);
} else {
if (entity.getSetupPerson() == null || !entity.getSetupPerson().getId().equals(vo.getSetupPersonId())) {
entity.setSetupPerson(personService.getById(vo.getSetupPersonId()));
}
}
if (vo.getUpdatePersonId() == null) {
entity.setUpdatePerson(null);
} else {
if (entity.getUpdatePerson() == null || !entity.getUpdatePerson().getId().equals(vo.getUpdatePersonId())) {
entity.setUpdatePerson(personService.getById(vo.getUpdatePersonId()));
}
}
}
@Override
public Page<ContractInvoiceVo> findAll(JsonNode paramsNode, Pageable pageable) {
Specification<ContractInvoice> spec = buildParameterSpecification(paramsNode);
Page<ContractInvoice> page = repository.findAll(spec, pageable);
return page.map(ContractInvoice::toVo);
}
/**
* 构建参数规范
*
* @param paramsNode 参数节点
* @return 规范对象
*/
protected Specification<ContractInvoice> buildParameterSpecification(JsonNode paramsNode) {
Specification<ContractInvoice> spec = null;
// 处理基本字段参数
spec = SpecificationUtils.andFieldEqualParam(spec, paramsNode, "name", "code");
// 处理关联实体参数
spec = SpecificationUtils.andParam(spec, paramsNode, "contract", "invoice", "setupPerson", "updatePerson");
// 处理搜索文本
if (paramsNode.has("searchText")) {
String searchText = paramsNode.get("searchText").asText();
if (searchText != null && !searchText.isEmpty()) {
spec = SpecificationUtils.and(spec, getSpecification(searchText));
}
}
return spec;
}
@Override
public ContractInvoice getById(Integer id) {
return repository.findById(id).orElse(null);
}
@Override
public Page<ContractInvoice> findAll(Specification<ContractInvoice> spec, Pageable pageable) {
return repository.findAll(spec, pageable);
}
@Override
public Specification<ContractInvoice> getSpecification(String searchText) {
return (root, query, builder) -> {
return builder.or(
builder.like(root.get("name"), "%" + searchText + "%"),
builder.like(root.get("code"), "%" + searchText + "%"),
builder.like(root.get("remark"), "%" + searchText + "%"));
};
}
@Override
public void delete(ContractInvoice entity) {
try {
repository.delete(entity);
} catch (Exception e) {
log.error("删除合同发票失败", e);
throw new RuntimeException("删除合同发票失败", e);
}
}
}

View File

@@ -3,6 +3,9 @@ package com.ecep.contract.ds.customer.model;
import java.time.LocalDate;
import java.util.Objects;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import com.ecep.contract.ds.company.model.Company;
import com.ecep.contract.ds.contract.model.Contract;
import com.ecep.contract.ds.contract.model.ContractBasedEntity;
@@ -22,6 +25,7 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.ForeignKey;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@@ -42,7 +46,8 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CONTRACT_ID")
@JoinColumn(name = "CONTRACT_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_CONTRACT"))
@OnDelete(action = OnDeleteAction.CASCADE)
@ToString.Exclude
private Contract contract;
@@ -50,7 +55,8 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
* 业务员
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "EMPLOYEE_ID")
@JoinColumn(name = "EMPLOYEE_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_EMPLOYEE"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee employee;
@@ -73,10 +79,20 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
* 客户
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
@JoinColumn(name = "CUSTOMER_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_CUSTOMER"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Company customer;
/**
* 客户联系人
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_PERSON_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_CUSTOMER_PERSON"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee customerPerson;
/**
* 客户地址
*/
@@ -87,7 +103,8 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
* 制单人
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MAKER_ID")
@JoinColumn(name = "MAKER_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_MAKER"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee maker;
@@ -100,7 +117,8 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
* 审核人
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "VERIFIER_ID")
@JoinColumn(name = "VERIFIER_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_VERIFIER"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee verifier;
/**
@@ -109,8 +127,12 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
@Column(name = "VERIFIED_DATE")
private LocalDate verifierDate;
/**
* 修改人
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MODIFIER_ID")
@JoinColumn(name = "MODIFIER_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_MODIFIER"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee modifier;
@@ -121,7 +143,8 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
* 关闭人
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CLOSER_ID")
@JoinColumn(name = "CLOSER_ID", foreignKey = @ForeignKey(name = "FK_SALES_ORDER_CLOSER"))
@OnDelete(action = OnDeleteAction.SET_NULL)
@ToString.Exclude
private Employee closer;
@@ -170,7 +193,7 @@ public class SalesOrder implements IdentityEntity, BasedEntity, ContractBasedEnt
vo.setModifierDate(modifierDate);
vo.setCloserDate(closerDate);
vo.setDescription(description);
// 关联对象ID映射
if (contract != null) {
vo.setContractId(contract.getId());