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