✨ 完善应收账单毛利分析与提成展示
This commit is contained in:
+3
-3
@@ -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)
|
||||
|
||||
@@ -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<ArProfitScope> actorScope = new AtomicReference<>(new ArProfitScope(false, true, false, 7L, 10L));
|
||||
AtomicReference<Object[]> 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()
|
||||
{
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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<ArCommissionResult> selectResults(@Param("billId") Long billId, @Param("includeReversed") boolean includeReversed);
|
||||
List<ArCommissionSummary> selectCommissionSummary(@Param("billId") Long billId);
|
||||
List<ArCommissionResult> selectResults(@Param("billId") Long billId, @Param("includeReversed") boolean includeReversed,
|
||||
@Param("scope") ArProfitScope scope);
|
||||
List<ArCommissionSummary> selectCommissionSummary(@Param("billId") Long billId, @Param("scope") ArProfitScope scope);
|
||||
}
|
||||
|
||||
@@ -97,14 +97,17 @@ public class ArCommissionService
|
||||
|
||||
public List<ArCommissionResult> results(Long billId, boolean includeReversed)
|
||||
{
|
||||
requireFinance();
|
||||
return mapper.selectResults(billId, includeReversed);
|
||||
return mapper.selectResults(billId, includeReversed, scopeResolver.resolve());
|
||||
}
|
||||
|
||||
public List<ArCommissionSummary> summary(Long billId)
|
||||
{
|
||||
requireFinance();
|
||||
return mapper.selectCommissionSummary(billId);
|
||||
return mapper.selectCommissionSummary(billId, scopeResolver.resolve());
|
||||
}
|
||||
|
||||
public List<ArCommissionResult> exportResults(Long billId)
|
||||
{
|
||||
return mapper.selectResults(billId, false, requireFinance());
|
||||
}
|
||||
|
||||
public List<ArWriteoffRate> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<otherwise>and r.result_status != 'REVERSED' and b.status = 'WRITTEN_OFF' and b.generation_status = 'READY'</otherwise>
|
||||
</choose>
|
||||
<if test="billId != null">and r.ar_bill_id = #{billId}</if>
|
||||
<include refid="com.bms.ar.mapper.ArProfitMapper.scopeCondition"/>
|
||||
order by r.result_id desc
|
||||
</select>
|
||||
<select id="selectCommissionSummary" resultMap="commissionSummary">
|
||||
@@ -120,6 +121,7 @@
|
||||
from ar_profit_commission_result r join ar_bill b on b.ar_bill_id = r.ar_bill_id
|
||||
where r.result_status != 'REVERSED' and b.status = 'WRITTEN_OFF' and b.generation_status = 'READY'
|
||||
<if test="billId != null">and r.ar_bill_id = #{billId}</if>
|
||||
<include refid="com.bms.ar.mapper.ArProfitMapper.scopeCondition"/>
|
||||
group by r.sales_user_id, r.currency
|
||||
order by sales_user_name, r.currency
|
||||
</select>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<mapper namespace="com.bms.ar.mapper.ArProfitMapper">
|
||||
<resultMap id="row" type="com.bms.ar.profit.ArProfitRow">
|
||||
<id property="arBillId" column="ar_bill_id"/>
|
||||
<result property="billNo" column="bill_no"/><result property="billDate" column="bill_date"/>
|
||||
<result property="billNo" column="bill_no"/><result property="billName" column="bill_name"/>
|
||||
<result property="billDate" column="bill_date"/>
|
||||
<result property="customerCode" column="customer_code"/><result property="customerName" column="customer_name"/>
|
||||
<result property="customerArea" column="customer_area"/>
|
||||
<result property="salesUserName" column="sales_user_name"/><result property="currency" column="currency"/>
|
||||
@@ -16,6 +17,7 @@
|
||||
<result property="currency" column="currency"/><result property="billCount" column="bill_count"/>
|
||||
<result property="salesAmount" column="sales_amount"/><result property="productAmount" column="product_amount"/>
|
||||
<result property="salesGrossProfit" column="sales_gross_profit"/><result property="salesGrossMarginRate" column="sales_gross_margin_rate"/>
|
||||
<result property="commissionAmount" column="commission_amount"/>
|
||||
<result property="writtenOffAmount" column="written_off_amount"/><result property="unwrittenAmount" column="unwritten_amount"/>
|
||||
</resultMap>
|
||||
<resultMap id="detail" type="com.bms.ar.domain.ArBillDetail">
|
||||
@@ -62,7 +64,7 @@
|
||||
<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()"/>
|
||||
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 (<include refid="billRows"/>) 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 (<include refid="billRows"/>) 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'
|
||||
<include refid="billConditions"/>
|
||||
and b.status = 'WRITTEN_OFF'
|
||||
group by b.currency
|
||||
) commission on commission.currency = t.currency
|
||||
group by t.currency order by t.currency
|
||||
</select>
|
||||
<select id="selectSalesOptions" resultType="com.bms.ar.profit.ArProfitSalesOption">
|
||||
select u.user_id as userId, u.nick_name as name, u.user_name as code
|
||||
|
||||
Reference in New Issue
Block a user