4 Commits

Author SHA1 Message Date
wuweihua 2632e5aa78 渠道管理增加应收关联 2026-11-09 17:27:21 +08:00
liuminghui ace1eb06c0 去除计费列表的多余按钮 2026-11-09 16:15:55 +08:00
liuminghui f31bb4c54d Merge remote-tracking branch 'origin/test' into test 2026-11-09 16:00:23 +08:00
caojia f2d27f3502 🔥 移除应收毛利分析冗余展示和筛选 2026-11-09 15:58:14 +08:00
21 changed files with 182 additions and 94 deletions
@@ -114,17 +114,18 @@ public class PayProductController extends BaseController
return success(payProductService.selectTemplateOptions(payType));
}
/** 渠道当前价格副本 */
/** 渠道当前价格副本;payType 默认应付 AP */
@PreAuthorize("@ss.hasAnyPermi('price:pay:query,data:serviceChannel:query,data:serviceChannel:edit')")
@GetMapping("/channel/{channelId}")
public AjaxResult channelPrice(@PathVariable String channelId)
public AjaxResult channelPrice(@PathVariable String channelId,
@RequestParam(value = "payType", required = false) String payType)
{
return success(payProductService.selectChannelPrice(channelId));
return success(payProductService.selectChannelPrice(channelId, payType));
}
/** 渠道只绑定产品编号,以后按有效期取价 */
/** 渠道只绑定产品编号,以后按有效期取价;支持应收 AR 与应付 AP */
@PreAuthorize("@ss.hasAnyPermi('price:pay:edit,data:serviceChannel:edit')")
@Log(title = "渠道应付产品", businessType = BusinessType.UPDATE)
@Log(title = "渠道产品绑定", businessType = BusinessType.UPDATE)
@PutMapping("/channel/{channelId}/bind")
public AjaxResult bindChannel(@PathVariable String channelId, @RequestBody ChannelProductMap link)
{
@@ -135,7 +136,7 @@ public class PayProductController extends BaseController
return success();
}
payProductService.bindChannel(channelId, link.getProductId(), link.getPriceSetId(), payType);
return success(payProductService.selectChannelPrice(channelId));
return success(payProductService.selectChannelPrice(channelId, payType));
}
/** 对照两个价格版本 */
@@ -37,13 +37,10 @@ class ArProfitMapperSqlTest
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();
.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("exists (select 1 from ar_bill_detail matched"));
assertFalse(bills.contains("and d.order_no like"));
assertFalse(bills.contains("order_no"));
String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary")
.getBoundSql(Map.of("billId", 9L)).getSql();
assertTrue(billCommission.contains("r.ar_bill_id = ?"));
@@ -41,8 +41,6 @@ public class ArBill extends BaseEntity {
private Long profitSalesUserId;
private String profitSalesUserCode;
private String profitSalesUserName;
/** NEW=生成时固化;BACKFILLED=历史补录,来源为补录时可取得的数据。 */
private String profitSnapshotSource;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd")
/** 账单所属期开始日期,导出模板展示用。 */
@@ -9,7 +9,6 @@ public class ArProfitQuery
{
private Long customerId;
private String billNo;
private String orderNo;
private String status;
private Long salesUserId;
private Long arBillId;
@@ -14,7 +14,6 @@ public class ArProfitRow
@Excel(name = "客户代码", width = 16) private String customerCode;
@Excel(name = "客户名称", width = 24) private String customerName;
private String customerArea;
@Excel(name = "归属来源", readConverterExp = "NEW=生成时快照,BACKFILLED=历史补录", width = 14) private String snapshotSource;
@Excel(name = "销售员", width = 16) private String salesUserName;
@Excel(name = "币种", width = 10) private String currency;
@Excel(name = "账单状态", readConverterExp = "CONFIRMED=已确认,PARTIAL_WRITTEN_OFF=部分核销,WRITTEN_OFF=已核销", width = 14) private String status;
@@ -8,7 +8,6 @@ public class ArProfitSummary
{
private String currency;
private Long billCount;
private Long invalidBillCount;
private BigDecimal salesAmount;
private BigDecimal productAmount;
private BigDecimal salesGrossProfit;
@@ -449,7 +449,6 @@ public class ArBillServiceImpl implements IArBillService {
String customerName = StringUtils.trim(request.getCustomerName());
bill.setCustomerCode(customerCode == null ? "" : customerCode);
bill.setCustomerName(customerName == null ? "" : customerName);
bill.setProfitSnapshotSource("NEW");
// 固定账单生成时的客户归属;无法确定的用户保持为空,由财务核对。
BmsCustomer snapshotCustomer = customerMapper.selectBmsCustomerByCustomerId(request.getCustomerId());
if (snapshotCustomer != null) {
@@ -15,6 +15,9 @@ public class ChannelProductMap
/** 渠道表主键 srv_service_channel.id */
private String channelId;
/** 渠道编码,对应 srv_service_channel.channel_code */
private String channelCode;
private Long productId;
/** 应收套餐 prc_price_set.price_set_id */
@@ -22,7 +22,7 @@ public interface PayProductMapper
Long selectTemplateId(@Param("dictValue") String dictValue);
List<PayProduct> selectTemplateOptions();
List<PayProduct> selectTemplateOptions(@Param("payType") String payType);
Integer selectMaxVersion(@Param("originId") Long originId);
@@ -47,10 +47,13 @@ public interface IPayProductService
/** 公共模板下拉。payType 为空时按应付 AP */
List<PayProduct> selectTemplateOptions(String payType);
/** 渠道绑定的产品。返回的 originId 是不变的产品编号,价格是当前有效版 */
/** 渠道绑定的产品。返回的 originId 是不变的产品编号,价格是当前有效版;默认应付 AP */
PayProduct selectChannelPrice(String channelId);
/** 只绑定产品编号,不产生新版本。payType 为空时按应付 AP */
/** 按收付类型查询渠道绑定产品,payType 为空时按应付 AP */
PayProduct selectChannelPrice(String channelId, String payType);
/** 只绑定产品编号,不产生新版本;同时落 channel_code。payType 为空时按应付 AP */
void bindChannel(String channelId, Long productId, Long priceSetId, String payType);
/** 取消渠道上指定收付类型的产品。payType 为空时按应付 AP */
@@ -35,6 +35,8 @@ import com.bms.price.domain.PayProductTerm;
import com.bms.price.domain.PaySaveCommand;
import com.bms.price.domain.PaySurcharge;
import com.bms.price.domain.PayWeightBand;
import com.bms.masterdata.domain.ServiceChannel;
import com.bms.masterdata.mapper.ServiceChannelMapper;
import com.bms.price.mapper.ChannelProductMapMapper;
import com.bms.price.mapper.PayProductMapper;
import com.bms.price.mapper.PayProductTermMapper;
@@ -63,6 +65,9 @@ public class PayProductServiceImpl implements IPayProductService
@Autowired
private ChannelProductMapMapper channelProductMapMapper;
@Autowired
private ServiceChannelMapper serviceChannelMapper;
@Autowired
private PayProductTermMapper payProductTermMapper;
@@ -276,21 +281,23 @@ public class PayProductServiceImpl implements IPayProductService
@Override
public List<PayProduct> selectTemplateOptions(String payType)
{
if (!PAY_TYPE_AP.equals(payTypeOf(payType)))
{
return Collections.emptyList();
}
return payProductMapper.selectTemplateOptions();
return payProductMapper.selectTemplateOptions(payTypeOf(payType));
}
@Override
public PayProduct selectChannelPrice(String channelId)
{
return selectChannelPrice(channelId, PAY_TYPE_AP);
}
@Override
public PayProduct selectChannelPrice(String channelId, String payType)
{
if (isBlank(channelId))
{
return null;
}
Long root = channelProductRoot(channelId);
Long root = channelProductRoot(channelId, payTypeOf(payType));
if (root == null)
{
return null;
@@ -937,10 +944,16 @@ public class PayProductServiceImpl implements IPayProductService
{
throw new ServiceException("请先保存渠道,再选择产品");
}
ServiceChannel channel = serviceChannelMapper.selectServiceChannelById(channelId);
if (channel == null || isBlank(channel.getChannelCode()))
{
throw new ServiceException("渠道不存在或渠道编码为空");
}
String type = payTypeOf(payType);
Long storedId = PAY_TYPE_AP.equals(type) ? resolveRoot(productId) : productId;
Long storedId = resolveRoot(productId);
ChannelProductMap row = new ChannelProductMap();
row.setChannelId(channelId);
row.setChannelCode(channel.getChannelCode());
row.setProductId(storedId);
row.setPriceSetId(priceSetId);
row.setPayType(type);
@@ -1035,10 +1048,15 @@ public class PayProductServiceImpl implements IPayProductService
return saved;
}
/** 渠道应付绑定记在映射表,产品编号是不变的根产品。 */
/** 渠道产品绑定记在映射表,产品编号是不变的根产品。 */
private Long channelProductRoot(String channelId)
{
ChannelProductMap link = channelProductMapMapper.selectByChannelAndType(channelId, PAY_TYPE_AP);
return channelProductRoot(channelId, PAY_TYPE_AP);
}
private Long channelProductRoot(String channelId, String payType)
{
ChannelProductMap link = channelProductMapMapper.selectByChannelAndType(channelId, payType);
if (link == null || link.getProductId() == null)
{
return null;
@@ -15,7 +15,6 @@
<result property="profitSalesUserId" column="profit_sales_user_id"/>
<result property="profitSalesUserCode" column="profit_sales_user_code"/>
<result property="profitSalesUserName" column="profit_sales_user_name"/>
<result property="profitSnapshotSource" column="profit_snapshot_source"/>
<result property="billPeriodStart" column="bill_period_start"/>
<result property="billPeriodEnd" column="bill_period_end"/>
<result property="billDate" column="bill_date"/>
@@ -57,7 +56,6 @@
profit_sales_user_id,
profit_sales_user_code,
profit_sales_user_name,
profit_snapshot_source,
bill_period_start,
bill_period_end,
bill_date,
@@ -180,14 +178,14 @@
<insert id="insertArBill" parameterType="ArBill" useGeneratedKeys="true" keyProperty="arBillId">
insert into ar_bill (bill_no, bill_name, customer_id, customer_code, customer_name,
profit_customer_area, profit_sales_user_id, profit_sales_user_code, profit_sales_user_name, profit_snapshot_source, bill_period_start,
profit_customer_area, profit_sales_user_id, profit_sales_user_code, profit_sales_user_name, bill_period_start,
bill_period_end,
bill_date, currency, order_count, total_amount, written_off_amount, unwritten_amount,
status, generation_status, bill_version, bank_account_name, bank_account_no, bank_name,
remark,
confirmed_by, confirmed_time, create_by, create_user_id, create_time, update_by, update_time)
values (#{billNo}, #{billName}, #{customerId}, #{customerCode}, #{customerName},
#{profitCustomerArea}, #{profitSalesUserId}, #{profitSalesUserCode}, #{profitSalesUserName}, #{profitSnapshotSource}, #{billPeriodStart},
#{profitCustomerArea}, #{profitSalesUserId}, #{profitSalesUserCode}, #{profitSalesUserName}, #{billPeriodStart},
#{billPeriodEnd},
#{billDate}, #{currency}, #{orderCount}, #{totalAmount}, #{writtenOffAmount}, #{unwrittenAmount},
#{status}, #{generationStatus}, #{billVersion}, #{bankAccountName}, #{bankAccountNo}, #{bankName},
@@ -5,7 +5,7 @@
<id property="arBillId" column="ar_bill_id"/>
<result property="billNo" column="bill_no"/><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="snapshotSource" column="snapshot_source"/>
<result property="customerArea" column="customer_area"/>
<result property="salesUserName" column="sales_user_name"/><result property="currency" column="currency"/>
<result property="status" column="status"/><result property="salesAmount" column="sales_amount"/>
<result property="productAmount" column="product_amount"/><result property="salesGrossProfit" column="sales_gross_profit"/>
@@ -14,7 +14,6 @@
</resultMap>
<resultMap id="summary" type="com.bms.ar.profit.ArProfitSummary">
<result property="currency" column="currency"/><result property="billCount" column="bill_count"/>
<result property="invalidBillCount" column="invalid_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="writtenOffAmount" column="written_off_amount"/><result property="unwrittenAmount" column="unwritten_amount"/>
@@ -47,17 +46,12 @@
<otherwise>and 1 = 0</otherwise>
</choose>
</sql>
<!-- 订单号仅用于筛选账单,不限制该账单内参与计算的明细。 -->
<sql id="billConditions">
where b.status in ('CONFIRMED', 'PARTIAL_WRITTEN_OFF', 'WRITTEN_OFF') and b.generation_status = 'READY'
<include refid="scopeCondition"/>
<if test="query.customerId != null">and b.customer_id = #{query.customerId}</if>
<if test="query.billNo != null and query.billNo != ''">and b.bill_no like concat('%', #{query.billNo}, '%')</if>
<if test="query.arBillId != null">and b.ar_bill_id = #{query.arBillId}</if>
<if test="query.orderNo != null and query.orderNo != ''">
and exists (select 1 from ar_bill_detail matched where matched.ar_bill_id = b.ar_bill_id
and matched.order_no like concat('%', #{query.orderNo}, '%'))
</if>
<if test="query.status != null and query.status != ''">and b.status = #{query.status}</if>
<if test="query.salesUserId != null">and b.profit_sales_user_id = #{query.salesUserId}</if>
<if test="query.beginBillDate != null">and b.bill_date &gt;= #{query.beginBillDate}</if>
@@ -69,7 +63,7 @@
<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,
b.profit_customer_area as customer_area, b.profit_snapshot_source as snapshot_source,
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
@@ -118,7 +112,7 @@
order by b.bill_date desc, b.ar_bill_id desc
</select>
<select id="selectSummary" resultMap="summary">
select t.currency, count(*) as bill_count, sum(t.profit_incomplete) as invalid_bill_count,
select t.currency, count(*) as bill_count,
sum(t.sales_amount) as sales_amount, sum(t.product_amount) as product_amount,
sum(t.sales_gross_profit) as sales_gross_profit,
case when sum(case when t.profit_incomplete = 0 then t.sales_amount else null end) = 0 then null
@@ -5,8 +5,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<mapper namespace="com.bms.price.mapper.ChannelProductMapMapper">
<select id="selectByChannelAndType" resultType="com.bms.price.domain.ChannelProductMap">
select id, channel_id as channelId, product_id as productId, price_set_id as priceSetId,
pay_type as payType, create_by as createBy, create_time as createTime,
select id, channel_id as channelId, channel_code as channelCode, product_id as productId,
price_set_id as priceSetId, pay_type as payType, create_by as createBy, create_time as createTime,
update_by as updateBy, update_time as updateTime
from srv_channel_product_map
where channel_id = #{channelId} and pay_type = #{payType}
@@ -15,11 +15,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="upsert">
insert into srv_channel_product_map (
channel_id, product_id, price_set_id, pay_type, create_by, create_time, update_by, update_time
channel_id, channel_code, product_id, price_set_id, pay_type, create_by, create_time, update_by, update_time
) values (
#{channelId}, #{productId}, #{priceSetId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
#{channelId}, #{channelCode}, #{productId}, #{priceSetId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
)
on duplicate key update
channel_code = values(channel_code),
product_id = values(product_id),
price_set_id = values(price_set_id),
update_by = values(update_by),
@@ -212,7 +212,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectTemplateOptions" resultMap="PayProductResult">
select p.product_id, p.product_name, p.dict_value, p.sheet_name, p.supplier_name, p.effective_date,
select p.product_id, p.product_name, p.product_type, p.dict_value, p.sheet_name, p.supplier_name, p.effective_date,
p.currency, ifnull(cur.version_no, p.version_no) as version_no
from pay_product p
left join pay_product_term t on t.term_id = (
@@ -227,6 +227,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
left join pay_product cur on cur.product_id = t.product_id
where (p.channel_id is null or p.channel_id = '')
and p.origin_id is null
and ifnull(p.product_type, 'AP') = #{payType}
order by p.product_name
</select>
+38 -13
View File
@@ -42,17 +42,12 @@ BEGIN
SELECT COUNT(*) INTO n FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'profit_sales_user_name';
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN profit_sales_user_name VARCHAR(64) NULL COMMENT '账单销售员姓名快照'; END IF;
SELECT COUNT(*) INTO n FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'profit_snapshot_source';
IF n = 0 THEN ALTER TABLE ar_bill ADD COLUMN profit_snapshot_source VARCHAR(16) NULL COMMENT 'NEW生成时快照/BACKFILLED历史补录'; END IF;
SELECT COUNT(*) INTO n FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'ar_bill'
AND is_nullable = 'YES'
AND ((column_name = 'profit_customer_area' AND data_type = 'varchar' AND character_maximum_length = 50)
OR (column_name = 'profit_sales_user_id' AND data_type = 'bigint')
OR (column_name IN ('profit_sales_user_code','profit_sales_user_name') AND data_type = 'varchar' AND character_maximum_length = 64)
OR (column_name = 'profit_snapshot_source' AND data_type = 'varchar' AND character_maximum_length = 16));
IF n <> 5 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '账单快照字段结构冲突'; END IF;
OR (column_name IN ('profit_sales_user_code','profit_sales_user_name') AND data_type = 'varchar' AND character_maximum_length = 64));
IF n <> 4 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '账单快照字段结构冲突'; END IF;
SELECT COUNT(*) INTO n FROM information_schema.statistics WHERE table_schema = DATABASE()
AND table_name = 'ar_bill' AND index_name = 'idx_ar_profit_sales_date';
IF n = 0 THEN ALTER TABLE ar_bill ADD INDEX idx_ar_profit_sales_date (profit_sales_user_id, bill_date, ar_bill_id); END IF;
@@ -256,7 +251,7 @@ JOIN ctr_customer c ON c.customer_id = b.customer_id
JOIN sys_user u ON CONVERT(CAST(u.user_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_unicode_ci = c.sales_user_id AND u.del_flag = '0'
JOIN ar_bill_detail d ON d.ar_bill_id = b.ar_bill_id
JOIN prc_order_bill p ON p.bill_id = d.prc_order_bill_id
WHERE b.profit_sales_user_id IS NULL AND (b.profit_snapshot_source IS NULL OR b.profit_snapshot_source = 'BACKFILLED')
WHERE b.profit_sales_user_id IS NULL
AND p.belong_saler_code IS NOT NULL
AND TRIM(p.belong_saler_code) <> ''
AND TRIM(p.belong_saler_code) <> u.user_name COLLATE utf8mb4_unicode_ci
@@ -265,7 +260,7 @@ WHERE b.profit_sales_user_id IS NULL AND (b.profit_snapshot_source IS NULL OR b.
-- 此处仅补空值。客户区域来自客户当前资料,并非出账时的历史事实。
UPDATE ar_bill b JOIN ctr_customer c ON c.customer_id = b.customer_id
SET b.profit_customer_area = c.customer_area
WHERE b.profit_customer_area IS NULL AND (b.profit_snapshot_source IS NULL OR b.profit_snapshot_source = 'BACKFILLED')
WHERE b.profit_customer_area IS NULL
AND c.customer_area IS NOT NULL AND TRIM(c.customer_area) <> '';
UPDATE ar_bill b
@@ -274,7 +269,7 @@ JOIN sys_user u ON CONVERT(CAST(u.user_id AS CHAR) USING utf8mb4) COLLATE utf8mb
SET b.profit_sales_user_id = u.user_id,
b.profit_sales_user_code = u.user_name,
b.profit_sales_user_name = u.nick_name
WHERE b.profit_sales_user_id IS NULL AND (b.profit_snapshot_source IS NULL OR b.profit_snapshot_source = 'BACKFILLED')
WHERE b.profit_sales_user_id IS NULL
AND NOT EXISTS (
SELECT 1 FROM ar_bill_detail d JOIN prc_order_bill p ON p.bill_id = d.prc_order_bill_id
WHERE d.ar_bill_id = b.ar_bill_id AND p.belong_saler_code IS NOT NULL
@@ -283,9 +278,6 @@ WHERE b.profit_sales_user_id IS NULL AND (b.profit_snapshot_source IS NULL OR b.
AND TRIM(p.belong_saler_code) <> CONVERT(CAST(u.user_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_unicode_ci
);
UPDATE ar_bill SET profit_snapshot_source = 'BACKFILLED'
WHERE profit_snapshot_source IS NULL AND (profit_customer_area IS NOT NULL OR profit_sales_user_id IS NOT NULL);
-- 历史核销比例不可追溯,不能使用当前比例替代。
INSERT INTO ar_profit_writeoff_rate (writeoff_id, sales_user_id, rate_id, rate, create_time)
SELECT w.writeoff_id, b.profit_sales_user_id, NULL, NULL, NOW()
@@ -337,3 +329,36 @@ END$$
DELIMITER ;
CALL bms_ar_profit_upgrade_20261105_v1();
DROP PROCEDURE IF EXISTS bms_ar_profit_upgrade_20261105_v1;
-- 删除已废弃的来源标记,并把保留的四个快照字段放在 customer_name 后;顺序正确时跳过表变更。
SELECT COUNT(*) INTO @bms_ar_profit_source_column_count
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ar_bill' AND column_name = 'profit_snapshot_source';
SELECT COUNT(*) INTO @bms_ar_profit_column_order_count
FROM information_schema.columns c
JOIN information_schema.columns base_column
ON base_column.table_schema = c.table_schema
AND base_column.table_name = c.table_name
AND base_column.column_name = 'customer_name'
WHERE c.table_schema = DATABASE() AND c.table_name = 'ar_bill'
AND ((c.column_name = 'profit_customer_area' AND c.ordinal_position = base_column.ordinal_position + 1)
OR (c.column_name = 'profit_sales_user_id' AND c.ordinal_position = base_column.ordinal_position + 2)
OR (c.column_name = 'profit_sales_user_code' AND c.ordinal_position = base_column.ordinal_position + 3)
OR (c.column_name = 'profit_sales_user_name' AND c.ordinal_position = base_column.ordinal_position + 4));
SET @bms_ar_profit_snapshot_column_definitions =
'MODIFY COLUMN profit_customer_area VARCHAR(50) COLLATE utf8mb4_unicode_ci NULL COMMENT ''账单客户区域快照'' AFTER customer_name,
MODIFY COLUMN profit_sales_user_id BIGINT(20) NULL COMMENT ''账单销售员用户ID快照'' AFTER profit_customer_area,
MODIFY COLUMN profit_sales_user_code VARCHAR(64) COLLATE utf8mb4_unicode_ci NULL COMMENT ''账单销售员工号快照'' AFTER profit_sales_user_id,
MODIFY COLUMN profit_sales_user_name VARCHAR(64) COLLATE utf8mb4_unicode_ci NULL COMMENT ''账单销售员姓名快照'' AFTER profit_sales_user_code';
SET @bms_ar_profit_column_order_sql = CASE
WHEN @bms_ar_profit_source_column_count > 0 AND @bms_ar_profit_column_order_count = 4
THEN 'ALTER TABLE ar_bill DROP COLUMN profit_snapshot_source'
WHEN @bms_ar_profit_source_column_count > 0
THEN CONCAT('ALTER TABLE ar_bill DROP COLUMN profit_snapshot_source, ', @bms_ar_profit_snapshot_column_definitions)
WHEN @bms_ar_profit_column_order_count <> 4
THEN CONCAT('ALTER TABLE ar_bill ', @bms_ar_profit_snapshot_column_definitions)
ELSE 'SELECT 1 WHERE 1 = 0'
END;
PREPARE bms_ar_profit_column_order_stmt FROM @bms_ar_profit_column_order_sql;
EXECUTE bms_ar_profit_column_order_stmt;
DEALLOCATE PREPARE bms_ar_profit_column_order_stmt;
+22 -2
View File
@@ -4,7 +4,8 @@ SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `srv_channel_product_map` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`channel_id` varchar(50) NOT NULL COMMENT '渠道ID,对应 srv_service_channel.id',
`product_id` bigint(20) NOT NULL COMMENT '产品ID,应付时为价卡产品编号',
`channel_code` varchar(64) DEFAULT NULL COMMENT '渠道编码,对应 srv_service_channel.channel_code',
`product_id` bigint(20) NOT NULL COMMENT '产品ID,应付/应收时为价卡产品编号',
`price_set_id` bigint(20) DEFAULT NULL COMMENT '应收套餐ID,对应 prc_price_set.price_set_id',
`pay_type` varchar(10) NOT NULL COMMENT '收付类型,字典 bms_pay_type:AR应收 AP应付',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
@@ -14,7 +15,8 @@ CREATE TABLE IF NOT EXISTS `srv_channel_product_map` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_channel_pay_type` (`channel_id`, `pay_type`),
KEY `idx_channel_product_map_product` (`product_id`),
KEY `idx_channel_product_map_price_set` (`price_set_id`)
KEY `idx_channel_product_map_price_set` (`price_set_id`),
KEY `idx_channel_product_map_channel_code` (`channel_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='渠道产品关系映射';
SET @sql = (
@@ -30,6 +32,24 @@ PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql_channel_code = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE srv_channel_product_map ADD COLUMN channel_code varchar(64) DEFAULT NULL COMMENT ''渠道编码,对应 srv_service_channel.channel_code'' AFTER channel_id, ADD KEY idx_channel_product_map_channel_code (channel_code)',
'SELECT 1'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'srv_channel_product_map' AND COLUMN_NAME = 'channel_code'
);
PREPARE stmt FROM @sql_channel_code;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE srv_channel_product_map m
INNER JOIN srv_service_channel c ON c.id = m.channel_id
SET m.channel_code = c.channel_code
WHERE m.channel_code IS NULL OR m.channel_code = '';
SET @copy_sql = (
SELECT IF(
COUNT(*) > 0,
+3 -2
View File
@@ -80,10 +80,11 @@ export function comparePay(leftId, rightId) {
})
}
export function getChannelPay(channelId) {
export function getChannelPay(channelId, payType = 'AP') {
return request({
url: '/price/pay/channel/' + channelId,
method: 'get'
method: 'get',
params: { payType }
})
}
@@ -149,6 +149,14 @@
<div class="pay-tip">{{ payTip }}</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="应收产品">
<el-select v-model="arOriginId" filterable clearable placeholder="选择已导入的应收产品" style="width: 280px" :disabled="viewing || !form.id" @change="onArOrigin">
<el-option v-for="item in arOptions" :key="item.productId" :label="payOptionLabel(item)" :value="item.productId" />
</el-select>
<div class="pay-tip">{{ arTip }}</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -175,9 +183,13 @@ const open = ref(false)
const viewing = ref(false)
const title = ref('')
const payOptions = ref([])
const arOptions = ref([])
const payOriginId = ref()
const arOriginId = ref()
const payProduct = ref(null)
const arProduct = ref(null)
const syncingPay = ref(false)
const syncingAr = ref(false)
const payTip = computed(() => {
if (viewing.value) {
if (payProduct.value?.versionNo) return `已绑定第 ${payProduct.value.versionNo} 版。`
@@ -187,6 +199,15 @@ const payTip = computed(() => {
if (payProduct.value?.versionNo) return `当前按有效期使用第 ${payProduct.value.versionNo} 版。历史订单按下单或计费时间取当时的版本。`
return '选择产品后即绑定,按应付 AP 保存。'
})
const arTip = computed(() => {
if (viewing.value) {
if (arProduct.value?.versionNo) return `已绑定第 ${arProduct.value.versionNo} 版。`
return arOriginId.value ? '已绑定应收产品。' : '未绑定应收产品。'
}
if (!form.value?.id) return '先保存渠道,再选择应收产品。渠道绑定的是产品编号,更新价格不会换编号。'
if (arProduct.value?.versionNo) return `当前按有效期使用第 ${arProduct.value.versionNo} 版。历史订单按下单或计费时间取当时的版本。`
return '选择产品后即绑定,按应收 AR 保存。'
})
const openOptions = [
{ label: '启用', value: '1' },
@@ -260,10 +281,14 @@ function emptyForm() {
function resetPay() {
syncingPay.value = true
syncingAr.value = true
payOriginId.value = undefined
arOriginId.value = undefined
payProduct.value = null
arProduct.value = null
nextTick(() => {
syncingPay.value = false
syncingAr.value = false
})
}
@@ -276,11 +301,14 @@ function loadPayOptions() {
listPayOptions('AP').then(res => {
payOptions.value = res.data || []
})
listPayOptions('AR').then(res => {
arOptions.value = res.data || []
})
}
function loadChannelPay(channelId) {
syncingPay.value = true
getChannelPay(channelId).then(res => {
getChannelPay(channelId, 'AP').then(res => {
payProduct.value = res.data || null
payOriginId.value = res.data?.originId || res.data?.productId
}).finally(() => {
@@ -290,6 +318,18 @@ function loadChannelPay(channelId) {
})
}
function loadChannelAr(channelId) {
syncingAr.value = true
getChannelPay(channelId, 'AR').then(res => {
arProduct.value = res.data || null
arOriginId.value = res.data?.originId || res.data?.productId
}).finally(() => {
nextTick(() => {
syncingAr.value = false
})
})
}
function handleAdd() {
viewing.value = false
form.value = emptyForm()
@@ -307,6 +347,7 @@ function handleUpdate(row) {
resetPay()
loadPayOptions()
loadChannelPay(row.id)
loadChannelAr(row.id)
title.value = '修改渠道'
open.value = true
nextTick(() => formRef.value && formRef.value.clearValidate())
@@ -320,6 +361,7 @@ function handleDetail(row) {
resetPay()
loadPayOptions()
loadChannelPay(row.id)
loadChannelAr(row.id)
title.value = '渠道详情'
open.value = true
nextTick(() => formRef.value && formRef.value.clearValidate())
@@ -339,6 +381,19 @@ function onPayOrigin(id) {
})
}
function onArOrigin(id) {
if (viewing.value || !form.value.id || syncingAr.value) return
if (!id) {
arProduct.value = null
bindChannelPay(form.value.id, null, 'AR')
return
}
bindChannelPay(form.value.id, id, 'AR').then(res => {
arProduct.value = res.data || null
arOriginId.value = res.data?.originId || id
})
}
function submitForm() {
if (viewing.value) return
formRef.value.validate(valid => {
+1 -20
View File
@@ -181,7 +181,6 @@
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
<el-button link type="primary" icon="Money" v-hasPermi="['price:billing:bill']" @click="handlePayBill(row)">生成应付</el-button>
<!-- <el-button
link
type="primary"
@@ -320,7 +319,6 @@
<script setup name="BillingBill">
import { listBill, getBill, refundBill, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
import { generatePayBill } from '@/api/price/pay'
import { exportAsyncFromTable } from '@/utils/excelExport'
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
import { BILLING_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
@@ -578,7 +576,7 @@ const columns = [
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
{ label: '操作', prop: 'action', align: 'center', width: 260, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
{ label: '操作', prop: 'action', align: 'center', width: 150, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
]
const arrearsStatusOptions = [
@@ -774,23 +772,6 @@ function handleUpdateArrears(ids, selection) {
}).catch(() => {})
}
function handlePayBill(row) {
if (!row || !row.billId) return
ElMessageBox.confirm(
'按订单的渠道服务商和已配置的应付产品计算应付?',
'生成应付',
{ type: 'warning' }
).then(() => generatePayBill(row.billId)).then(res => {
const data = res.data || {}
if (data.skipped && data.skipped.length) {
ElMessageBox.alert(data.skipped.join('\n'), data.message || '应付已生成', { type: 'warning' })
} else {
ElMessage.success(data.message || '应付已生成')
}
tableRef.value?.refresh?.()
}).catch(() => {})
}
function handleRefund(row) {
if (!row || !row.billId) return
if (row.refundStatus === 'REFUNDED') {
@@ -20,7 +20,6 @@
</el-select>
</el-form-item>
<el-form-item label="账单号"><el-input v-model="query.billNo" clearable @keyup.enter="search" /></el-form-item>
<el-form-item label="订单号"><el-input v-model="query.orderNo" clearable @keyup.enter="search" /></el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部状态" clearable style="width: 140px">
<el-option label="已确认" value="CONFIRMED" /><el-option label="部分核销" value="PARTIAL_WRITTEN_OFF" /><el-option label="已核销" value="WRITTEN_OFF" />
@@ -38,10 +37,8 @@
<div>销售毛利 <b>{{ money(summary.salesGrossProfit) }}</b></div>
<div>销售毛利率 <b>{{ rate(summary.salesGrossMarginRate) }}</b></div>
<div>账单已核销 <b>{{ money(summary.writtenOffAmount) }}</b></div><div>账单未核销 <b>{{ money(summary.unwrittenAmount) }}</b></div>
<div>毛利待核对账单 <b>{{ summary.invalidBillCount ?? '-' }}</b></div>
</div>
<div class="summary-note">广州产品价按红线价展示,深圳产品价按产品价展示;待核对账单不计入产品价、毛利及毛利率汇总。</div>
<div v-if="query.orderNo" class="summary-note">订单号用于查找账单,金额按完整账单统计。</div>
<div class="summary-note">广州产品价按红线价展示,深圳产品价按产品价展示。</div>
</el-card>
<el-card shadow="never" class="data-card">
@@ -58,7 +55,6 @@
<el-table-column prop="customerCode" label="客户代码" width="110" />
<el-table-column prop="customerName" label="客户名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="salesUserName" label="销售员" width="110" />
<el-table-column label="归属来源" width="110"><template #default="{ row }">{{ row.snapshotSource === 'NEW' ? '生成时快照' : row.snapshotSource === 'BACKFILLED' ? '历史补录' : '-' }}</template></el-table-column>
<el-table-column prop="currency" label="币种" width="80" />
<el-table-column label="销售价" min-width="110" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
<el-table-column label="产品价" min-width="110" align="right"><template #default="{ row }">{{ money(row.productAmount) }}</template></el-table-column>
@@ -161,7 +157,7 @@ import { parseTime } from '@/utils/bms'
import { ElMessage } from 'element-plus'
const { proxy } = getCurrentInstance()
const query = reactive({ customerId: null, salesUserId: null, billNo: '', orderNo: '', status: '' })
const query = reactive({ customerId: null, salesUserId: null, billNo: '', status: '' })
const salesOptions = ref([])
const salesLoading = ref(false)
let salesSearchVersion = 0
@@ -188,7 +184,7 @@ const writeoffRatesVisible = ref(false), writeoffRatesLoading = ref(false), writ
function params() { return { ...query, beginBillDate: dateRange.value?.[0], endBillDate: dateRange.value?.[1] } }
function search() { pageNum.value = 1; loadRows(); loadSummary() }
function reset() { Object.assign(query, { customerId: null, salesUserId: null, billNo: '', orderNo: '', status: '' }); dateRange.value = []; search() }
function reset() { Object.assign(query, { customerId: null, salesUserId: null, billNo: '', status: '' }); dateRange.value = []; search() }
function handleSalesInput() {
salesSearchVersion++
salesOptions.value = []
@@ -273,8 +269,8 @@ search()
<style scoped>
.summary-card, .data-card { margin-top: 14px; }
.metrics { display: flex; flex-wrap: wrap; gap: 14px; }
.metrics > div { min-width: 150px; padding: 10px; background: var(--el-fill-color-lighter); color: var(--el-text-color-secondary); }
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); gap: 14px; }
.metrics > div { min-width: 0; padding: 10px; background: var(--el-fill-color-lighter); color: var(--el-text-color-secondary); }
.metrics b { display: block; margin-top: 5px; color: var(--el-text-color-primary); }
.summary-note { margin-top: 10px; color: var(--el-text-color-secondary); font-size: 12px; }
.heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }