Fms-Java版开发实录:25 合同 - 改删功能

Fms-Java版开发实录:25 合同 - 改删功能

合同的增删改查也写完了

现在是时候来编写合同的详情页了,目前由于还没有汇总数据,现在只能够来展示合同最基础的信息,其他的执行情况,就要等到后边有了journal类才可以。

合同详情页面

合同详情页面类似于展示项目的详情页面,所以我就基本照抄那个页面。

控制器

由于URLpms/contract/detail/{id},这里我们就来获取合同id,然后把对应的预算和项目都一并传入页面,控制器如下:

    @GetMapping("/detail/{id}")
    public String contractDetailPage(@PathVariable("id") int cid, Model model) {
        Contract contract = contractService.getContractById(cid);
        Set<Budget> budgets = contract.getBudgets();
        Project project = ((Budget) budgets.toArray()[0]).getProject();
        model.addAttribute("contract", contract);
        model.addAttribute("budgets", budgets);
        model.addAttribute("project", project);

        return "pms/contract/contractDetail";
    }

页面比较简单:

<main class="col-lg-8 mx-auto 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>
        </ol>
    </nav>
    <h1 th:text="${contract.name}">合同名称</h1>
    <hr>

    <div class="row g-5">
        <div class="col-md-9">
            <h2>合同基础信息</h2>
            <table class="table table-sm table-striped" th:object="${contract}">
                <thead>
                <tr>
                    <th>属性</th>
                    <th>内容</th>
                </tr>
                </thead>
                <tbody>
                <tr>
                    <td>合同编号</td>
                    <td th:text="*{number}"></td>
                </tr>
                <tr>
                    <td>合同总价</td>
                    <td th:text="${#numbers.formatDecimal(contract.totalPrice,0,'COMMA',2,'POINT')}"></td>
                </tr>
                <tr>
                    <td>合同条款</td>
                    <td th:text="*{terms}"></td>
                </tr>
                <tr>
                    <td>主要合同对手</td>
                    <td th:text="*{supplier}"></td>
                </tr>
                <tr>
                    <td>合同所属预算条目</td>
                    <td >
                        <span class="pe-1" th:each="budget:${budgets}" th:text="${budget.getBudgetName()}"></span>
                    </td>
                </tr>
                <tr>
                    <td>合同印花税类型</td>
                    <td>[[${contract.stamp.getType()}]] - [[${contract.stamp.getRate()}]]</td>
                </tr>
                <tr>
                    <td>不含税价</td>
                    <td th:text="${#numbers.formatDecimal(contract.getPriceWithoutVAT(),0,'COMMA',2,'POINT')}"></td>
                </tr>
                <tr>
                    <td>增值税额</td>
                    <td th:text="${#numbers.formatDecimal(contract.getVAT(),0,'COMMA',2,'POINT')}"></td>
                </tr>
                <tr>
                    <td>结算价格</td>
                    <td th:text="${#numbers.formatDecimal(contract.getFinalPrice(),0,'COMMA',2,'POINT')}"></td>
                </tr>
                <tr>
                    <td>其他合同当事方1</td>
                    <td th:text="*{getSecondarySupplier1()}"></td>
                </tr>
                <tr>
                    <td>其他合同当事方2</td>
                    <td th:text="*{getSecondarySupplier2()}"></td>
                </tr>
                <tr>
                    <td>其他合同当事方3</td>
                    <td th:text="*{getSecondarySupplier3()}"></td>
                </tr>
                <tr>
                    <td>签订时间</td>
                    <td th:text="*{signDate}"></td>
                </tr>
                <tr>
                    <td>终止时间</td>
                    <td th:text="*{endDate}"></td>
                </tr>
                <tr>
                    <td>合同标的</td>
                    <td th:text="*{target}"></td>
                </tr>
                <tr>
                    <td>联系人</td>
                    <td th:text="*{contact}"></td>
                </tr>
                <tr>
                    <td>联系电话</td>
                    <td th:text="*{phone}"></td>
                </tr>
                </tbody>
            </table>
        </div>

        <div class="col-md-3">
            <h2>功能</h2>
            <ul class="list-group list-group-flush">
                <li class="list-group-item"><a href="/pms/project/detail" th:href="${project.editUrl()}"><i
                        class="fas fa-pencil-alt"></i> 编辑合同信息</a></li>
                <li class="list-group-item"><a href="/pms/contract/project/add" th:href="${project.addContractUrl()}"><i class="fas fa-file-import"></i> 新增合同</a></li>
                <li class="list-group-item"><a href="/pms/budget/list" th:href="${project.budgetUrl()}" class="link-success"><i class="fas fa-chart-line"></i> 项目预算详情</a>
                </li>
                <li class="list-group-item"><a th:href="${project.contractListUrl()}" class="link-success"><i class="fas fa-list"></i> 项目合同列表</a></li>
                <li class="list-group-item"><a href="" class="link-danger" data-bs-toggle="modal"
                                               data-bs-target="#deleteModal"><i class="fas fa-trash-alt"></i> &nbsp;删除项目</a>
                </li>
            </ul>
        </div>
    </div>
</main>

编辑合同

这个几乎就是新增合同的翻版,写起来也很快,只要记得写完之后将合同的编辑链接添加到详情页上,这里要给Project再添加一个生成URL的方法:

    public String editContractByProject() {
        return "/pms/contract/" + id + "/edit";
    }

控制器与页面

这个几乎就是复制新添加合同的控制器,编写如下:

    @GetMapping("/{pid}/edit/{id}")
    public String editContractPage(@PathVariable("pid") int pid, @PathVariable("id") int cid, Model model) {
        Project project = projectService.findById(pid);
        List<Budget> budgets = budgetService.getBudgetByProjectId(pid);
        Contract contract = contractService.getContractById(cid);
        model.addAttribute("project", project);
        model.addAttribute("budgets", budgets);
        model.addAttribute("contract", contract);
        model.addAttribute("stamps", stampService.getAllStamps());
        return "pms/contract/contractEdit";
    }

在编写页面的时候,发现之前犯了个错误。即:

<div class="col-sm-6">
    <div class="form-check form-switch">
        <input class="form-check-input" type="checkbox" id="isOpen" name="isOpen"
               th:checked="*{isOpen}">
        <label class="form-check-label" for="isOpen">单价合同或开口合同</label>
    </div>
</div>

这其中,不勾选的时候,这个数据不会发送到服务器,所以必须要单独来获取isOpen的属性,接受POST请求的控制器如下:

    // 接受编辑修改
    @PostMapping("/{pid}/edit/{id}")
    public String editContract(@ModelAttribute("contract") Contract contract, BindingResult rs,
                               @PathVariable("pid") int pid, @PathVariable("id") int cid, Model model,
                               RedirectAttributes attributes, HttpServletRequest request) {
        Project project = projectService.findById(pid);
        List<Budget> budgets = budgetService.getBudgetByProjectId(pid);
        model.addAttribute("project", project);
        model.addAttribute("budgets", budgets);
        model.addAttribute("stamps", stampService.getAllStamps());

        if (rs.hasErrors()) {
            return "pms/contract/contractEdit";
        } else {
            // 不再判断合同编号是否重复

            // 没有对应的预算条目
            if (contract.getBudgets() == null || contract.getBudgets().size() == 0) {
                model.addAttribute("numberError", "未选择预算条目");
                return "pms/contract/contractEdit";
            }

            // 如果有一个不为空,另外一个为空,将其设置为0
            if ((contract.getPriceWithoutVAT() == null) && (contract.getVAT() != null)) {
                contract.setPriceWithoutVAT(new BigDecimal("0.00"));
            }
            if ((contract.getPriceWithoutVAT() != null) && (contract.getVAT() == null)) {
                contract.setVAT(new BigDecimal("0.00"));
            }

            // 判断都不为空的情况下,是否相等
            if ((contract.getPriceWithoutVAT() != null) && (contract.getVAT() != null)) {
                if (!contract.getTotalPrice().equals(contract.getPriceWithoutVAT().add(contract.getVAT()))) {
                    model.addAttribute("numberError", "不含税价+增值税 不等于 合同总价");
                    return "pms/contract/contractEdit";
                }
            }
            contract.setId(cid);
            contract.setOpen(request.getParameter("isOpen") != null);
            contractService.saveContract(contract);
            attributes.addFlashAttribute("updated", "成功修改合同 " + contract.getName());
            return "redirect:" + contract.absoluteUrl();
        }


    }

在接受新增合同的控制器那里,也应该传入HttpServletRequest request然后添加一行:

contract.setOpen(request.getParameter("isOpen") != null);

最后是页面,基本上和新增合同的页面差不多:

<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="${project.editContractByProject()+'/'+contract.getId()}"
                                                               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">
            <h4 class="mb-3">必填内容</h4>
            <form class="needs-validation" action="/pms/contract/edit" th:action="${project.editContractByProject()+'/'+contract.getId()}"
                  method="post"
                  th:object="${contract}">
                <div class="row g-3 mb-3">
                    <div class="col-sm-12">
                        <label for="contractName" class="form-label">合同名称</label>
                        <input type="text" class="form-control" id="contractName" th:field="*{name}"
                               th:classappend="${#fields.hasErrors('name')}? 'is-invalid'" required>
                        <div id="addressError" th:if="${#fields.hasErrors('name')}"
                             th:errors="*{name}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="contractNumber" class="form-label">合同编号</label>
                        <input type="text" class="form-control" id="contractNumber"
                               th:field="*{number}"
                               th:classappend="${#fields.hasErrors('number')}? 'is-invalid'" required>
                        <div id="numberError" th:if="${#fields.hasErrors('number')}"
                             th:errors="*{number}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="projectManager" class="form-label">主要合同方</label>
                        <input type="text" class="form-control" id="projectManager" th:field="*{supplier}"
                               th:classappend="${#fields.hasErrors('supplier')}? 'is-invalid'" required>
                        <div id="managerError" th:if="${#fields.hasErrors('supplier')}"
                             th:errors="*{supplier}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-12">
                        <label for="projectDescription" class="form-label">合同主要条款</label>
                        <textarea class="form-control" id="projectDescription" th:field="*{terms}" rows="3"
                                  th:classappend="${#fields.hasErrors('terms')}? 'is-invalid'" required>
                        </textarea>
                        <div id="descriptionError" th:if="${#fields.hasErrors('terms')}"
                             th:errors="*{terms}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <p class="mb-0">合同所属预算条目(可按Ctrl键多选)</p>
                        <select class="form-select mt-2" multiple aria-label="budgets" name="budgets" th:field="*{budgets}">
                            <option th:each="budget, control:${budgets}" th:value="${budget.getId()}" >
                                [[${budget.budgetName}]]
                            </option>
                        </select>
                    </div>

                    <div class="col-sm-6">
                        <p class="mb-0">合同印花税类型</p>
                        <select class="form-select mt-2" aria-label="stamp" name="stamp" th:field="*{stamp}">
                            <option th:each="stamp, control:${stamps}" th:value="${stamp.getId()}">[[${stamp.type}]] -
                                [[${stamp.rate}]]
                            </option>
                        </select>
                    </div>

                    <div class="col-sm-6">
                        <label for="totalPrice" class="form-label">合同总价</label>
                        <input type="number" step="0.01" class="form-control" id="totalPrice" th:field="*{totalPrice}"
                               th:classappend="${#fields.hasErrors('totalPrice')}? 'is-invalid'" required>
                        <div id="totalPriceError" th:if="${#fields.hasErrors('totalPrice')}"
                             th:errors="*{totalPrice}"
                             class="invalid-feedback"></div>
                    </div>
                    <div class="col-sm-6">
                        <div class="form-check form-switch">
                            <input class="form-check-input" type="checkbox" id="isOpen" name="isOpen" th:checked="*{isOpen}"
                                   >
                            <label class="form-check-label" for="isOpen">单价合同或开口合同</label>
                        </div>
                    </div>
                    <hr>
                    <h4 class="mb-3">选填内容</h4>
                    <div class="col-sm-6">
                        <label for="priceWithoutVAT" class="form-label">不含税价</label>
                        <input type="number" step="0.01" class="form-control" id="priceWithoutVAT"
                               th:field="*{priceWithoutVAT}"
                               th:classappend="${#fields.hasErrors('priceWithoutVAT')}? 'is-invalid'"
                               placeholder="无法区分不含税价请留空">
                        <div id="priceWithoutVATError" th:if="${#fields.hasErrors('priceWithoutVAT')}"
                             th:errors="*{priceWithoutVAT}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="VAT" class="form-label">增值税额</label>
                        <input type="number" step="0.01" class="form-control" id="VAT" th:field="*{VAT}"
                               th:classappend="${#fields.hasErrors('VAT')}? 'is-invalid'" placeholder="无法区分税额请留空">
                        <div id="VATError" th:if="${#fields.hasErrors('VAT')}"
                             th:errors="*{VAT}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="finalPrice" class="form-label">结算价格</label>
                        <input type="number" step="0.01" class="form-control" id="finalPrice" th:field="*{finalPrice}"
                               th:classappend="${#fields.hasErrors('finalPrice')}? 'is-invalid'" placeholder="未结算请留空">
                        <div id="finalPriceError" th:if="${#fields.hasErrors('finalPrice')}"
                             th:errors="*{finalPrice}"
                             class="invalid-feedback"></div>
                    </div>
                    <p class="text-secondary">合同无法区分不含税价和增值税则留空。需要结算的合同在尚未结算时也请留空。(不需要填写0)</p>
                    <div class="col-sm-6">
                        <label for="signDate" class="form-label">签订时间</label>
                        <input type="date" class="form-control" id="signDate" th:field="*{signDate}"
                               th:classappend="${#fields.hasErrors('signDate')}? 'is-invalid'">
                        <div id="startTimeError" th:if="${#fields.hasErrors('signDate')}"
                             th:errors="*{signDate}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="endDate" class="form-label">终止时间</label>
                        <input type="date" class="form-control" id="endDate" th:field="*{endDate}"
                               th:classappend="${#fields.hasErrors('endDate')}? 'is-invalid'">
                        <div id="endTimeError" th:if="${#fields.hasErrors('endDate')}"
                             th:errors="*{endDate}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-4">
                        <label for="secondarySupplier1" class="form-label">其他合同方1</label>
                        <input class="form-control" id="secondarySupplier1" th:field="*{secondarySupplier1}"
                               th:classappend="${#fields.hasErrors('secondarySupplier1')}? 'is-invalid'">
                        <div id="supplier1Error" th:if="${#fields.hasErrors('secondarySupplier1')}"
                             th:errors="*{secondarySupplier1}"
                             class="invalid-feedback"></div>
                    </div>
                    <div class="col-sm-4">
                        <label for="secondarySupplier2" class="form-label">其他合同方2</label>
                        <input class="form-control" id="secondarySupplier2" th:field="*{secondarySupplier2}"
                               th:classappend="${#fields.hasErrors('secondarySupplier2')}? 'is-invalid'">
                        <div id="supplier2Error" th:if="${#fields.hasErrors('secondarySupplier2')}"
                             th:errors="*{secondarySupplier2}"
                             class="invalid-feedback"></div>
                    </div>
                    <div class="col-sm-4">
                        <label for="secondarySupplier3" class="form-label">其他合同方3</label>
                        <input class="form-control" id="secondarySupplier3" th:field="*{secondarySupplier3}"
                               th:classappend="${#fields.hasErrors('secondarySupplier3')}? 'is-invalid'">
                        <div id="supplier3Error" th:if="${#fields.hasErrors('secondarySupplier3')}"
                             th:errors="*{secondarySupplier3}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="target" class="form-label">合同标的</label>
                        <input class="form-control" id="target" th:field="*{target}"
                               th:classappend="${#fields.hasErrors('target')}? 'is-invalid'">
                        <div id="targetError" th:if="${#fields.hasErrors('target')}"
                             th:errors="*{target}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="contact" class="form-label">联系人</label>
                        <input class="form-control" id="contact" th:field="*{contact}"
                               th:classappend="${#fields.hasErrors('contact')}? 'is-invalid'">
                        <div id="contactError" th:if="${#fields.hasErrors('contact')}"
                             th:errors="*{contact}"
                             class="invalid-feedback"></div>
                    </div>

                    <div class="col-sm-6">
                        <label for="phone" class="form-label">联系电话</label>
                        <input class="form-control" id="phone" th:field="*{phone}"
                               th:classappend="${#fields.hasErrors('phone')}? 'is-invalid'">
                        <div id="phoneError" th:if="${#fields.hasErrors('phone')}"
                             th:errors="*{phone}"
                             class="invalid-feedback"></div>
                    </div>
                </div>
                <div class="text-center mb-3">
                    <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="${project.absoluteUrl()}" href="/pms/project/list"><i
                            class="fas fa-undo-alt"></i> 返回</a>
                </div>
            </form>
        </div>
        <div class="col-2"></div>

    </div>

</main>

删除合同

这都是套路了,只要配置好控制器以及权限就行了

    @PostMapping("/delete")
    public String deleteContract(@RequestParam("deleteId") int id,
                                 @RequestParam("name") String name,
                                 @RequestParam("projectId") int pid,
                                 RedirectAttributes attributes) {
        contractService.deleteContractById(id);
        attributes.addFlashAttribute("deleted", "已删除合同:" + name);
        String url = "/pms/contract/project/" + pid + "/list";
        return "redirect:" + url;
    }

然后是页面,其实就是修改那个模态框,为了方便,加了一个项目id的隐藏输入框:

<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="deleteLabel">删除合同</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    将删除[[${contract.name}]]
                </div>
                <form class="modal-footer" method="post" action="/pms/project/delete" th:action="@{/pms/contract/delete}">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><i class="fas fa-undo"></i>
                        返回
                    </button>
                    <input type="hidden" name="deleteId" th:value="${contract.id}">
                    <input type="hidden" name="projectId" th:value="${project.id}">
                    <input type="hidden" name="name" th:value="${contract.name}">
                    <button type="submit" class="btn btn-danger"><i class="fas fa-info-circle"></i>
                        确定删除
                    </button>
                </form>
            </div>
        </div>
    </div>

这里我进行测试的时候,提示了SQL错误,这是因为我在Contract类中把多对多映射设置成cascade = CascadeType.ALL,结果导致在删除一个合同的时候,也会去尝试删除对应的预算,这显然不合理,所以将其修改成CascadeType.PERSIST即可,之后还有一个问题,就是删除一个预算,就需要删除其对应的所有合同,所以还需要在Bugdet类中修改映射的级联为级联删除(或者设置为ALL也可以)即可:

    //关联到合同的多对多映射
    @ManyToMany(mappedBy = "budgets",cascade = CascadeType.ALL)
    private List<Contract> contracts;

这样做有一个问题,就是删除某一个预算的时候,会把其对应的所有合同都删除,即使合同已经关联到了其他的预算项目。不过考虑到预算项目一般是比较稳固不会变动的,所以就暂且这么做吧。否则应该保留对应合同。

最后在Spring Security中把对应的路径保护上:

.antMatchers("/pms/contract/delete").hasAnyAuthority("ADMIN", "SUPERUSER")

总结

这里千万不要忘记,CHECKBOX类型的输入框,在设置为不勾选的时候,是不会包含在表单中的,如果勾选,默认是传入一个on到后端。这个其实我在编写修改用户的时候已经单独获取了布尔值的输入框,可惜今天写的时候忘记了,还好回去看了一下,发现问题并解决掉了。

LICENSED UNDER CC BY-NC-SA 4.0
Comment