package com.ruoyi.sales; import com.ruoyi.RuoYiApplication; import com.ruoyi.account.pojo.AccountIncome; import com.ruoyi.account.service.AccountIncomeService; import com.ruoyi.sales.mapper.ReceiptPaymentMapper; import com.ruoyi.sales.mapper.SalesLedgerMapper; import com.ruoyi.sales.mapper.SalesLedgerProductMapper; import com.ruoyi.sales.pojo.ReceiptPayment; import com.ruoyi.sales.pojo.SalesLedger; import com.ruoyi.sales.pojo.SalesLedgerProduct; import com.ruoyi.sales.service.impl.ReceiptPaymentServiceImpl; import lombok.Getter; import lombok.Setter; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import javax.sql.DataSource; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.Collections; import java.util.Date; import java.util.List; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 回款批量结清 —— 依据库脚本 {@code product-inventory-management-hbtmblc.sql} 中表结构与真实 INSERT 样本编写。 *

* 表结构摘要(与脚本一致) *

*

* 列表页:{@code GET /sales/product/listPageSalesLedger} → {@code SalesLedgerProductMapper.listPage}, * 筛选「待回款>0」即 {@code pending_invoice_total > 0}。 *

* 单笔保存:{@link ReceiptPaymentServiceImpl#receiptPaymentSaveOrUpdate(java.util.List)}(本测试复用其更新逻辑与收入字段含义)。 *

* 开票台账校验:结清前根据 {@code invoice_ledger} + {@code invoice_registration_product} 汇总开票金额, * 与已有 {@code receipt_payment} 比对;若开票侧已全部覆盖(按产品汇总回款或按真实开票台账 id 汇总回款),则跳过该产品,避免重复结清。 *

* 执行前去掉 {@link Disabled};默认 {@link Rollback},落库时改为 {@code @Rollback(false)}。 */ @SpringBootTest(classes = RuoYiApplication.class) //@Disabled("连接 hbtmblc 库并配置 spring.profiles 后去掉本注解再运行") class InvoiceLedgerPendingReceiptBatchTest { /** * 对应 {@code sales_ledger.tenant_id};null 表示不按租户过滤。 */ private static final Long TENANT_ID = null; /** * 仅处理这些 {@code sales_ledger_product.id}(与列表行主键一致);空表示全部待回款产品行。 */ private static final List ONLY_SALES_LEDGER_PRODUCT_IDS = Collections.emptyList(); /** * 与 {@code product-inventory-management-hbtmblc.sql} 中 {@code receipt_payment} 样本的 {@code registrant} 一致(如 id=286 一行:樊志英)。 * 若导入库中登记人不同,请改成与你库 {@code receipt_payment.registrant} 实际取值相同。 */ private static final String REGISTRANT = "樊志英"; private static final String RECEIPT_PAYMENT_TYPE = "0"; private static final LocalDate RECEIPT_PAYMENT_DATE = LocalDate.now(); /** * 是否在结清前校验「开票台账是否已回款完毕」。关闭则仅看 {@code pending_invoice_total}。 */ private static final boolean VERIFY_INVOICE_LEDGER_PAID = true; /** * 开票金额与已回款比较时的容差(元) */ private static final BigDecimal INVOICE_PAY_TOLERANCE = new BigDecimal("0.02"); /** * 与 listPage 维度一致:待回款 = {@code pending_invoice_total}。 */ private static final String PENDING_BY_PRODUCT_SQL = "SELECT slp.id AS salesLedgerProductId, " + "slp.sales_ledger_id AS salesLedgerId, " + "sl.tenant_id AS tenantId " + "FROM sales_ledger_product slp " + "INNER JOIN sales_ledger sl ON sl.id = slp.sales_ledger_id " + "WHERE slp.type = 1 " + "AND IFNULL(slp.pending_invoice_total, 0) > 0 "; @Autowired private DataSource dataSource; @Autowired private ReceiptPaymentMapper receiptPaymentMapper; @Autowired private SalesLedgerMapper salesLedgerMapper; @Autowired private SalesLedgerProductMapper salesLedgerProductMapper; @Autowired private AccountIncomeService accountIncomeService; @Test @Transactional void batchCompletePendingReceipts_hbtmblcSchema() { NamedParameterJdbcTemplate named = new NamedParameterJdbcTemplate(dataSource); StringBuilder sql = new StringBuilder(PENDING_BY_PRODUCT_SQL); MapSqlParameterSource params = new MapSqlParameterSource(); if (!ONLY_SALES_LEDGER_PRODUCT_IDS.isEmpty()) { sql.append(" AND slp.id IN (:productIds) "); params.addValue("productIds", ONLY_SALES_LEDGER_PRODUCT_IDS); } sql.append(" ORDER BY slp.id ASC "); List rows = named.query(sql.toString(), params, new BeanPropertyRowMapper<>(ProductPendingRow.class)); int done = 0; for (ProductPendingRow row : rows) { SalesLedger salesLedger = salesLedgerMapper.selectById(row.getSalesLedgerId().longValue()); if (salesLedger == null) { throw new IllegalStateException("sales_ledger_product.id=" + row.getSalesLedgerProductId() + " 关联 sales_ledger 不存在"); } SalesLedgerProduct product = salesLedgerProductMapper.selectById(row.getSalesLedgerProductId().longValue()); if (product == null) { throw new IllegalStateException("sales_ledger_product.id=" + row.getSalesLedgerProductId() + " 不存在"); } BigDecimal remaining = product.getPendingInvoiceTotal() == null ? BigDecimal.ZERO : product.getPendingInvoiceTotal(); if (remaining.compareTo(BigDecimal.ZERO) <= 0) { continue; } remaining = remaining.setScale(2, RoundingMode.HALF_UP); if (VERIFY_INVOICE_LEDGER_PAID && isInvoiceLedgerSideFullyReceived(named, row.getSalesLedgerProductId())) { continue; } BigDecimal invoicedTotal = product.getInvoiceTotal() == null ? BigDecimal.ZERO : product.getInvoiceTotal(); BigDecimal taxTotal = product.getTaxInclusiveTotalPrice() == null ? BigDecimal.ZERO : product.getTaxInclusiveTotalPrice(); product.setInvoiceTotal(invoicedTotal.add(remaining)); product.setPendingInvoiceTotal(taxTotal.subtract(product.getInvoiceTotal())); ReceiptPayment rp = new ReceiptPayment(); rp.setReceiptPaymentType(RECEIPT_PAYMENT_TYPE); rp.setReceiptPaymentAmount(remaining); rp.setRegistrant(REGISTRANT); rp.setInvoiceLedgerId(row.getSalesLedgerProductId()); rp.setSalesLedgerId(row.getSalesLedgerId().longValue()); rp.setSalesLedgerProductId(row.getSalesLedgerProductId().longValue()); rp.setReceiptPaymentDate(RECEIPT_PAYMENT_DATE); rp.setTenantId(row.getTenantId()); receiptPaymentMapper.insert(rp); AccountIncome income = new AccountIncome(); income.setIncomeDate(salesLedger.getEntryDate()); income.setIncomeType("3"); income.setCustomerName(salesLedger.getCustomerName()); income.setIncomeMoney(remaining); income.setIncomeDescribed("回款收入"); income.setIncomeMethod(RECEIPT_PAYMENT_TYPE); income.setInputTime(new Date()); income.setInputUser(REGISTRANT); if (rp.getId() != null) { income.setBusinessId(rp.getId().longValue()); } income.setBusinessType(1); income.setTenantId(row.getTenantId()); accountIncomeService.save(income); salesLedgerProductMapper.updateById(product); done++; } assertTrue(done >= 0, "新增回款笔数: " + done); } /** * 判断本产品关联的「开票台账」是否已在回款侧结清(无需再插入回款)。 *

*/ private boolean isInvoiceLedgerSideFullyReceived(NamedParameterJdbcTemplate named, int salesLedgerProductId) { MapSqlParameterSource p = new MapSqlParameterSource("pid", salesLedgerProductId); BigDecimal invoicedSum = named.queryForObject( "SELECT COALESCE(SUM(il.invoice_total), 0) FROM invoice_ledger il " + "INNER JOIN invoice_registration_product irp ON irp.id = il.invoice_registration_product_id " + "WHERE irp.sales_ledger_product_id = :pid", p, BigDecimal.class); if (invoicedSum == null) { invoicedSum = BigDecimal.ZERO; } invoicedSum = invoicedSum.setScale(2, RoundingMode.HALF_UP); if (invoicedSum.compareTo(BigDecimal.ZERO) <= 0) { return false; } BigDecimal paidByProduct = named.queryForObject( "SELECT COALESCE(SUM(receipt_payment_amount), 0) FROM receipt_payment WHERE sales_ledger_product_id = :pid", p, BigDecimal.class); if (paidByProduct == null) { paidByProduct = BigDecimal.ZERO; } paidByProduct = paidByProduct.setScale(2, RoundingMode.HALF_UP); if (paidByProduct.compareTo(invoicedSum.subtract(INVOICE_PAY_TOLERANCE)) >= 0) { return true; } Integer unpaidLedgerLineCount = named.queryForObject( "SELECT COUNT(1) FROM (" + " SELECT 1 FROM invoice_ledger il " + " INNER JOIN invoice_registration_product irp ON irp.id = il.invoice_registration_product_id " + " LEFT JOIN receipt_payment rp ON rp.invoice_ledger_id = il.id " + " WHERE irp.sales_ledger_product_id = :pid " + " GROUP BY il.id, il.invoice_total " + " HAVING (IFNULL(il.invoice_total, 0) - IFNULL(SUM(rp.receipt_payment_amount), 0)) > :tol" + ") t", p.addValue("tol", INVOICE_PAY_TOLERANCE), Integer.class); return unpaidLedgerLineCount != null && unpaidLedgerLineCount == 0; } @Setter @Getter @SuppressWarnings("unused") public static class ProductPendingRow { private Integer salesLedgerProductId; private Integer salesLedgerId; private Long tenantId; } }