Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2632e5aa78 | |||
| ace1eb06c0 | |||
| f31bb4c54d | |||
| f2d27f3502 |
+7
-6
@@ -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 */
|
||||
|
||||
+27
-9
@@ -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 >= #{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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user