gongchunyi
4 天以前 9f281fb21c139b96cd2be8809e1c704782868c95
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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 样本编写。
 * <p>
 * <b>表结构摘要(与脚本一致)</b>
 * <ul>
 *   <li>{@code receipt_payment}:{@code sales_ledger_id}、{@code sales_ledger_product_id} 为 {@code int};
 *       {@code invoice_ledger_id} 为 {@code bigint};{@code receipt_payment_type} 为 {@code char(1)}(0/1);
 *       {@code receipt_payment_amount} {@code decimal(10,2)};{@code receipt_payment_date} 为 {@code datetime}。</li>
 *   <li>脚本中回款样本:{@code invoice_ledger_id} 与 {@code sales_ledger_product_id} 取值相同(例如 2158),
 *       与回款登记页把列表行主键当作 {@code invoiceLedgerId} 提交的行为一致。</li>
 *   <li>{@code sales_ledger_product}:待回款/已回款列为 {@code pending_invoice_total}、{@code invoice_total}(注释为回款总金额);
 *       产品行无 {@code tenant_id},租户在 {@code sales_ledger.tenant_id}。</li>
 *   <li>{@code account_income}:{@code business_id} 为 {@code int},与 {@code receipt_payment.id} 对应;
 *       {@code income_type='3'}、{@code income_described='回款收入'}、{@code business_type=1} 与库中回款样本一致。</li>
 *   <li>{@code registrant} / {@code input_user}:与脚本里 {@code receipt_payment} 登记人字段一致(样本为「樊志英」)。</li>
 * </ul>
 * <p>
 * <b>列表页</b>:{@code GET /sales/product/listPageSalesLedger} → {@code SalesLedgerProductMapper.listPage},
 * 筛选「待回款&gt;0」即 {@code pending_invoice_total &gt; 0}。
 * <p>
 * <b>单笔保存</b>:{@link ReceiptPaymentServiceImpl#receiptPaymentSaveOrUpdate(java.util.List)}(本测试复用其更新逻辑与收入字段含义)。
 * <p>
 * <b>开票台账校验</b>:结清前根据 {@code invoice_ledger} + {@code invoice_registration_product} 汇总开票金额,
 * 与已有 {@code receipt_payment} 比对;若开票侧已全部覆盖(按产品汇总回款或按真实开票台账 id 汇总回款),则跳过该产品,避免重复结清。
 * <p>
 * 执行前去掉 {@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<Integer> 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<ProductPendingRow> 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);
    }
 
    /**
     * 判断本产品关联的「开票台账」是否已在回款侧结清(无需再插入回款)。
     * <ul>
     *   <li>无开票记录(Σ invoice_total 为 0):返回 false,不据此拦截。</li>
     *   <li>有开票:若 {@code receipt_payment} 按 {@code sales_ledger_product_id} 汇总已 ≥ 开票总额,视为已回款(兼容 hbtmblc 将 invoice_ledger_id 存成产品 id 的写法)。</li>
     *   <li>否则:若每条 {@code invoice_ledger} 按真实 {@code invoice_ledger_id} 汇总的已回款均 ≥ 该行发票金额,也视为已回款。</li>
     * </ul>
     */
    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;
 
    }
}