Merge branch 'test' into feauture-20261104
# Conflicts: # bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcOrderBillMapper.java
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
package com.bms.web.controller.report;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.bms.ar.profit.ArCommissionRate;
|
||||
import com.bms.ar.profit.ArCommissionService;
|
||||
import com.bms.ar.profit.ArProfitQuery;
|
||||
import com.bms.ar.profit.ArProfitService;
|
||||
import com.bms.ar.profit.ArWriteoffRate;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import com.bms.ar.profit.ArProfitRow;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.common.utils.poi.ExcelUtil;
|
||||
import com.bms.price.enums.ProfitAreaRule;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/report/ar-profit")
|
||||
public class ArProfitController extends BaseController
|
||||
{
|
||||
private final ArProfitService report;
|
||||
private final ArCommissionService commissions;
|
||||
|
||||
public ArProfitController(ArProfitService report, ArCommissionService commissions)
|
||||
{
|
||||
this.report = report;
|
||||
this.commissions = commissions;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/summary")
|
||||
public AjaxResult summary(ArProfitQuery query) { return success(report.selectSummary(query)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/sales-options")
|
||||
public AjaxResult salesOptions(@RequestParam(required = false) String keyword) { return success(report.selectSalesOptions(keyword)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/area-rules")
|
||||
public AjaxResult areaRules() { return success(ProfitAreaRule.areaRules()); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/orders/{detailId}/fees")
|
||||
public AjaxResult fees(@PathVariable Long detailId) { return success(report.selectFees(detailId)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/bills/{billId}/details")
|
||||
public TableDataInfo billDetails(@PathVariable Long billId)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(report.selectBillDetails(billId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
|
||||
@GetMapping("/bills")
|
||||
public TableDataInfo bills(ArProfitQuery query)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(report.selectBills(query));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/rates")
|
||||
public AjaxResult rates() { return success(commissions.selectRates()); }
|
||||
|
||||
@Log(title = "账单毛利提成比例", businessType = BusinessType.INSERT)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@PostMapping("/rates")
|
||||
public AjaxResult addRate(@RequestBody ArCommissionRate rate)
|
||||
{
|
||||
commissions.addRate(rate);
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/writeoffs/missing-rates")
|
||||
public TableDataInfo missingRates()
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(commissions.missingRates());
|
||||
}
|
||||
|
||||
@Log(title = "历史核销提成比例确认", businessType = BusinessType.UPDATE)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@PostMapping("/writeoffs/confirm-rate")
|
||||
public AjaxResult confirmRate(@RequestBody ArWriteoffRate input)
|
||||
{
|
||||
commissions.confirmMissingRate(input);
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/commissions")
|
||||
public TableDataInfo commissions(@RequestParam(required = false) Long billId,
|
||||
@RequestParam(defaultValue = "false") boolean includeReversed)
|
||||
{
|
||||
startPage();
|
||||
return getDataTable(commissions.results(billId, includeReversed));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/commissions/summary")
|
||||
public AjaxResult commissionSummary(@RequestParam(required = false) Long billId) { return success(commissions.summary(billId)); }
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@GetMapping("/commissions/{billId}/writeoffs")
|
||||
public AjaxResult commissionWriteoffs(@PathVariable Long billId) { return success(commissions.writeoffRates(billId)); }
|
||||
|
||||
@Log(title = "应收账单提成明细", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
|
||||
@PostMapping("/commissions/export")
|
||||
public void exportCommissions(HttpServletResponse response, @RequestParam(required = false) Long billId)
|
||||
{
|
||||
new ExcelUtil<>(ArCommissionResult.class).exportExcel(response, commissions.results(billId, false), "应收账单提成明细");
|
||||
}
|
||||
|
||||
@Log(title = "应收账单毛利分析", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('report:arProfit:export')")
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, ArProfitQuery query)
|
||||
{
|
||||
ExcelUtil<ArProfitRow> excel = new ExcelUtil<>(ArProfitRow.class);
|
||||
excel.exportExcel(response, report.selectBills(query), "应收账单毛利分析");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.domain.ArBalanceWriteoff;
|
||||
import com.bms.ar.domain.ArBill;
|
||||
import com.bms.ar.mapper.ArCommissionMapper;
|
||||
import com.bms.ar.profit.ArCommissionRate;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.ar.profit.ArCommissionService;
|
||||
|
||||
class ArCommissionServiceTest
|
||||
{
|
||||
@Test
|
||||
void snapshotsTheRateOnceAtWriteoffAndPreservesMissingRates()
|
||||
{
|
||||
List<String> calls = new ArrayList<>();
|
||||
AtomicReference<Object[]> inserted = new AtomicReference<>();
|
||||
AtomicReference<ArCommissionRate> effective = new AtomicReference<>();
|
||||
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
|
||||
calls.add(method.getName());
|
||||
return switch (method.getName()) {
|
||||
case "lockSalesUser" -> 7L;
|
||||
case "selectEffective" -> effective.get();
|
||||
case "insertWriteoffRate" -> { inserted.set(args.clone()); yield 1; }
|
||||
default -> throw new AssertionError(method.getName());
|
||||
};
|
||||
});
|
||||
ArCommissionService service = new ArCommissionService(mapper, null, null, null);
|
||||
ArBill bill = new ArBill();
|
||||
bill.setProfitSalesUserId(7L);
|
||||
ArBalanceWriteoff writeoff = new ArBalanceWriteoff();
|
||||
writeoff.setWriteoffId(18L);
|
||||
ArCommissionRate rate = new ArCommissionRate();
|
||||
rate.setRateId(3L);
|
||||
rate.setRate(new BigDecimal("10"));
|
||||
effective.set(rate);
|
||||
service.snapshotWriteoff(bill, writeoff);
|
||||
assertEquals(List.of("lockSalesUser", "selectEffective", "insertWriteoffRate"), calls);
|
||||
assertEquals(3L, inserted.get()[2]);
|
||||
assertEquals(new BigDecimal("10"), inserted.get()[3]);
|
||||
|
||||
calls.clear();
|
||||
effective.set(null);
|
||||
service.snapshotWriteoff(bill, writeoff);
|
||||
assertNull(inserted.get()[2]);
|
||||
assertNull(inserted.get()[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void savesOnlyCalculatedAmountsForReadyRows()
|
||||
{
|
||||
ArCommissionResult ready = new ArCommissionResult();
|
||||
ready.setResultStatus("READY");
|
||||
ready.setGrossProfit(new BigDecimal("-40"));
|
||||
ready.setWeightedRate(new BigDecimal("1000"));
|
||||
ready.setBillAmount(new BigDecimal("100"));
|
||||
ArCommissionResult missing = new ArCommissionResult();
|
||||
missing.setResultStatus("MISSING_PRICE");
|
||||
List<ArCommissionResult> saved = new ArrayList<>();
|
||||
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
|
||||
if ("selectSettlementInputs".equals(method.getName())) return List.of(ready, missing);
|
||||
if ("insertResults".equals(method.getName())) {
|
||||
@SuppressWarnings("unchecked") List<ArCommissionResult> batch = (List<ArCommissionResult>) args[0];
|
||||
saved.addAll(batch);
|
||||
return batch.size();
|
||||
}
|
||||
throw new AssertionError(method.getName());
|
||||
});
|
||||
new ArCommissionService(mapper, null, null, null).createFinalResults(1L, "finance");
|
||||
assertEquals(new BigDecimal("-4.00"), saved.get(0).getCommissionAmount());
|
||||
assertNull(saved.get(1).getCommissionAmount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.profit.ArProfitQuery;
|
||||
import com.bms.ar.profit.ArProfitScope;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
|
||||
class ArProfitMapperSqlTest
|
||||
{
|
||||
@Test
|
||||
void parsesReportAndCommissionMappersAndScopesEachReportQuery() throws Exception
|
||||
{
|
||||
Configuration configuration = new Configuration();
|
||||
for (String resource : new String[] { "mapper/ar/ArProfitMapper.xml", "mapper/ar/ArCommissionMapper.xml" })
|
||||
{
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource))
|
||||
{
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
}
|
||||
for (String id : new String[] { "selectBills", "selectSummary", "selectBillDetails", "selectFees" })
|
||||
{
|
||||
String name = "com.bms.ar.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(
|
||||
"query", new ArProfitQuery(), "scope", new ArProfitScope(false, false, true, 8L, 10L))).getSql();
|
||||
assertTrue(self.contains("b.profit_sales_user_id = ?"));
|
||||
assertFalse(self.contains("find_in_set"));
|
||||
assertTrue(manager.contains("find_in_set"));
|
||||
assertTrue(manager.contains("b.status in"));
|
||||
}
|
||||
ArProfitQuery orderFilter = new ArProfitQuery();
|
||||
orderFilter.setOrderNo("ORDER-1");
|
||||
String bills = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectBills")
|
||||
.getBoundSql(Map.of("query", orderFilter, "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
|
||||
assertTrue(bills.contains("from ar_bill b"));
|
||||
assertTrue(bills.contains("exists (select 1 from ar_bill_detail matched"));
|
||||
assertFalse(bills.contains("and d.order_no like"));
|
||||
String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary")
|
||||
.getBoundSql(Map.of("billId", 9L)).getSql();
|
||||
assertTrue(billCommission.contains("r.ar_bill_id = ?"));
|
||||
assertTrue(billCommission.contains("then null"));
|
||||
var salesSql = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSalesOptions")
|
||||
.getBoundSql(Map.of("keyword", "sales"));
|
||||
assertTrue(salesSql.getSql().contains("from sys_user u"));
|
||||
assertTrue(salesSql.getSql().contains("u.nick_name like"));
|
||||
assertTrue(salesSql.getSql().contains("u.user_name like"));
|
||||
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")
|
||||
.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")
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.bms.ar.profit.ArProfitScopeResolver;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
|
||||
class ArProfitScopeResolverTest
|
||||
{
|
||||
private static final String PERMISSION = "report:arProfit:list";
|
||||
|
||||
private LoginUser actor(String... keys)
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(99L);
|
||||
user.setStatus("0");
|
||||
user.setDelFlag("0");
|
||||
user.setRoles(java.util.Arrays.stream(keys).map(key -> {
|
||||
SysRole role = new SysRole();
|
||||
role.setRoleKey(key);
|
||||
role.setStatus("0");
|
||||
role.setPermissions(Set.of(PERMISSION));
|
||||
return role;
|
||||
}).toList());
|
||||
return new LoginUser(99L, 10L, user, Set.of(PERMISSION));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowedRolesHaveOnlyTheirApprovedScope()
|
||||
{
|
||||
ArProfitScopeResolver resolver = new ArProfitScopeResolver();
|
||||
var sales = resolver.resolve(actor("ORA_6041"), PERMISSION);
|
||||
assertTrue(sales.self());
|
||||
assertFalse(sales.all());
|
||||
var manager = resolver.resolve(actor("ORA_6040"), PERMISSION);
|
||||
assertTrue(manager.department());
|
||||
assertFalse(manager.all());
|
||||
assertTrue(resolver.resolve(actor("ORA_6042"), PERMISSION).all());
|
||||
assertTrue(resolver.resolve(actor("ORA_6043"), PERMISSION).all());
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("ORA_6163"), PERMISSION));
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("UNKNOWN"), PERMISSION));
|
||||
assertThrows(ServiceException.class, () -> resolver.resolve(actor("ORA_6042"), "other:permission"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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.assertThrows;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.bms.ar.profit.CommissionCalculator;
|
||||
import com.bms.ar.profit.ArCommissionResult;
|
||||
import com.bms.ar.profit.CommissionCalculator.Writeoff;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CommissionCalculatorTest
|
||||
{
|
||||
private static BigDecimal n(String value) { return new BigDecimal(value); }
|
||||
|
||||
@Test
|
||||
void waitsForFullSettlementAndUsesEachWriteoffRate()
|
||||
{
|
||||
var first = new Writeoff(n("50"), n("10"));
|
||||
var partial = CommissionCalculator.calculate(n("100"), n("20"), List.of(first));
|
||||
assertEquals(n("1.00"), partial.estimated());
|
||||
assertNull(partial.formal());
|
||||
var complete = CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(first, new Writeoff(n("50"), n("20"))));
|
||||
assertEquals(n("3.00"), complete.formal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deductsNegativeProfitAndKeepsMissingRatesUnknown()
|
||||
{
|
||||
assertEquals(n("-4.00"), CommissionCalculator.calculate(n("100"), n("-40"),
|
||||
List.of(new Writeoff(n("100"), n("10")))).formal());
|
||||
assertNull(CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(new Writeoff(n("100"), null))).formal());
|
||||
assertThrows(IllegalArgumentException.class, () -> CommissionCalculator.calculate(n("100"), n("20"),
|
||||
List.of(new Writeoff(n("101"), n("10")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocatesRoundingRemainderAcrossOrderResults()
|
||||
{
|
||||
List<ArCommissionResult> orders = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
ArCommissionResult order = new ArCommissionResult();
|
||||
order.setResultStatus("READY");
|
||||
order.setBillAmount(n("100"));
|
||||
order.setGrossProfit(n("0.05"));
|
||||
order.setWeightedRate(n("1000"));
|
||||
orders.add(order);
|
||||
}
|
||||
CommissionCalculator.allocateFinalAmounts(orders);
|
||||
assertEquals(n("0.01"), orders.get(0).getCommissionAmount());
|
||||
assertEquals(n("0.00"), orders.get(1).getCommissionAmount());
|
||||
assertEquals(n("0.01"), orders.get(2).getCommissionAmount());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user