diff --git a/bms-back/bms-admin/src/test/java/com/bms/report/ArProfitMapperSqlTest.java b/bms-back/bms-admin/src/test/java/com/bms/report/ArProfitMapperSqlTest.java
index ff422aae..225d2433 100644
--- a/bms-back/bms-admin/src/test/java/com/bms/report/ArProfitMapperSqlTest.java
+++ b/bms-back/bms-admin/src/test/java/com/bms/report/ArProfitMapperSqlTest.java
@@ -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);
diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillMapper.java b/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillMapper.java
index fdbe8aec..3bf8ab41 100644
--- a/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillMapper.java
+++ b/bms-back/bms-system/src/main/java/com/bms/receivable/mapper/ArBillMapper.java
@@ -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);
diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java
index 9b02fb6d..e0431a02 100644
--- a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java
+++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java
@@ -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) {
diff --git a/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml
index 93528de1..4d917000 100644
--- a/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml
+++ b/bms-back/bms-system/src/main/resources/mapper/receivable/ArBillMapper.xml
@@ -193,6 +193,49 @@
#{confirmedBy}, #{confirmedTime}, #{createBy}, #{createUserId}, sysdate(), #{updateBy}, sysdate())
+
+
+ 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 ar_bill
set status = #{status},
diff --git a/bms-back/bms-system/src/main/resources/mapper/receivable/ArProfitMapper.xml b/bms-back/bms-system/src/main/resources/mapper/receivable/ArProfitMapper.xml
index 31f48e1f..5aaefa3a 100644
--- a/bms-back/bms-system/src/main/resources/mapper/receivable/ArProfitMapper.xml
+++ b/bms-back/bms-system/src/main/resources/mapper/receivable/ArProfitMapper.xml
@@ -60,7 +60,16 @@
and b.bill_date <= #{query.endBillDate}
-
+
+
+ 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))
+
+
+ case when b.profit_customer_area = #{shenzhenArea} then b.product_amount
+ when b.profit_customer_area = #{guangzhouArea} then b.redline_amount else null end
+
@@ -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 then else null end as product_amount,
+ case when then b.total_amount - () else null end as sales_gross_profit,
+ case when and b.total_amount != 0
+ then round((b.total_amount - ()) * 100 / b.total_amount, 4) else null end as sales_gross_margin_rate,
+ case when 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
-
- group by fee.detail_id
- ) f on f.detail_id = d.detail_id
-
- group by d.ar_bill_id
- ) a on a.ar_bill_id = b.ar_bill_id