✨ 按客户区域计算毛利并完善汇总展示
This commit is contained in:
+19
-4
@@ -32,6 +32,7 @@ import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.common.utils.poi.ExcelUtil;
|
||||
// 提成功能暂停使用:import com.bms.price.domain.PrcProfitCommissionRate;
|
||||
import com.bms.price.domain.PrcProfitReportExport;
|
||||
import com.bms.price.enums.ProfitAreaRule;
|
||||
import com.bms.price.domain.vo.PrcProfitReportOrderRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportProductRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportQuery;
|
||||
@@ -58,6 +59,14 @@ public class PrcProfitReportController extends BaseController
|
||||
return getDataTable(profitReportService.selectCustomerOptions(keywords));
|
||||
}
|
||||
|
||||
/** 客户区域字典值与毛利规则的映射。 */
|
||||
@PreAuthorize("@ss.hasPermi('report:profit:list')")
|
||||
@GetMapping("/area-rules")
|
||||
public AjaxResult areaRules()
|
||||
{
|
||||
return success(profitReportService.selectAreaRules());
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('report:profit:list')")
|
||||
@GetMapping("/summary")
|
||||
public AjaxResult summary(PrcProfitReportQuery query)
|
||||
@@ -118,7 +127,7 @@ public class PrcProfitReportController extends BaseController
|
||||
try
|
||||
{
|
||||
List<PrcProfitReportOrderRow> rows = profitReportService.selectOrderRows(query);
|
||||
return finishExport(export, rows, PrcProfitReportOrderRow.class, "订单毛利报表");
|
||||
return finishExport(export, rows, PrcProfitReportOrderRow.class, "订单毛利报表", query.getProfitRule());
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
@@ -136,7 +145,7 @@ public class PrcProfitReportController extends BaseController
|
||||
try
|
||||
{
|
||||
List<PrcProfitReportProductRow> rows = profitReportService.selectProductRows(query);
|
||||
return finishExport(export, rows, PrcProfitReportProductRow.class, "产品毛利报表");
|
||||
return finishExport(export, rows, PrcProfitReportProductRow.class, "产品毛利报表", query.getProfitRule());
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
@@ -200,7 +209,8 @@ public class PrcProfitReportController extends BaseController
|
||||
return export;
|
||||
}
|
||||
|
||||
private <T> AjaxResult finishExport(PrcProfitReportExport export, List<T> rows, Class<T> rowType, String sheetName)
|
||||
private <T> AjaxResult finishExport(PrcProfitReportExport export, List<T> rows, Class<T> rowType, String sheetName,
|
||||
String profitRule)
|
||||
{
|
||||
Path temp = null;
|
||||
try
|
||||
@@ -208,7 +218,12 @@ public class PrcProfitReportController extends BaseController
|
||||
temp = Files.createTempFile("profit-report-", ".xlsx");
|
||||
try (OutputStream output = Files.newOutputStream(temp))
|
||||
{
|
||||
new ExcelUtil<T>(rowType).exportExcelToStream(rows, sheetName, output);
|
||||
ExcelUtil<T> excelUtil = new ExcelUtil<>(rowType);
|
||||
if (ProfitAreaRule.GUANGZHOU.name().equals(profitRule))
|
||||
{
|
||||
excelUtil.hideColumn("costAmount", "redlineGrossProfit", "redlineGrossMarginRate");
|
||||
}
|
||||
excelUtil.exportExcelToStream(rows, sheetName, output);
|
||||
}
|
||||
String objectKey = "report/profit/" + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + ".xlsx";
|
||||
export.setStorageKey(ossStorage.upload(temp, objectKey,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.bms.report;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.price.domain.vo.PrcProfitReportQuery;
|
||||
import com.bms.price.mapper.PrcProfitReportMapper;
|
||||
import com.bms.price.service.impl.PrcProfitReportServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
class ProfitAreaRuleTest
|
||||
{
|
||||
@Test
|
||||
void reportQueriesResolveRuleOnServerAndRejectUnknownAreas()
|
||||
{
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
PrcProfitReportMapper mapper = (PrcProfitReportMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(), new Class<?>[] { PrcProfitReportMapper.class },
|
||||
(proxy, method, args) -> {
|
||||
calls.incrementAndGet();
|
||||
return Collections.emptyList();
|
||||
});
|
||||
PrcProfitReportServiceImpl service = new PrcProfitReportServiceImpl();
|
||||
ReflectionTestUtils.setField(service, "profitReportMapper", mapper);
|
||||
|
||||
PrcProfitReportQuery query = new PrcProfitReportQuery();
|
||||
query.setCustomerArea(" GZ ");
|
||||
query.setProfitRule("SHENZHEN");
|
||||
service.selectSummary(query);
|
||||
assertEquals("GZ", query.getCustomerArea());
|
||||
assertEquals("GUANGZHOU", query.getProfitRule());
|
||||
|
||||
query.setCustomerArea("C");
|
||||
query.setProfitRule("GUANGZHOU");
|
||||
service.selectOrderRows(query);
|
||||
assertEquals("SHENZHEN", query.getProfitRule());
|
||||
service.selectProductRows(query);
|
||||
assertEquals(3, calls.get());
|
||||
|
||||
query.setCustomerArea("UNKNOWN");
|
||||
assertThrows(ServiceException.class, () -> service.selectProductRows(query));
|
||||
query.setCustomerArea("");
|
||||
assertThrows(ServiceException.class, () -> service.selectSummary(query));
|
||||
assertEquals(3, calls.get());
|
||||
assertEquals("SHENZHEN", service.selectAreaRules().get("C"));
|
||||
assertEquals("GUANGZHOU", service.selectAreaRules().get("GZ"));
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ public class PrcProfitReportQuery extends BaseEntity
|
||||
private List<String> customerCodes;
|
||||
/** 按客户当前区域筛选。 */
|
||||
private String customerArea;
|
||||
/** 服务端根据客户区域确定的计算规则。 */
|
||||
private String profitRule;
|
||||
private String customerName;
|
||||
/** 客户名称拆分后的多值查询条件。 */
|
||||
private List<String> customerNames;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.bms.price.enums;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** 客户区域字典值对应的毛利计算规则。 */
|
||||
public enum ProfitAreaRule
|
||||
{
|
||||
SHENZHEN("C"),
|
||||
GUANGZHOU("GZ");
|
||||
|
||||
private final String customerArea;
|
||||
|
||||
ProfitAreaRule(String customerArea)
|
||||
{
|
||||
this.customerArea = customerArea;
|
||||
}
|
||||
|
||||
public static ProfitAreaRule fromCustomerArea(String customerArea)
|
||||
{
|
||||
if (customerArea != null)
|
||||
{
|
||||
for (ProfitAreaRule rule : values())
|
||||
{
|
||||
if (rule.customerArea.equals(customerArea.trim()))
|
||||
{
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Map<String, String> areaRules()
|
||||
{
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (ProfitAreaRule rule : values())
|
||||
{
|
||||
result.put(rule.customerArea, rule.name());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bms.price.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
|
||||
// 提成功能暂停使用:import com.bms.price.domain.PrcProfitCommissionRate;
|
||||
import com.bms.price.domain.PrcProfitReportExport;
|
||||
@@ -14,6 +15,8 @@ public interface IPrcProfitReportService
|
||||
{
|
||||
List<BmsCustomerOption> selectCustomerOptions(String keywords);
|
||||
|
||||
Map<String, String> selectAreaRules();
|
||||
|
||||
List<PrcProfitReportOrderRow> selectOrderRows(PrcProfitReportQuery query);
|
||||
|
||||
List<PrcProfitReportProductRow> selectProductRows(PrcProfitReportQuery query);
|
||||
|
||||
+20
-1
@@ -3,6 +3,7 @@ package com.bms.price.service.impl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
// 提成功能暂停使用:import java.math.BigDecimal;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -19,6 +20,7 @@ import com.bms.price.domain.vo.PrcProfitReportOrderRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportProductRow;
|
||||
import com.bms.price.domain.vo.PrcProfitReportQuery;
|
||||
import com.bms.price.domain.vo.PrcProfitReportSummary;
|
||||
import com.bms.price.enums.ProfitAreaRule;
|
||||
import com.bms.price.mapper.PrcProfitReportMapper;
|
||||
import com.bms.price.service.IPrcProfitReportService;
|
||||
|
||||
@@ -48,6 +50,12 @@ public class PrcProfitReportServiceImpl implements IPrcProfitReportService
|
||||
return customerMapper.selectCustomerOptions(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> selectAreaRules()
|
||||
{
|
||||
return ProfitAreaRule.areaRules();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrcProfitReportOrderRow> selectOrderRows(PrcProfitReportQuery query)
|
||||
{
|
||||
@@ -105,12 +113,23 @@ public class PrcProfitReportServiceImpl implements IPrcProfitReportService
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
return;
|
||||
throw new ServiceException("请选择客户区域");
|
||||
}
|
||||
if ("COMMISSION_MISSING".equals(query.getDetailType()))
|
||||
{
|
||||
throw new ServiceException("提成功能已暂停使用");
|
||||
}
|
||||
if (query.getCustomerArea() == null || query.getCustomerArea().trim().isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择客户区域");
|
||||
}
|
||||
ProfitAreaRule rule = ProfitAreaRule.fromCustomerArea(query.getCustomerArea());
|
||||
if (rule == null)
|
||||
{
|
||||
throw new ServiceException("该客户区域未配置毛利规则");
|
||||
}
|
||||
query.setCustomerArea(query.getCustomerArea().trim());
|
||||
query.setProfitRule(rule.name());
|
||||
query.setOrderNos(splitFilterValues(query.getOrderNo(), "订单号"));
|
||||
query.setCustomerCodes(splitFilterValues(query.getCustomerCode(), "客户代码"));
|
||||
query.setCustomerNames(splitFilterValues(query.getCustomerName(), "客户名称"));
|
||||
|
||||
@@ -200,8 +200,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</where>
|
||||
</sql>
|
||||
|
||||
<!-- 红线毛利 = 产品价 - 红线价;红线毛利率 = 红线毛利 / 红线价 * 100%。汇总先逐单计算再求和。 -->
|
||||
<!-- 广州缺少有效销售价或红线价时不计算毛利;计费失败留下的双零占位也不参与计算。 -->
|
||||
<sql id="guangzhouProfitAvailable">
|
||||
i.sales_amount is not null and i.redline_amount is not null
|
||||
and not (coalesce(p.billing_fail_status, 0) in (23, 24, 25) and i.sales_amount = 0 and i.redline_amount = 0)
|
||||
</sql>
|
||||
|
||||
<sql id="profitExpressions">
|
||||
<choose>
|
||||
<when test="profitRule == 'GUANGZHOU'">
|
||||
null as cost_amount, i.redline_amount,
|
||||
null as redline_gross_profit, null as redline_gross_margin_rate,
|
||||
i.sales_amount,
|
||||
case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount - i.redline_amount else null end as sales_gross_profit,
|
||||
case when <include refid="guangzhouProfitAvailable"/> and i.sales_amount != 0
|
||||
then round((i.sales_amount - i.redline_amount) * 100 / i.sales_amount, 4)
|
||||
else null end as sales_gross_margin_rate
|
||||
</when>
|
||||
<otherwise>
|
||||
i.product_amount as cost_amount,
|
||||
i.redline_amount,
|
||||
case when i.product_amount is not null and i.redline_amount is not null
|
||||
@@ -213,11 +230,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
then i.sales_amount - i.product_amount else null end as sales_gross_profit,
|
||||
case when i.product_amount is not null and i.sales_amount is not null and i.sales_amount != 0
|
||||
then round((i.sales_amount - i.product_amount) * 100 / i.sales_amount, 4) else null end as sales_gross_margin_rate
|
||||
<!-- 提成比例与金额计算暂停使用;恢复时包含前置逗号。
|
||||
, cr.commission_rate,
|
||||
case when cr.commission_rate is not null and i.product_amount is not null and i.sales_amount is not null
|
||||
then round((i.sales_amount - i.product_amount) * cr.commission_rate / 100, 4) else null end as commission_amount
|
||||
-->
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<select id="selectOrderRows" resultMap="PrcProfitReportOrderResult">
|
||||
@@ -234,9 +248,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by p.biz_date desc, p.bill_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectProductRows" resultMap="PrcProfitReportProductResult">
|
||||
select p.product_code, p.product_name, p.country_code, p.country_name,
|
||||
i.currency, count(*) as order_count,
|
||||
<!-- 汇总和产品维度使用同一套逐单有效数据计算,毛利率按有效销售额加权。 -->
|
||||
<sql id="profitAggregateExpressions">
|
||||
<choose>
|
||||
<when test="profitRule == 'GUANGZHOU'">
|
||||
null as cost_amount,
|
||||
sum(i.redline_amount) as redline_amount,
|
||||
null as redline_gross_profit, null as redline_gross_margin_rate,
|
||||
sum(i.sales_amount) as sales_amount,
|
||||
sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount - i.redline_amount else null end) as sales_gross_profit,
|
||||
case when sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount else null end) is null
|
||||
or sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount else null end) = 0
|
||||
then null
|
||||
else round(sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount - i.redline_amount else null end) * 100 /
|
||||
sum(case when <include refid="guangzhouProfitAvailable"/>
|
||||
then i.sales_amount else null end), 4)
|
||||
end as sales_gross_margin_rate
|
||||
</when>
|
||||
<otherwise>
|
||||
sum(i.product_amount) as cost_amount,
|
||||
sum(i.redline_amount) as redline_amount,
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
@@ -266,12 +299,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end), 4)
|
||||
end as sales_gross_margin_rate
|
||||
<!-- 提成金额与未计算订单数暂停使用。
|
||||
sum(case when cr.commission_rate is not null and i.product_amount is not null and i.sales_amount is not null
|
||||
then (i.sales_amount - i.product_amount) * cr.commission_rate / 100 else null end) as commission_amount,
|
||||
sum(case when cr.commission_rate is null or i.product_amount is null or i.sales_amount is null
|
||||
then 1 else 0 end) as commission_missing_order_count,
|
||||
-->
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<select id="selectProductRows" resultMap="PrcProfitReportProductResult">
|
||||
select p.product_code, p.product_name, p.country_code, p.country_name,
|
||||
i.currency, count(*) as order_count,
|
||||
<include refid="profitAggregateExpressions"/>
|
||||
<include refid="profitReportFromAndWhere"/>
|
||||
group by p.product_code, p.product_name, p.country_code, p.country_name, i.currency
|
||||
order by p.product_code, p.country_code, i.currency
|
||||
@@ -279,41 +314,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectSummary" resultMap="PrcProfitReportSummaryResult">
|
||||
select i.currency, count(*) as order_count,
|
||||
sum(i.product_amount) as cost_amount,
|
||||
sum(i.redline_amount) as redline_amount,
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.product_amount - i.redline_amount else null end) as redline_gross_profit,
|
||||
case when sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.redline_amount else null end) is null
|
||||
or sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.redline_amount else null end) = 0
|
||||
then null
|
||||
else round(
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.product_amount - i.redline_amount else null end) * 100 /
|
||||
sum(case when i.product_amount is not null and i.redline_amount is not null
|
||||
then i.redline_amount else null end), 4)
|
||||
end as redline_gross_margin_rate,
|
||||
sum(i.sales_amount) as sales_amount,
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount - i.product_amount else null end) as sales_gross_profit,
|
||||
case when sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end) is null
|
||||
or sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end) = 0
|
||||
then null
|
||||
else round(
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount - i.product_amount else null end) * 100 /
|
||||
sum(case when i.product_amount is not null and i.sales_amount is not null
|
||||
then i.sales_amount else null end), 4)
|
||||
end as sales_gross_margin_rate
|
||||
<!-- 提成汇总计算暂停使用;恢复时包含前置逗号。
|
||||
, sum(case when cr.commission_rate is not null and i.product_amount is not null and i.sales_amount is not null
|
||||
then (i.sales_amount - i.product_amount) * cr.commission_rate / 100 else null end) as commission_amount,
|
||||
sum(case when cr.commission_rate is null or i.product_amount is null or i.sales_amount is null
|
||||
then 1 else 0 end) as commission_missing_order_count
|
||||
-->
|
||||
<include refid="profitAggregateExpressions"/>
|
||||
<include refid="profitReportFromAndWhere"/>
|
||||
<if test="arBillStatus != null and arBillStatus != ''">
|
||||
and p.ar_bill_status = #{arBillStatus}
|
||||
|
||||
@@ -19,6 +19,10 @@ export function listProfitCustomerOptions(params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function listProfitAreaRules() {
|
||||
return request({ url: '/report/profit/area-rules', method: 'get' })
|
||||
}
|
||||
|
||||
export function listProfitSummary(params) {
|
||||
return request({
|
||||
url: '/report/profit/summary',
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8" class="filter-actions-col">
|
||||
<div class="filter-item filter-actions">
|
||||
<el-button type="primary" icon="Search" :disabled="!hasCustomerArea()" @click="handleSearch">搜索</el-button>
|
||||
<el-button type="primary" icon="Search" :disabled="!hasProfitRule()" @click="handleSearch">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -78,23 +78,26 @@
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div v-if="summaryList.length" class="summary-list">
|
||||
<el-card v-for="summary in summaryList" :key="summary.currency" shadow="never" class="summary-card">
|
||||
<el-alert v-if="areaRulesFailed" title="毛利规则加载失败,请刷新页面重试" type="error" :closable="false" show-icon class="rule-alert" />
|
||||
<el-alert v-else-if="areaRules && hasCustomerArea() && !currentRule" title="该客户区域未配置毛利规则" type="warning" :closable="false" show-icon class="rule-alert" />
|
||||
|
||||
<div v-if="summaryLoaded && hasProfitRule()" class="summary-list">
|
||||
<el-card v-for="summary in summaryCards" :key="summary.currency || 'empty'" shadow="never" class="summary-card">
|
||||
<template #header>
|
||||
<span>{{ summary.currency || '未标币种' }} 汇总</span>
|
||||
<span class="summary-subtitle">订单 {{ summary.orderCount || 0 }} 条</span>
|
||||
<span>{{ summary.empty ? '汇总' : `${summary.currency || '未标币种'} 汇总` }}</span>
|
||||
<span class="summary-subtitle">订单 {{ summary.orderCount ?? '-' }} 条</span>
|
||||
</template>
|
||||
<el-row :gutter="12" class="summary-grid">
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">红线价</div><div class="metric-value">{{ money(summary.redlineAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<el-col v-if="!isGuangzhou" :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">产品价</div><div class="metric-value">{{ money(summary.costAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">销售价</div><div class="metric-value">{{ money(summary.salesAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<el-col v-if="!isGuangzhou" :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">红线毛利</div><div class="metric-value">{{ money(summary.redlineGrossProfit) }} <small>{{ rate(summary.redlineGrossMarginRate) }}</small></div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
@@ -139,14 +142,14 @@
|
||||
icon="Download"
|
||||
v-hasPermi="['report:profit:export']"
|
||||
:loading="exporting"
|
||||
:disabled="!hasCustomerArea()"
|
||||
:disabled="!hasProfitRule()"
|
||||
@click="handleExport"
|
||||
>导出当前维度</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-text-drag-copy="true" class="report-table-copy-area">
|
||||
<el-table :key="dimension" v-loading="loading" :data="rows" border stripe :row-key="rowKey">
|
||||
<el-table :key="dimension + '-' + currentRule" v-loading="loading" :data="rows" border stripe :row-key="rowKey">
|
||||
<template v-if="dimension === 'product'">
|
||||
<el-table-column prop="productCode" label="产品代码" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="180" show-overflow-tooltip />
|
||||
@@ -156,10 +159,10 @@
|
||||
<el-table-column prop="orderCount" label="订单数" width="100" align="right">
|
||||
<template #default="{ row }"><span class="success-value order-count-link" :class="{ 'is-disabled': !row.orderCount }" @click.stop="openOrderList(row)">{{ row.orderCount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossProfit" label="销售毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossMarginRate" label="销售毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
@@ -181,10 +184,10 @@
|
||||
<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="countryName" label="国家/地区" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossProfit" label="销售毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossMarginRate" label="销售毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
@@ -220,9 +223,11 @@
|
||||
<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="countryName" label="国家/地区" width="110" />
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column prop="costAmount" label="产品价" width="110" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="costAmount" label="产品价" width="110" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" width="110" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" width="110" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="isGuangzhou" prop="salesGrossProfit" label="销售毛利" width="110" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column v-if="isGuangzhou" prop="salesGrossMarginRate" label="销售毛利率" width="110" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="arBillStatus" label="应收状态" width="100" align="center"><template #default="{ row }">{{ arBillStatusLabel(row.arBillStatus) }}</template></el-table-column>
|
||||
<template #empty><el-empty description="暂无符合条件的订单" /></template>
|
||||
</el-table>
|
||||
@@ -304,7 +309,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="ProfitReport">
|
||||
import { downloadProfitExport, exportProfitOrders, exportProfitProducts, listProfitCustomerOptions, listProfitExports, listProfitOrders, listProfitProducts, listProfitSummary } from '@/api/report/profit'
|
||||
import { downloadProfitExport, exportProfitOrders, exportProfitProducts, listProfitAreaRules, listProfitCustomerOptions, listProfitExports, listProfitOrders, listProfitProducts, listProfitSummary } from '@/api/report/profit'
|
||||
// 提成功能暂停使用:import { deleteProfitCommissionRate, listProfitCommissionRates, saveProfitCommissionRate } from '@/api/report/profit'
|
||||
import { parseTime } from '@/utils/bms'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -323,7 +328,13 @@ const query = reactive({
|
||||
belongSalerCode: '',
|
||||
productCode: ''
|
||||
})
|
||||
const areaRules = ref(null)
|
||||
const areaRulesFailed = ref(false)
|
||||
const currentRule = computed(() => areaRules.value?.[String(query.customerArea)] || '')
|
||||
const isGuangzhou = computed(() => currentRule.value === 'GUANGZHOU')
|
||||
const summaryList = ref([])
|
||||
const summaryLoaded = ref(false)
|
||||
const summaryCards = computed(() => summaryList.value.length ? summaryList.value : [{ empty: true }])
|
||||
let summaryRequestVersion = 0
|
||||
const rows = ref([])
|
||||
let rowsRequestVersion = 0
|
||||
@@ -412,17 +423,23 @@ function hasCustomerArea() {
|
||||
return query.customerArea !== '' && query.customerArea !== null && query.customerArea !== undefined
|
||||
}
|
||||
|
||||
function hasProfitRule() {
|
||||
return hasCustomerArea() && !!currentRule.value
|
||||
}
|
||||
|
||||
function loadSummary() {
|
||||
if (!hasCustomerArea() || !validateMultiValueFilters(false)) return Promise.resolve()
|
||||
if (!hasProfitRule() || !validateMultiValueFilters(false)) return Promise.resolve()
|
||||
const requestVersion = ++summaryRequestVersion
|
||||
summaryLoaded.value = false
|
||||
return listProfitSummary(buildQuery(false)).then(res => {
|
||||
if (requestVersion !== summaryRequestVersion) return
|
||||
summaryList.value = res.data || []
|
||||
summaryLoaded.value = true
|
||||
})
|
||||
}
|
||||
|
||||
function loadRows() {
|
||||
if (!hasCustomerArea() || !validateMultiValueFilters()) return Promise.resolve()
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return Promise.resolve()
|
||||
const requestVersion = ++rowsRequestVersion
|
||||
const requestDimension = dimension.value
|
||||
loading.value = true
|
||||
@@ -441,6 +458,7 @@ function clearReportResults() {
|
||||
rowsRequestVersion++
|
||||
orderListRequestVersion++
|
||||
summaryList.value = []
|
||||
summaryLoaded.value = false
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
loading.value = false
|
||||
@@ -453,7 +471,7 @@ function clearReportResults() {
|
||||
|
||||
function handleSearch() {
|
||||
clearReportResults()
|
||||
if (!hasCustomerArea() || !validateMultiValueFilters()) return
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return
|
||||
pageNum.value = 1
|
||||
Promise.all([loadSummary(), loadRows()])
|
||||
}
|
||||
@@ -493,7 +511,7 @@ function openOrderList(row) {
|
||||
|
||||
function loadOrderList() {
|
||||
if (!orderListContext.value) return
|
||||
if (!hasCustomerArea() || !validateMultiValueFilters()) return
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return
|
||||
const requestVersion = ++orderListRequestVersion
|
||||
orderListLoading.value = true
|
||||
const params = {
|
||||
@@ -547,7 +565,7 @@ function arBillStatusLabel(value) {
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (!hasCustomerArea() || !validateMultiValueFilters()) return
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return
|
||||
exporting.value = true
|
||||
const exporter = dimension.value === 'product' ? exportProfitProducts : exportProfitOrders
|
||||
try {
|
||||
@@ -659,6 +677,15 @@ function handleDeleteRate(row) {
|
||||
}
|
||||
*/
|
||||
|
||||
listProfitAreaRules().then(res => {
|
||||
areaRules.value = res.data || {}
|
||||
handleSearch()
|
||||
}).catch(() => {
|
||||
areaRules.value = {}
|
||||
areaRulesFailed.value = true
|
||||
clearReportResults()
|
||||
})
|
||||
|
||||
watch(customer_area, options => {
|
||||
if (!options?.length) {
|
||||
query.customerArea = ''
|
||||
@@ -707,6 +734,10 @@ watch(customer_area, options => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rule-alert {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(560px, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user