Merge branch 'test' of https://git.lionquexpress.com/wuweihua/BMS into test
This commit is contained in:
@@ -14,6 +14,31 @@ import com.bms.receivable.profit.ArCommissionResult;
|
||||
|
||||
class ArProfitMapperSqlTest
|
||||
{
|
||||
@Test
|
||||
void snapshotCalculationIsScopedToTheGeneratingBill() throws Exception
|
||||
{
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.getTypeAliasRegistry().registerAlias("ArBill", com.bms.receivable.domain.ArBill.class);
|
||||
for (String resource : new String[] {"mapper/receivable/ArBillScopeMapper.xml", "mapper/receivable/ArBillMapper.xml"})
|
||||
{
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource))
|
||||
{
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
}
|
||||
var bound = configuration.getMappedStatement("com.bms.receivable.mapper.ArBillMapper.updateProfitSnapshot")
|
||||
.getBoundSql(Map.of("arBillId", 42L, "shenzhenArea", "C", "guangzhouArea", "GZ"));
|
||||
String sql = bound.getSql();
|
||||
assertTrue(sql.contains("fd.ar_bill_id = ?"));
|
||||
assertTrue(sql.contains("d.ar_bill_id = ?"));
|
||||
assertTrue(sql.contains("b.ar_bill_id = ? and b.generation_status = 'GENERATING'"));
|
||||
assertTrue(sql.contains("f.sales_amount = d.total_amount"));
|
||||
assertTrue(sql.contains("a.detail_total != b.total_amount"));
|
||||
assertTrue(sql.contains("'MISSING_PRICE'"));
|
||||
assertTrue(sql.contains("'CURRENCY_MISMATCH'"));
|
||||
assertTrue(sql.contains("'PRICE_OVERFLOW'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commissionInReportSummaryUsesBillFiltersAndDataScope() throws Exception
|
||||
{
|
||||
@@ -23,7 +48,7 @@ class ArProfitMapperSqlTest
|
||||
{
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
var statement = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSummary");
|
||||
var statement = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectSummary");
|
||||
ArProfitQuery query = new ArProfitQuery();
|
||||
query.setCustomerId(5L);
|
||||
query.setBillNo("AR123");
|
||||
@@ -74,7 +99,7 @@ class ArProfitMapperSqlTest
|
||||
}
|
||||
for (String id : new String[] { "selectBills", "selectSummary", "selectBillDetails", "selectFees" })
|
||||
{
|
||||
String name = "com.bms.ar.mapper.ArProfitMapper." + id;
|
||||
String name = "com.bms.receivable.mapper.ArProfitMapper." + id;
|
||||
String self = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
"query", new ArProfitQuery(), "scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
|
||||
String manager = configuration.getMappedStatement(name).getBoundSql(Map.of(
|
||||
@@ -84,18 +109,26 @@ class ArProfitMapperSqlTest
|
||||
assertTrue(manager.contains("find_in_set"));
|
||||
assertTrue(manager.contains("b.status in"));
|
||||
}
|
||||
String bills = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectBills")
|
||||
String bills = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectBills")
|
||||
.getBoundSql(Map.of("query", new ArProfitQuery(), "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertTrue(bills.contains("from ar_bill b"));
|
||||
assertTrue(bills.contains("b.bill_name"));
|
||||
assertTrue(bills.contains("b.profit_calc_status = 'READY'"));
|
||||
assertTrue(bills.contains("b.product_amount"));
|
||||
assertTrue(bills.contains("b.redline_amount"));
|
||||
assertFalse(bills.contains("ar_bill_detail"));
|
||||
assertFalse(bills.contains("order_no"));
|
||||
String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary")
|
||||
String summary = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectSummary")
|
||||
.getBoundSql(Map.of("query", new ArProfitQuery(), "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertFalse(summary.contains("ar_bill_detail"));
|
||||
assertTrue(summary.contains("ar_profit_commission_result"));
|
||||
String billCommission = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.selectCommissionSummary")
|
||||
.getBoundSql(Map.of("billId", 9L, "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertTrue(billCommission.contains("r.ar_bill_id = ?"));
|
||||
assertTrue(billCommission.contains("then null"));
|
||||
for (String id : new String[] { "selectResults", "selectCommissionSummary" })
|
||||
{
|
||||
var statement = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper." + id);
|
||||
var statement = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper." + id);
|
||||
String self = statement.getBoundSql(Map.of("billId", 9L, "includeReversed", true,
|
||||
"scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
|
||||
String department = statement.getBoundSql(Map.of("includeReversed", false,
|
||||
@@ -113,7 +146,7 @@ class ArProfitMapperSqlTest
|
||||
assertFalse(all.contains("b.profit_sales_user_id = ?"));
|
||||
assertFalse(all.contains("find_in_set"));
|
||||
}
|
||||
var salesSql = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSalesOptions")
|
||||
var salesSql = configuration.getMappedStatement("com.bms.receivable.mapper.ArProfitMapper.selectSalesOptions")
|
||||
.getBoundSql(Map.of("keyword", "sales"));
|
||||
assertTrue(salesSql.getSql().contains("from sys_user u"));
|
||||
assertTrue(salesSql.getSql().contains("u.nick_name like"));
|
||||
@@ -121,11 +154,11 @@ class ArProfitMapperSqlTest
|
||||
assertFalse(salesSql.getSql().contains("ar_bill"));
|
||||
assertFalse(salesSql.getSql().contains("sys_role"));
|
||||
assertTrue(salesSql.getSql().contains("limit 50"));
|
||||
String settlement = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectSettlementInputs")
|
||||
String settlement = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.selectSettlementInputs")
|
||||
.getBoundSql(Map.of("billId", 1L)).getSql();
|
||||
assertTrue(settlement.contains("w.status = 'EFFECTIVE'"));
|
||||
assertTrue(settlement.contains("b.status = 'WRITTEN_OFF'"));
|
||||
var insert = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.insertResults")
|
||||
var insert = configuration.getMappedStatement("com.bms.receivable.mapper.ArCommissionMapper.insertResults")
|
||||
.getBoundSql(Map.of("items", java.util.List.of(new ArCommissionResult(), new ArCommissionResult()), "operator", "test"));
|
||||
assertTrue(insert.getSql().contains("ar_profit_commission_result"));
|
||||
assertTrue(insert.getParameterMappings().size() == 16);
|
||||
|
||||
@@ -36,6 +36,10 @@ public interface ArBillMapper {
|
||||
|
||||
int insertArBill(ArBill bill);
|
||||
|
||||
int updateProfitSnapshot(@Param("arBillId") Long arBillId,
|
||||
@Param("shenzhenArea") String shenzhenArea,
|
||||
@Param("guangzhouArea") String guangzhouArea);
|
||||
|
||||
int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status,
|
||||
@Param("remark") String remark, @Param("operator") String operator,
|
||||
@Param("fromStatus") String fromStatus);
|
||||
|
||||
+6
@@ -935,6 +935,12 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
throw new ServiceException("账单生成结果为空或订单数量发生变化,请重新生成");
|
||||
}
|
||||
ArBill finalBill = billMapper.selectArBillById(task.getArBillId());
|
||||
// 明细与费用已全部落库;快照和 READY 状态必须在同一事务中提交。
|
||||
if (billMapper.updateProfitSnapshot(task.getArBillId(),
|
||||
com.bms.price.enums.ProfitAreaRule.SHENZHEN.getCustomerArea(),
|
||||
com.bms.price.enums.ProfitAreaRule.GUANGZHOU.getCustomerArea()) != 1) {
|
||||
throw new ServiceException("账单毛利快照更新失败,请重新生成");
|
||||
}
|
||||
billMapper.updateArBillGenerationStatus(task.getArBillId(),
|
||||
ArBillGenerationStatus.READY, task.getCreateBy(), null);
|
||||
if (generateTaskMapper.markSuccess(taskId, new Date()) != 1) {
|
||||
|
||||
@@ -193,6 +193,49 @@
|
||||
#{confirmedBy}, #{confirmedTime}, #{createBy}, #{createUserId}, sysdate(), #{updateBy}, sysdate())
|
||||
</insert>
|
||||
|
||||
<!-- 只在生成完成时汇总当前账单;历史回填使用相同的校验口径。 -->
|
||||
<update id="updateProfitSnapshot">
|
||||
update ar_bill b
|
||||
left join (
|
||||
select d.ar_bill_id, count(*) as detail_count, sum(d.total_amount) as detail_total,
|
||||
count(distinct d.currency) as detail_currency_count, min(d.currency) as detail_currency,
|
||||
sum(case when d.currency is not null and f.currency_count = 1 and f.fee_currency = d.currency
|
||||
then 0 else 1 end) as currency_errors,
|
||||
sum(case when d.total_amount is not null and f.sales_amount is not null
|
||||
and f.sales_amount = d.total_amount then 0 else 1 end) as amount_errors,
|
||||
case when count(f.product_amount) = count(*) then sum(f.product_amount) else null end as product_total,
|
||||
case when count(f.redline_amount) = count(*) then sum(f.redline_amount) else null end as redline_total
|
||||
from ar_bill_detail d
|
||||
left join (
|
||||
select fee.detail_id,
|
||||
case when count(fee.amount) = count(*) then sum(fee.amount) else null end as sales_amount,
|
||||
case when count(fee.currency) = count(*) then count(distinct fee.currency) else 0 end as currency_count,
|
||||
min(fee.currency) as fee_currency,
|
||||
case when count(fee.product_amount) = count(*) then sum(fee.product_amount) else null end as product_amount,
|
||||
case when count(fee.redline_amount) = count(*) then sum(fee.redline_amount) else null end as redline_amount
|
||||
from ar_bill_detail_fee fee
|
||||
join ar_bill_detail fd on fd.detail_id = fee.detail_id
|
||||
where fd.ar_bill_id = #{arBillId}
|
||||
group by fee.detail_id
|
||||
) f on f.detail_id = d.detail_id
|
||||
where d.ar_bill_id = #{arBillId}
|
||||
group by d.ar_bill_id
|
||||
) a on a.ar_bill_id = b.ar_bill_id
|
||||
set b.product_amount = case when abs(a.product_total) <= 99999999999999.9999 then a.product_total else null end,
|
||||
b.redline_amount = case when abs(a.redline_total) <= 99999999999999.9999 then a.redline_total else null end,
|
||||
b.profit_calc_status = case
|
||||
when a.detail_count is null or a.detail_count = 0 then 'NO_DETAIL'
|
||||
when b.profit_customer_area is null or b.profit_customer_area not in (#{shenzhenArea}, #{guangzhouArea}) then 'MISSING_AREA'
|
||||
when b.currency is null or a.currency_errors > 0 or a.detail_currency_count != 1 or a.detail_currency != b.currency then 'CURRENCY_MISMATCH'
|
||||
when b.total_amount is null or a.amount_errors > 0 or a.detail_total != b.total_amount then 'AMOUNT_MISMATCH'
|
||||
when (b.profit_customer_area = #{shenzhenArea} and a.product_total is null)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and a.redline_total is null) then 'MISSING_PRICE'
|
||||
when (b.profit_customer_area = #{shenzhenArea} and abs(a.product_total) > 99999999999999.9999)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and abs(a.redline_total) > 99999999999999.9999) then 'PRICE_OVERFLOW'
|
||||
else 'READY' end
|
||||
where b.ar_bill_id = #{arBillId} and b.generation_status = 'GENERATING'
|
||||
</update>
|
||||
|
||||
<update id="updateArBillStatus">
|
||||
update ar_bill
|
||||
set status = #{status},
|
||||
|
||||
@@ -60,7 +60,16 @@
|
||||
<if test="query.endBillDate != null">and b.bill_date <= #{query.endBillDate}</if>
|
||||
</sql>
|
||||
|
||||
<!-- 按费用快照先聚合到订单,再聚合到完整账单,避免账单金额及费用重复。 -->
|
||||
<!-- 报表直接读取账单快照。 -->
|
||||
<sql id="validProfitSnapshot">
|
||||
b.profit_calc_status = 'READY' and b.total_amount is not null
|
||||
and ((b.profit_customer_area = #{shenzhenArea} and b.product_amount is not null)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and b.redline_amount is not null))
|
||||
</sql>
|
||||
<sql id="areaProductAmount">
|
||||
case when b.profit_customer_area = #{shenzhenArea} then b.product_amount
|
||||
when b.profit_customer_area = #{guangzhouArea} then b.redline_amount else null end
|
||||
</sql>
|
||||
<sql id="billRows">
|
||||
<bind name="shenzhenArea" value="@com.bms.price.enums.ProfitAreaRule@SHENZHEN.getCustomerArea()"/>
|
||||
<bind name="guangzhouArea" value="@com.bms.price.enums.ProfitAreaRule@GUANGZHOU.getCustomerArea()"/>
|
||||
@@ -68,45 +77,12 @@
|
||||
b.profit_customer_area as customer_area,
|
||||
b.profit_sales_user_name as sales_user_name, b.currency, b.status,
|
||||
b.total_amount as sales_amount, b.written_off_amount, b.unwritten_amount,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount then a.product_total else null end as product_amount,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount then b.total_amount - a.product_total else null end as sales_gross_profit,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount and b.total_amount != 0
|
||||
then round((b.total_amount - a.product_total) * 100 / b.total_amount, 4) else null end as sales_gross_margin_rate,
|
||||
case when a.detail_count > 0 and a.invalid_count = 0 and b.total_amount is not null
|
||||
and a.detail_total = b.total_amount then 0 else 1 end as profit_incomplete
|
||||
case when <include refid="validProfitSnapshot"/> then <include refid="areaProductAmount"/> else null end as product_amount,
|
||||
case when <include refid="validProfitSnapshot"/> then b.total_amount - (<include refid="areaProductAmount"/>) else null end as sales_gross_profit,
|
||||
case when <include refid="validProfitSnapshot"/> and b.total_amount != 0
|
||||
then round((b.total_amount - (<include refid="areaProductAmount"/>)) * 100 / b.total_amount, 4) else null end as sales_gross_margin_rate,
|
||||
case when <include refid="validProfitSnapshot"/> then 0 else 1 end as profit_incomplete
|
||||
from ar_bill b
|
||||
left join (
|
||||
select d.ar_bill_id, count(*) as detail_count, sum(d.total_amount) as detail_total,
|
||||
sum(case when d.total_amount is not null and d.currency is not null and b.currency is not null
|
||||
and d.currency = b.currency and f.currency_count = 1 and f.fee_currency = d.currency
|
||||
and f.sales_amount is not null and f.sales_amount = d.total_amount
|
||||
and ((b.profit_customer_area = #{shenzhenArea} and f.product_complete = 1)
|
||||
or (b.profit_customer_area = #{guangzhouArea} and f.redline_complete = 1))
|
||||
then 0 else 1 end) as invalid_count,
|
||||
sum(case when b.profit_customer_area = #{shenzhenArea} then f.product_amount
|
||||
when b.profit_customer_area = #{guangzhouArea} then f.redline_amount else null end) as product_total
|
||||
from ar_bill_detail d join ar_bill b on b.ar_bill_id = d.ar_bill_id
|
||||
left join (
|
||||
select fee.detail_id,
|
||||
case when count(fee.amount) = count(*) then sum(fee.amount) else null end as sales_amount,
|
||||
case when count(fee.currency) = count(*) then count(distinct fee.currency) else 0 end as currency_count,
|
||||
min(fee.currency) as fee_currency,
|
||||
case when count(fee.product_amount) = count(*) then sum(fee.product_amount) else null end as product_amount,
|
||||
case when count(fee.redline_amount) = count(*) then sum(fee.redline_amount) else null end as redline_amount,
|
||||
case when count(fee.product_amount) = count(*) then 1 else 0 end as product_complete,
|
||||
case when count(fee.redline_amount) = count(*) then 1 else 0 end as redline_complete
|
||||
from ar_bill_detail_fee fee
|
||||
join ar_bill_detail d on d.detail_id = fee.detail_id
|
||||
join ar_bill b on b.ar_bill_id = d.ar_bill_id
|
||||
<include refid="billConditions"/>
|
||||
group by fee.detail_id
|
||||
) f on f.detail_id = d.detail_id
|
||||
<include refid="billConditions"/>
|
||||
group by d.ar_bill_id
|
||||
) a on a.ar_bill_id = b.ar_bill_id
|
||||
<include refid="billConditions"/>
|
||||
</sql>
|
||||
<select id="selectBills" resultMap="row">
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
-- 应收账单毛利汇总快照升级 v1,MySQL 5.7+。
|
||||
-- 前置:执行 20261105_ar_profit_report.sql;停止所有旧版后端(包括异步任务),在独立连接执行本文件,
|
||||
-- 核对末尾统计后再部署新版本。执行工具须支持 DELIMITER 和存储过程。
|
||||
-- DDL 自动提交,历史回填每批独立提交;异常修复后可重跑完整文件,已成功的快照不会覆盖。
|
||||
-- 历史客户区域沿用账单已有快照;NULL 表示未计算,异常账单不填零。
|
||||
-- READY 有效;NO_DETAIL 无明细;MISSING_AREA 区域缺失或不支持;MISSING_PRICE 缺价格;
|
||||
-- CURRENCY_MISMATCH 币种异常;AMOUNT_MISMATCH 金额不一致;PRICE_OVERFLOW 超出字段范围。
|
||||
SET NAMES utf8mb4;
|
||||
DROP PROCEDURE IF EXISTS bms_ar_bill_profit_snapshot_20261111_v1;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE bms_ar_bill_profit_snapshot_20261111_v1()
|
||||
BEGIN
|
||||
DECLARE n BIGINT DEFAULT 0;
|
||||
DECLARE first_id BIGINT DEFAULT NULL;
|
||||
DECLARE last_id BIGINT DEFAULT 0;
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
SELECT COUNT(*) INTO n FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name IN ('ar_bill', 'ar_bill_detail', 'ar_bill_detail_fee');
|
||||
IF n <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '毛利快照升级前置表缺失'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND ((table_name = 'ar_bill' AND column_name IN ('ar_bill_id','profit_customer_area','total_amount','currency','generation_status'))
|
||||
OR (table_name = 'ar_bill_detail' AND column_name IN ('detail_id','ar_bill_id','total_amount','currency'))
|
||||
OR (table_name = 'ar_bill_detail_fee' AND column_name IN ('detail_id','amount','currency','product_amount','redline_amount')));
|
||||
IF n <> 14 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '请先完成应收毛利及费用价格快照升级'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill'
|
||||
AND column_name IN ('product_amount', 'redline_amount', 'profit_calc_status')
|
||||
AND NOT (is_nullable = 'YES' AND ((column_name IN ('product_amount', 'redline_amount')
|
||||
AND data_type = 'decimal' AND numeric_precision = 18 AND numeric_scale = 4)
|
||||
OR (column_name = 'profit_calc_status' AND data_type = 'varchar' AND character_maximum_length = 24)));
|
||||
IF n > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '已有账单毛利快照字段结构冲突'; END IF;
|
||||
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'product_amount';
|
||||
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN product_amount DECIMAL(18,4) NULL COMMENT '账单产品价合计快照' AFTER total_amount; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'redline_amount';
|
||||
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN redline_amount DECIMAL(18,4) NULL COMMENT '账单红线价合计快照' AFTER product_amount; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'profit_calc_status';
|
||||
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN profit_calc_status VARCHAR(24) NULL COMMENT '毛利快照校验状态,NULL表示未计算' AFTER redline_amount; END IF;
|
||||
SELECT COUNT(*) INTO n FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND is_nullable = 'YES'
|
||||
AND ((column_name IN ('product_amount', 'redline_amount') AND data_type = 'decimal' AND numeric_precision = 18 AND numeric_scale = 4)
|
||||
OR (column_name = 'profit_calc_status' AND data_type = 'varchar' AND character_maximum_length = 24));
|
||||
IF n <> 3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '账单毛利快照字段结构冲突,请核对后重试'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM ar_bill WHERE profit_calc_status IS NULL
|
||||
AND (product_amount IS NOT NULL OR redline_amount IS NOT NULL);
|
||||
IF n > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '存在无状态的已填价格快照,请人工核对'; END IF;
|
||||
SELECT COUNT(*) INTO n FROM ar_bill WHERE profit_calc_status IS NOT NULL
|
||||
AND profit_calc_status NOT IN ('READY','NO_DETAIL','MISSING_AREA','CURRENCY_MISMATCH','AMOUNT_MISMATCH','MISSING_PRICE','PRICE_OVERFLOW');
|
||||
IF n > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '存在未知毛利快照状态,请人工核对'; END IF;
|
||||
|
||||
backfill: LOOP
|
||||
SELECT MIN(ar_bill_id) INTO first_id FROM ar_bill
|
||||
WHERE generation_status = 'READY' AND (profit_calc_status IS NULL OR profit_calc_status != 'READY') AND ar_bill_id > last_id;
|
||||
IF first_id IS NULL THEN LEAVE backfill; END IF;
|
||||
SELECT MAX(batch.ar_bill_id) INTO last_id FROM (
|
||||
SELECT ar_bill_id FROM ar_bill WHERE generation_status = 'READY' AND (profit_calc_status IS NULL OR profit_calc_status != 'READY')
|
||||
AND ar_bill_id >= first_id ORDER BY ar_bill_id LIMIT 200
|
||||
) batch;
|
||||
START TRANSACTION;
|
||||
-- 与 ArBillMapper.updateProfitSnapshot 的汇总和校验保持一致。
|
||||
UPDATE ar_bill b
|
||||
LEFT JOIN (
|
||||
SELECT d.ar_bill_id, COUNT(*) AS detail_count, SUM(d.total_amount) AS detail_total,
|
||||
COUNT(DISTINCT d.currency) AS detail_currency_count, MIN(d.currency) AS detail_currency,
|
||||
SUM(CASE WHEN d.currency IS NOT NULL AND f.currency_count = 1 AND f.fee_currency = d.currency
|
||||
THEN 0 ELSE 1 END) AS currency_errors,
|
||||
SUM(CASE WHEN d.total_amount IS NOT NULL AND f.sales_amount IS NOT NULL
|
||||
AND f.sales_amount = d.total_amount THEN 0 ELSE 1 END) AS amount_errors,
|
||||
CASE WHEN COUNT(f.product_amount) = COUNT(*) THEN SUM(f.product_amount) ELSE NULL END AS product_total,
|
||||
CASE WHEN COUNT(f.redline_amount) = COUNT(*) THEN SUM(f.redline_amount) ELSE NULL END AS redline_total
|
||||
FROM ar_bill_detail d
|
||||
LEFT JOIN (
|
||||
SELECT fee.detail_id,
|
||||
CASE WHEN COUNT(fee.amount) = COUNT(*) THEN SUM(fee.amount) ELSE NULL END AS sales_amount,
|
||||
CASE WHEN COUNT(fee.currency) = COUNT(*) THEN COUNT(DISTINCT fee.currency) ELSE 0 END AS currency_count,
|
||||
MIN(fee.currency) AS fee_currency,
|
||||
CASE WHEN COUNT(fee.product_amount) = COUNT(*) THEN SUM(fee.product_amount) ELSE NULL END AS product_amount,
|
||||
CASE WHEN COUNT(fee.redline_amount) = COUNT(*) THEN SUM(fee.redline_amount) ELSE NULL END AS redline_amount
|
||||
FROM ar_bill_detail_fee fee
|
||||
JOIN ar_bill_detail fd ON fd.detail_id = fee.detail_id
|
||||
WHERE fd.ar_bill_id BETWEEN first_id AND last_id
|
||||
GROUP BY fee.detail_id
|
||||
) f ON f.detail_id = d.detail_id
|
||||
WHERE d.ar_bill_id BETWEEN first_id AND last_id
|
||||
GROUP BY d.ar_bill_id
|
||||
) a ON a.ar_bill_id = b.ar_bill_id
|
||||
SET b.product_amount = CASE WHEN ABS(a.product_total) <= 99999999999999.9999 THEN a.product_total ELSE NULL END,
|
||||
b.redline_amount = CASE WHEN ABS(a.redline_total) <= 99999999999999.9999 THEN a.redline_total ELSE NULL END,
|
||||
b.profit_calc_status = CASE
|
||||
WHEN a.detail_count IS NULL OR a.detail_count = 0 THEN 'NO_DETAIL'
|
||||
WHEN b.profit_customer_area IS NULL OR b.profit_customer_area NOT IN ('C', 'GZ') THEN 'MISSING_AREA'
|
||||
WHEN b.currency IS NULL OR a.currency_errors > 0 OR a.detail_currency_count != 1 OR a.detail_currency != b.currency THEN 'CURRENCY_MISMATCH'
|
||||
WHEN b.total_amount IS NULL OR a.amount_errors > 0 OR a.detail_total != b.total_amount THEN 'AMOUNT_MISMATCH'
|
||||
WHEN (b.profit_customer_area = 'C' AND a.product_total IS NULL)
|
||||
OR (b.profit_customer_area = 'GZ' AND a.redline_total IS NULL) THEN 'MISSING_PRICE'
|
||||
WHEN (b.profit_customer_area = 'C' AND ABS(a.product_total) > 99999999999999.9999)
|
||||
OR (b.profit_customer_area = 'GZ' AND ABS(a.redline_total) > 99999999999999.9999) THEN 'PRICE_OVERFLOW'
|
||||
ELSE 'READY' END
|
||||
WHERE b.ar_bill_id BETWEEN first_id AND last_id AND b.generation_status = 'READY'
|
||||
AND (b.profit_calc_status IS NULL OR b.profit_calc_status != 'READY');
|
||||
COMMIT;
|
||||
END LOOP;
|
||||
SELECT COALESCE(profit_calc_status, 'NOT_CALCULATED') AS profit_calc_status, COUNT(*) AS bill_count
|
||||
FROM ar_bill WHERE generation_status = 'READY' GROUP BY profit_calc_status ORDER BY profit_calc_status;
|
||||
SELECT COUNT(*) AS ready_unprocessed FROM ar_bill WHERE generation_status = 'READY' AND profit_calc_status IS NULL;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL bms_ar_bill_profit_snapshot_20261111_v1();
|
||||
DROP PROCEDURE IF EXISTS bms_ar_bill_profit_snapshot_20261111_v1;
|
||||
@@ -76,6 +76,9 @@ CREATE TABLE `ar_bill` (
|
||||
`currency` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'CNY' COMMENT '币种',
|
||||
`order_count` int(11) NOT NULL DEFAULT '0' COMMENT '订单数量',
|
||||
`total_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '账单应收总额',
|
||||
`product_amount` decimal(18,4) DEFAULT NULL COMMENT '账单产品价合计快照',
|
||||
`redline_amount` decimal(18,4) DEFAULT NULL COMMENT '账单红线价合计快照',
|
||||
`profit_calc_status` varchar(24) DEFAULT NULL COMMENT '毛利快照校验状态,NULL表示未计算',
|
||||
`written_off_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已核销金额缓存',
|
||||
`unwritten_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '未核销金额缓存',
|
||||
`status` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '状态:DRAFT=草稿,CONFIRMED=已确认,PARTIAL_WRITTEN_OFF=部分核销,WRITTEN_OFF=已核销,VOID=已作废',
|
||||
|
||||
Reference in New Issue
Block a user