diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/ArProfitController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/ArProfitController.java index 68752349..a23c3977 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/ArProfitController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/report/ArProfitController.java @@ -99,7 +99,7 @@ public class ArProfitController extends BaseController return success(); } - @PreAuthorize("@ss.hasPermi('report:arProfit:commission')") + @PreAuthorize("@ss.hasPermi('report:arProfit:list')") @GetMapping("/commissions") public TableDataInfo commissions(@RequestParam(required = false) Long billId, @RequestParam(defaultValue = "false") boolean includeReversed) @@ -108,7 +108,7 @@ public class ArProfitController extends BaseController return getDataTable(commissions.results(billId, includeReversed)); } - @PreAuthorize("@ss.hasPermi('report:arProfit:commission')") + @PreAuthorize("@ss.hasPermi('report:arProfit:list')") @GetMapping("/commissions/summary") public AjaxResult commissionSummary(@RequestParam(required = false) Long billId) { return success(commissions.summary(billId)); } @@ -121,7 +121,7 @@ public class ArProfitController extends BaseController @PostMapping("/commissions/export") public void exportCommissions(HttpServletResponse response, @RequestParam(required = false) Long billId) { - new ExcelUtil<>(ArCommissionResult.class).exportExcel(response, commissions.results(billId, false), "应收账单提成明细"); + new ExcelUtil<>(ArCommissionResult.class).exportExcel(response, commissions.exportResults(billId), "应收账单提成明细"); } @Log(title = "应收账单毛利分析", businessType = BusinessType.EXPORT) diff --git a/bms-back/bms-admin/src/test/java/com/bms/report/ArCommissionServiceTest.java b/bms-back/bms-admin/src/test/java/com/bms/report/ArCommissionServiceTest.java index 62fa5351..1fa6ffaf 100644 --- a/bms-back/bms-admin/src/test/java/com/bms/report/ArCommissionServiceTest.java +++ b/bms-back/bms-admin/src/test/java/com/bms/report/ArCommissionServiceTest.java @@ -2,6 +2,8 @@ package com.bms.report; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.lang.reflect.Proxy; import java.math.BigDecimal; @@ -15,9 +17,49 @@ import com.bms.ar.mapper.ArCommissionMapper; import com.bms.ar.profit.ArCommissionRate; import com.bms.ar.profit.ArCommissionResult; import com.bms.ar.profit.ArCommissionService; +import com.bms.ar.profit.ArProfitScope; +import com.bms.ar.profit.ArProfitScopeResolver; +import com.bms.common.exception.ServiceException; class ArCommissionServiceTest { + @Test + void commissionReadsUseServerResolvedScopeAndManagementRemainsRestricted() + { + AtomicReference actorScope = new AtomicReference<>(new ArProfitScope(false, true, false, 7L, 10L)); + AtomicReference selected = new AtomicReference<>(); + ArProfitScopeResolver resolver = new ArProfitScopeResolver() { + @Override public ArProfitScope resolve() { return actorScope.get(); } + }; + ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] { ArCommissionMapper.class }, (proxy, method, args) -> { + if ("selectResults".equals(method.getName()) || "selectCommissionSummary".equals(method.getName())) { + selected.set(args.clone()); + return List.of(); + } + throw new AssertionError(method.getName()); + }); + ArCommissionService service = new ArCommissionService(mapper, resolver, null, null); + + service.results(999L, true); + assertEquals(999L, selected.get()[0]); + assertSame(actorScope.get(), selected.get()[2]); + service.summary(999L); + assertSame(actorScope.get(), selected.get()[1]); + assertThrows(ServiceException.class, service::selectRates); + assertThrows(ServiceException.class, service::missingRates); + assertThrows(ServiceException.class, () -> service.exportResults(null)); + + actorScope.set(new ArProfitScope(false, false, true, 8L, 10L)); + service.results(null, false); + assertSame(actorScope.get(), selected.get()[2]); + assertThrows(ServiceException.class, service::selectRates); + + actorScope.set(new ArProfitScope(true, false, false, 9L, 10L)); + service.exportResults(null); + assertSame(actorScope.get(), selected.get()[2]); + } + @Test void snapshotsTheRateOnceAtWriteoffAndPreservesMissingRates() { 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 9c5fbb0b..f40d5c05 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,11 +14,58 @@ import com.bms.ar.profit.ArCommissionResult; class ArProfitMapperSqlTest { + @Test + void commissionInReportSummaryUsesBillFiltersAndDataScope() throws Exception + { + Configuration configuration = new Configuration(); + String resource = "mapper/ar/ArProfitMapper.xml"; + try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource)) + { + new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse(); + } + var statement = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSummary"); + ArProfitQuery query = new ArProfitQuery(); + query.setCustomerId(5L); + query.setBillNo("AR123"); + query.setStatus("WRITTEN_OFF"); + query.setSalesUserId(8L); + query.setBeginBillDate(new java.util.Date(0)); + query.setEndBillDate(new java.util.Date()); + String sql = statement.getBoundSql(Map.of("query", query, + "scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql(); + int commissionStart = sql.indexOf("from ar_bill b join ar_profit_commission_result r"); + assertTrue(commissionStart >= 0); + String commission = sql.substring(commissionStart); + assertTrue(commission.contains("r.result_status = 'READY'")); + assertTrue(commission.contains("r.currency = b.currency")); + assertTrue(commission.contains("b.status = 'WRITTEN_OFF'")); + assertTrue(commission.contains("b.profit_sales_user_id = ?")); + assertTrue(commission.contains("b.customer_id = ?")); + assertTrue(commission.contains("b.bill_no like")); + assertTrue(commission.contains("b.status = ?")); + assertTrue(commission.contains("b.bill_date >= ?")); + assertTrue(commission.contains("b.bill_date <= ?")); + assertTrue(commission.contains("group by b.currency")); + + for (ArProfitScope scope : new ArProfitScope[] { + new ArProfitScope(false, false, true, 8L, 10L), + new ArProfitScope(false, false, false, 8L, 10L), + new ArProfitScope(true, false, false, 8L, 10L) }) + { + String scopedSql = statement.getBoundSql(Map.of("query", new ArProfitQuery(), "scope", scope)).getSql(); + String scopedCommission = scopedSql.substring(scopedSql.indexOf("from ar_bill b join ar_profit_commission_result r")); + if (scope.department()) assertTrue(scopedCommission.contains("find_in_set")); + else if (scope.all()) assertFalse(scopedCommission.contains("b.profit_sales_user_id = ?")); + else assertTrue(scopedCommission.contains("and 1 = 0")); + } + } + @Test void parsesReportAndCommissionMappersAndScopesEachReportQuery() throws Exception { Configuration configuration = new Configuration(); - for (String resource : new String[] { "mapper/ar/ArProfitMapper.xml", "mapper/ar/ArCommissionMapper.xml" }) + // 提成 Mapper 先加载时也必须能解析引用的报表权限片段。 + for (String resource : new String[] { "mapper/ar/ArCommissionMapper.xml", "mapper/ar/ArProfitMapper.xml" }) { try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource)) { @@ -40,11 +87,32 @@ class ArProfitMapperSqlTest String bills = configuration.getMappedStatement("com.bms.ar.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")); assertFalse(bills.contains("order_no")); String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary") - .getBoundSql(Map.of("billId", 9L)).getSql(); + .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); + 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, + "scope", new ArProfitScope(false, false, true, 8L, 10L))).getSql(); + String denied = statement.getBoundSql(Map.of("includeReversed", false, + "scope", new ArProfitScope(false, false, false, 8L, 10L))).getSql(); + String all = statement.getBoundSql(Map.of("includeReversed", false, + "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql(); + assertTrue(self.contains("b.profit_sales_user_id = ?")); + assertTrue(self.contains("r.ar_bill_id = ?")); + assertFalse(self.contains("find_in_set")); + assertTrue(department.contains("u.user_id = b.profit_sales_user_id")); + assertTrue(department.contains("find_in_set")); + assertTrue(denied.contains("and 1 = 0")); + assertFalse(all.contains("b.profit_sales_user_id = ?")); + assertFalse(all.contains("find_in_set")); + } var salesSql = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSalesOptions") .getBoundSql(Map.of("keyword", "sales")); assertTrue(salesSql.getSql().contains("from sys_user u")); diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArCommissionMapper.java b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArCommissionMapper.java index d602a708..1a77e51e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArCommissionMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/mapper/ArCommissionMapper.java @@ -7,6 +7,7 @@ import com.bms.ar.profit.ArCommissionRate; import com.bms.ar.profit.ArWriteoffRate; import com.bms.ar.profit.ArCommissionResult; import com.bms.ar.profit.ArCommissionSummary; +import com.bms.ar.profit.ArProfitScope; public interface ArCommissionMapper { @@ -25,6 +26,7 @@ public interface ArCommissionMapper int countMissingRates(@Param("billId") Long billId); int confirmMissingRate(@Param("writeoffId") Long writeoffId, @Param("rate") java.math.BigDecimal rate, @Param("operator") String operator, @Param("remark") String remark); - List selectResults(@Param("billId") Long billId, @Param("includeReversed") boolean includeReversed); - List selectCommissionSummary(@Param("billId") Long billId); + List selectResults(@Param("billId") Long billId, @Param("includeReversed") boolean includeReversed, + @Param("scope") ArProfitScope scope); + List selectCommissionSummary(@Param("billId") Long billId, @Param("scope") ArProfitScope scope); } diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArCommissionService.java b/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArCommissionService.java index 8dfe1acc..3447c988 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArCommissionService.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArCommissionService.java @@ -97,14 +97,17 @@ public class ArCommissionService public List results(Long billId, boolean includeReversed) { - requireFinance(); - return mapper.selectResults(billId, includeReversed); + return mapper.selectResults(billId, includeReversed, scopeResolver.resolve()); } public List summary(Long billId) { - requireFinance(); - return mapper.selectCommissionSummary(billId); + return mapper.selectCommissionSummary(billId, scopeResolver.resolve()); + } + + public List exportResults(Long billId) + { + return mapper.selectResults(billId, false, requireFinance()); } public List writeoffRates(Long billId) @@ -141,8 +144,10 @@ public class ArCommissionService } } - private void requireFinance() + private ArProfitScope requireFinance() { - if (!scopeResolver.resolve().all()) throw new ServiceException("仅财务和系统管理员可查看或维护提成"); + ArProfitScope scope = scopeResolver.resolve(); + if (!scope.all()) throw new ServiceException("仅财务和系统管理员可使用提成管理功能"); + return scope; } } diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitRow.java b/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitRow.java index 236389ef..a95aaa22 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitRow.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitRow.java @@ -11,6 +11,7 @@ public class ArProfitRow { private Long arBillId; @Excel(name = "账单号", width = 20) private String billNo; + @Excel(name = "账单名称", width = 24) private String billName; @Excel(name = "客户代码", width = 16) private String customerCode; @Excel(name = "客户名称", width = 24) private String customerName; private String customerArea; diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitSummary.java b/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitSummary.java index 21dc2aa9..54c10e1f 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitSummary.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/profit/ArProfitSummary.java @@ -12,6 +12,7 @@ public class ArProfitSummary private BigDecimal productAmount; private BigDecimal salesGrossProfit; private BigDecimal salesGrossMarginRate; + private BigDecimal commissionAmount; private BigDecimal writtenOffAmount; private BigDecimal unwrittenAmount; } diff --git a/bms-back/bms-system/src/main/resources/mapper/ar/ArCommissionMapper.xml b/bms-back/bms-system/src/main/resources/mapper/ar/ArCommissionMapper.xml index a9aec461..869e9086 100644 --- a/bms-back/bms-system/src/main/resources/mapper/ar/ArCommissionMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/ar/ArCommissionMapper.xml @@ -104,6 +104,7 @@ and r.result_status != 'REVERSED' and b.status = 'WRITTEN_OFF' and b.generation_status = 'READY' and r.ar_bill_id = #{billId} + order by r.result_id desc diff --git a/bms-back/bms-system/src/main/resources/mapper/ar/ArProfitMapper.xml b/bms-back/bms-system/src/main/resources/mapper/ar/ArProfitMapper.xml index dd27d77a..0bd971eb 100644 --- a/bms-back/bms-system/src/main/resources/mapper/ar/ArProfitMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/ar/ArProfitMapper.xml @@ -3,7 +3,8 @@ - + + @@ -16,6 +17,7 @@ + @@ -62,7 +64,7 @@ - select b.ar_bill_id, b.bill_no, b.bill_date, b.customer_code, b.customer_name, + select b.ar_bill_id, b.bill_no, b.bill_name, b.bill_date, b.customer_code, b.customer_name, 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, @@ -118,8 +120,18 @@ case when sum(case when t.profit_incomplete = 0 then t.sales_amount else null end) = 0 then null else round(sum(t.sales_gross_profit) * 100 / sum(case when t.profit_incomplete = 0 then t.sales_amount else null end), 4) end as sales_gross_margin_rate, - sum(t.written_off_amount) as written_off_amount, sum(t.unwritten_amount) as unwritten_amount - from () t group by t.currency order by t.currency + sum(t.written_off_amount) as written_off_amount, sum(t.unwritten_amount) as unwritten_amount, + max(commission.commission_amount) as commission_amount + from () t + left join ( + select b.currency, sum(r.commission_amount) as commission_amount + from ar_bill b join ar_profit_commission_result r + on r.ar_bill_id = b.ar_bill_id and r.currency = b.currency and r.result_status = 'READY' + + and b.status = 'WRITTEN_OFF' + group by b.currency + ) commission on commission.currency = t.currency + group by t.currency order by t.currency