终于到了目前我打算编写的最后一个Entity
类了,也就是一个会计凭证类。老样子,先来设计一下。
Journal
类
这里设计分为两部分,一部分是关系映射,一部分是基础字段
关系映射
这里最先要解决的问题,就是Journal
究竟要挂在什么东西下边,这里我想了一下,首先毫无疑问的,Journal
应该外键关联到合同,由于合同和预算是多对多的关系,但Journal
不行,一条只能归属于一个预算,所以在新增的时候,需要将这个合同对应的预算都列出来供选择。只要对应了预算,也就等于对应了项目。这里我打算用一点稍微非规范化的设计,就是再添加一个int的字段,手动保存上对应的项目即可,那么一条Journal
记录实际上同时对应某个合同,某个预算和项目。
至于修改页面,我暂时不打算让用户去修改这三个对应关系,仅仅只能修改数字,不像合同那么灵活。如果输入错误,就删除了再来吧,这里就做的死板一些,不要太灵活。
根据上边的设计,类的关系映射设计如下:
@Entity
@Table(name = "journal")
public class Journal implements Serializable {
private static final long serialVersionUID = 11L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
// 关联到合同的外键
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "contract_id", nullable = false)
private Contract contract;
// 关联到预算的外键
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "budget_id", nullable = false)
private Budget budget;
// 手工设置上项目的id
private int project_id;
}
这里最后一个非规范化的project_id
,将来可以快速的找到对应的项目。
还需要在预算和合同类中设置上相应的关联映射,这里代码就省略了。
普通字段映射
这里我基本上就参考原来Django
中的编写,把字段都设置好,从目前的应用来看,原来的设计足够,还是突出那些关键的科目字段。
@NotEmpty(message = "说明")
@Length(max = 255, message = "长度不能超过255个字符")
@Column(name = "description", nullable = false)
private String description;
// 凭证日期
@Temporal(TemporalType.DATE)
@Column(name = "start_time")
@DateTimeFormat(pattern = "yyyy-MM-dd", iso = DateTimeFormat.ISO.DATE)
private Date noteDate;
// 凭证号
@Digits(integer = 4, fraction = 0,message = "必须为0-9999的整数")
private int noteNumber;
//会计字段部分
// 损益-收入
@NotNull(message = "收入不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal revenuePL = new BigDecimal("0.00");
// 损益-成本
@NotNull(message = "成本不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal costPL = new BigDecimal("0.00");
// 损益-其他损益
@NotNull(message = "成本不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal otherPL = new BigDecimal("0.00");
// 资产-货币资金
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal cash = new BigDecimal("0.00");
// 资产-合同履约成本
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal contractCost = new BigDecimal("0.00");
// 资产-合同资产
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal contractAsset = new BigDecimal("0.00");
// 资产-合同取得成本
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal contractAcquireCost = new BigDecimal("0.00");
// 资产-应收账款
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal accountReceivable = new BigDecimal("0.00");
// 资产-预付账款
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal prepaid = new BigDecimal("0.00");
// 资产-进项税金
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal inputVAT = new BigDecimal("0.00");
// 资产-其他资产
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal otherAsset = new BigDecimal("0.00");
// 负债-合同负债
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal contractLiability = new BigDecimal("0.00");
// 负债-应付账款
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal accountPayable = new BigDecimal("0.00");
// 负债-销项税金
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal outputVAT = new BigDecimal("0.00");
// 负债-其他负债
@NotNull(message = "不能为空")
@Digits(integer = 14, fraction = 2)
private BigDecimal otherLiability = new BigDecimal("0.00");
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "create_time", updatable = false)
@CreationTimestamp
private Date createTime;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "update_time")
@UpdateTimestamp
protected Date updateTime;
编写自检方法
我们要根据会计的借贷是否相等,来判断这个类的数据是不是有效,所以需要编写几个方法:
public BigDecimal totalDebitSide() {
return costPL
.add(otherPL)
.add(cash)
.add(contractCost)
.add(contractAsset)
.add(contractAcquireCost)
.add(accountReceivable)
.add(prepaid)
.add(inputVAT)
.add(otherAsset);
}
public BigDecimal totalCreditSide() {
return revenuePL
.add(contractLiability)
.add(accountPayable)
.add(outputVAT)
.add(otherLiability);
}
public boolean isValid() {
return totalDebitSide().equals(totalCreditSide());
}
之后把DAO
和Service
类也写了,这里就不放具体代码了。
URL
设计
例行的还是要来设计一下URL
,由于这个类的上一级就是Contract
,所以URL
也很好设计,如下:
pms/journal/contract/{cid}/list
,某个合同下的所有明细记录pms/journal/contract/{cid}/add
,添加某个合同下的明细记录pms/journal/detail/{id}
,明细记录的详情页pms/journal/{cid}/edit/{id}
,修改明星记录的页面pms/journal/delete
,删除收款结算记录
新增Journal
类
这里就是要编写一个表单,用来新增Journal
类,这里需要获取合同对应的预算,让用户只能选择其中的一个预算项目。
先来编写表单:
<main class="container">
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb" class="mt-3">
<ol class="breadcrumb">
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list" th:href="@{/pms/project/list}"
class="link-secondary">项目管理</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list"
th:href="${project.absoluteUrl()}"
class="link-secondary">[[${project.projectName}]]</a>
</li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list"
th:href="${contract.absoluteUrl()}"
class="link-secondary">[[${contract.name}]]</a>
</li>
<li class="breadcrumb-item" aria-current="page"><a href="/pms/project/list"
th:href="${contract.addJournalUrl()}"
class="link-secondary">新增明细记录</a>
</li>
</ol>
</nav>
<div class="py-1 text-center">
<h2>新增明细记录</h2>
</div>
<div class="row py-1">
<div class="col-sm-4"></div>
<div th:if="${numberError}" class="text-center alert alert-danger col-12 col-sm-4" role="alert">
[[${numberError}]]
</div>
<div class="col-sm-4"></div>
</div>
<div class="row g-5">
<div class="col-2"></div>
<div class="col-md-8 col-lg-8">
<p class="text-secondary">不需填写的数值请保留0.00字样,不要删除</p>
<form class="needs-validation" action="/pms/journal/add" th:action="${contract.addJournalUrl()}" method="post"
th:object="${journal}">
<div class="row g-3 mb-3">
<h4 class="mb-1">基础信息</h4>
<div class="col-sm-6">
<p class="mb-0">所属预算条目</p>
<select class="form-select mt-2" aria-label="budget" name="budget" th:field="*{budget}">
<option th:each="budget, control:${budgets}" th:value="${budget.getId()}" >
[[${budget.budgetName}]]
</option>
</select>
</div>
<div class="col-sm-6">
<label for="description" class="form-label">说明</label>
<input type="text" class="form-control" id="description" th:field="*{description}"
th:classappend="${#fields.hasErrors('description')}? 'is-invalid'" required>
<div id="descriptionError" th:if="${#fields.hasErrors('description')}"
th:errors="*{description}"
class="invalid-feedback"></div>
</div>
<div class="col-6">
<label for="noteDate" class="form-label">凭证日期</label>
<input type="date" class="form-control" id="noteDate" th:field="*{noteDate}"
th:classappend="${#fields.hasErrors('noteDate')}? 'is-invalid'" required>
<div id="noteDateError" th:if="${#fields.hasErrors('noteDate')}"
th:errors="*{noteDate}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="noteNumber" class="form-label">凭证号</label>
<input type="number" step="1" class="form-control" id="noteNumber" th:field="*{noteNumber}"
th:classappend="${#fields.hasErrors('noteNumber')}? 'is-invalid'" required>
<div id="noteNumberError" th:if="${#fields.hasErrors('noteNumber')}"
th:errors="*{noteNumber}"
class="invalid-feedback"></div>
</div>
<hr>
<h4 class="mb-1">损益</h4>
<div class="col-sm-6">
<label for="revenuePL" class="form-label">营业收入</label>
<input type="number" step="0.01" class="form-control" id="revenuePL" th:field="*{revenuePL}"
th:classappend="${#fields.hasErrors('revenuePL')}? 'is-invalid'" required>
<div id="revenuePLError" th:if="${#fields.hasErrors('revenuePL')}"
th:errors="*{revenuePL}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="costPL" class="form-label">营业成本</label>
<input type="number" step="0.01" class="form-control" id="costPL" th:field="*{costPL}"
th:classappend="${#fields.hasErrors('costPL')}? 'is-invalid'" required>
<div id="costPLError" th:if="${#fields.hasErrors('costPL')}"
th:errors="*{costPL}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="otherPL" class="form-label">其他损益</label>
<input type="number" step="0.01" class="form-control" id="otherPL" th:field="*{otherPL}"
th:classappend="${#fields.hasErrors('otherPL')}? 'is-invalid'" required>
<div id="otherPLError" th:if="${#fields.hasErrors('otherPL')}"
th:errors="*{otherPL}"
class="invalid-feedback"></div>
</div>
<hr>
<h4 class="mb-1">资产</h4>
<div class="col-sm-6">
<label for="cash" class="form-label">货币资金</label>
<input type="number" step="0.01" class="form-control" id="cash" th:field="*{cash}"
th:classappend="${#fields.hasErrors('cash')}? 'is-invalid'" required>
<div id="cashError" th:if="${#fields.hasErrors('cash')}"
th:errors="*{cash}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="contractAsset" class="form-label">合同资产</label>
<input type="number" step="0.01" class="form-control" id="contractAsset" th:field="*{contractAsset}"
th:classappend="${#fields.hasErrors('contractAsset')}? 'is-invalid'" required>
<div id="contractAssetError" th:if="${#fields.hasErrors('contractAsset')}"
th:errors="*{contractAsset}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="contractCost" class="form-label">合同履约成本</label>
<input type="number" step="0.01" class="form-control" id="contractCost" th:field="*{contractCost}"
th:classappend="${#fields.hasErrors('contractCost')}? 'is-invalid'" required>
<div id="contractCostError" th:if="${#fields.hasErrors('contractCost')}"
th:errors="*{contractCost}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="contractAcquireCost" class="form-label">合同取得成本</label>
<input type="number" step="0.01" class="form-control" id="contractAcquireCost" th:field="*{contractAcquireCost}"
th:classappend="${#fields.hasErrors('contractAcquireCost')}? 'is-invalid'" required>
<div id="contractAcquireCostError" th:if="${#fields.hasErrors('contractAcquireCost')}"
th:errors="*{contractAcquireCost}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="accountReceivable" class="form-label">应收账款</label>
<input type="number" step="0.01" class="form-control" id="accountReceivable" th:field="*{accountReceivable}"
th:classappend="${#fields.hasErrors('accountReceivable')}? 'is-invalid'" required>
<div id="accountReceivableError" th:if="${#fields.hasErrors('accountReceivable')}"
th:errors="*{accountReceivable}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="prepaid" class="form-label">预付账款</label>
<input type="number" step="0.01" class="form-control" id="prepaid" th:field="*{prepaid}"
th:classappend="${#fields.hasErrors('prepaid')}? 'is-invalid'" required>
<div id="prepaidError" th:if="${#fields.hasErrors('prepaid')}"
th:errors="*{prepaid}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="inputVAT" class="form-label">进项税金</label>
<input type="number" step="0.01" class="form-control" id="inputVAT" th:field="*{inputVAT}"
th:classappend="${#fields.hasErrors('inputVAT')}? 'is-invalid'" required>
<div id="inputVATError" th:if="${#fields.hasErrors('inputVAT')}"
th:errors="*{inputVAT}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="otherAsset" class="form-label">其他资产</label>
<input type="number" step="0.01" class="form-control" id="otherAsset" th:field="*{otherAsset}"
th:classappend="${#fields.hasErrors('otherAsset')}? 'is-invalid'" required>
<div id="otherAssetError" th:if="${#fields.hasErrors('otherAsset')}"
th:errors="*{otherAsset}"
class="invalid-feedback"></div>
</div>
<hr>
<h4 class="mb-1">负债</h4>
<div class="col-sm-6">
<label for="contractLiability" class="form-label">合同负债</label>
<input type="number" step="0.01" class="form-control" id="contractLiability" th:field="*{contractLiability}"
th:classappend="${#fields.hasErrors('contractLiability')}? 'is-invalid'" required>
<div id="contractLiabilityError" th:if="${#fields.hasErrors('contractLiability')}"
th:errors="*{contractLiability}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="accountPayable" class="form-label">应付账款</label>
<input type="number" step="0.01" class="form-control" id="accountPayable" th:field="*{accountPayable}"
th:classappend="${#fields.hasErrors('accountPayable')}? 'is-invalid'" required>
<div id="accountPayableError" th:if="${#fields.hasErrors('accountPayable')}"
th:errors="*{accountPayable}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="outputVAT" class="form-label">销项税金</label>
<input type="number" step="0.01" class="form-control" id="outputVAT" th:field="*{outputVAT}"
th:classappend="${#fields.hasErrors('outputVAT')}? 'is-invalid'" required>
<div id="outputVATError" th:if="${#fields.hasErrors('outputVAT')}"
th:errors="*{outputVAT}"
class="invalid-feedback"></div>
</div>
<div class="col-sm-6">
<label for="otherLiability" class="form-label">其他负债</label>
<input type="number" step="0.01" class="form-control" id="otherLiability" th:field="*{otherLiability}"
th:classappend="${#fields.hasErrors('otherLiability')}? 'is-invalid'" required>
<div id="otherLiabilityError" th:if="${#fields.hasErrors('otherLiability')}"
th:errors="*{otherLiability}"
class="invalid-feedback"></div>
</div>
</div>
<div class="text-center mb-4">
<button class="btn btn-primary btn-lg" type="submit"><i class="fas fa-check"></i> 提交</button>
<a class="btn btn-secondary btn-lg" th:href="${contract.journalListUrl()}" href="/pms/project/list"><i class="fas fa-undo-alt"></i> 返回</a>
</div>
</form>
</div>
<div class="col-2"></div>
</div>
</main>
这里要给Contract
类添加几个生成URL
的方法:
public String addJournalUrl() {
return "/pms/journal/contract/" + id + "/add";
}
public String journalListUrl() {
return "/pms/journal/contract/" + id + "/list";
}
之后是控制器,也是之前编写过的套路,要注意手动设置上关联的合同和项目的id
:
// 新增Journal页面
@GetMapping("/contract/{cid}/add")
public String addJournalPage(@PathVariable("cid") int cid,
@ModelAttribute("journal") Journal journal, Model model) {
Contract contract = contractService.getContractById(cid);
Set<Budget> budgets = contract.getBudgets();
Budget budget = (Budget) (budgets.toArray()[0]);
Project project = budget.getProject();
model.addAttribute("project", project);
model.addAttribute("contract", contract);
model.addAttribute("budgets", budgets);
return "pms/journal/journalAdd";
}
// 接受修改
@PostMapping("/contract/{cid}/add")
public String addJournal(@Valid @ModelAttribute("journal") Journal journal, BindingResult rs,
Model model,
@PathVariable("cid") int cid,
RedirectAttributes attributes) {
Contract contract = contractService.getContractById(cid);
Set<Budget> budgets = contract.getBudgets();
Budget budget = (Budget) (budgets.toArray()[0]);
Project project = budget.getProject();
model.addAttribute("project", project);
model.addAttribute("contract", contract);
model.addAttribute("budgets", budgets);
if (rs.hasErrors()) {
return "pms/journal/journalAdd";
}
System.out.println(journal);
if (!journal.isValid()) {
model.addAttribute("numberError", "借贷方不平,请检查数字。");
return "pms/journal/journalAdd";
}
journal.setContract(contract);
journal.setProject_id(project.getId());
journalService.save(journal);
attributes.addFlashAttribute("created", "成功新增明细记录");
return "redirect:" + contract.journalListUrl();
}
这样就编写好了新增功能,剩下的功能下一篇文章中编写。