违约金
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import datetime # 日期时间处理模块,用于日期解析和计算
|
||||
import datetime # 日期时间处理模块,用于日期解析、计算、格式化
|
||||
import logging # 日志模块,用于调试输出
|
||||
import re # 正则表达式模块,用于解析收费区间字符串
|
||||
|
||||
from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类
|
||||
|
||||
@@ -7,7 +8,7 @@ _logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
|
||||
|
||||
|
||||
class PenaltyMonthReceivedReport(models.Model):
|
||||
_inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型
|
||||
_inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法
|
||||
|
||||
@api.model
|
||||
def get_penalty_month_received_report(self, params):
|
||||
@@ -15,12 +16,14 @@ class PenaltyMonthReceivedReport(models.Model):
|
||||
违约金本月实收报表
|
||||
|
||||
两路数据源:
|
||||
来源A:property.accounts.receivable 中 is_batch=True 的违约金记录
|
||||
来源A:property.accounts.receivable 中收款类型为违约金 且 is_batch=True 的记录
|
||||
通过 source_receivable_id 关联租金
|
||||
本月实收:取该违约金记录的 receivable.modify.line,reconciliation_date 在查询月内的行
|
||||
来源B:receivable.modify.line 中 is_penalty=True 的记录
|
||||
通过 receivable_id 关联租金(receivable_id 指向的就是那笔租金应收记录)
|
||||
本月实收:reconciliation_date 在查询月内
|
||||
通过其 receivable_id(违约金应收单)的 charging_range 反推租金的收款日期,
|
||||
在 receivable.modify.line 中找到匹配的租金记录(同承租方+同物业+同合同+同收款日期)
|
||||
|
||||
本月实收:来源A取 reconciliation_date 在查询月内的 modify.line;
|
||||
来源B取 is_penalty 且 reconciliation_date 在查询月内的 modify.line
|
||||
"""
|
||||
|
||||
# ========== 一、参数解析 ==========
|
||||
@@ -72,6 +75,30 @@ class PenaltyMonthReceivedReport(models.Model):
|
||||
return False
|
||||
return True
|
||||
|
||||
# ========== 辅助函数:modify.line 内存过滤(来源B用)==========
|
||||
def _match_filters_ml(ml, prop_name_str):
|
||||
"""对 receivable.modify.line 记录做内存过滤"""
|
||||
if keyword:
|
||||
kw = keyword.lower()
|
||||
match = False
|
||||
if ml.contract_id and ml.contract_id.contract_code and kw in ml.contract_id.contract_code.lower():
|
||||
match = True
|
||||
if prop_name_str and kw in prop_name_str.lower():
|
||||
match = True
|
||||
if ml.lessee_id and ml.lessee_id.name and kw in ml.lessee_id.name.lower():
|
||||
match = True
|
||||
if not match:
|
||||
return False
|
||||
if partner_id and ml.precinct_id and ml.precinct_id.id != int(partner_id):
|
||||
return False
|
||||
if company_id and ml.company_id and ml.company_id.id != int(company_id):
|
||||
return False
|
||||
if lessee_id and (not ml.lessee_id or ml.lessee_id.id != int(lessee_id)):
|
||||
return False
|
||||
if contract_id and (not ml.contract_id or ml.contract_id.id != int(contract_id)):
|
||||
return False
|
||||
return True
|
||||
|
||||
# ========== 辅助函数:构建物业名称显示(穿透对象) ==========
|
||||
def _build_property_display(contract_rec, property_name_str):
|
||||
"""构建物业名称穿透对象"""
|
||||
@@ -83,172 +110,276 @@ class PenaltyMonthReceivedReport(models.Model):
|
||||
}
|
||||
return property_name_str or ""
|
||||
|
||||
# ========== 辅助函数:构建行数据(共用) ==========
|
||||
def _build_row(pay_amount, pay_date, pay_remark, rent_rec, penalty_rec, contract_rec,
|
||||
prop_name, lessee_rec, company_name, source_tag):
|
||||
"""构建报表行字典"""
|
||||
# 租金信息
|
||||
rent_amount = 0.0
|
||||
rent_collected = 0.0
|
||||
rent_uncollected = 0.0
|
||||
rent_status = ""
|
||||
rent_due_date = ""
|
||||
rent_payout_date = ""
|
||||
if rent_rec:
|
||||
rent_amount = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2)
|
||||
rent_collected = round(rent_rec.delivered1 or 0.0, 2)
|
||||
rent_uncollected = round(rent_rec.submitted or 0.0, 2)
|
||||
rent_due_date = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else ""
|
||||
rent_payout_date = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else ""
|
||||
if rent_rec.deposit_state == 'paid':
|
||||
rent_status = "已收"
|
||||
elif rent_rec.deposit_state == 'no_paid':
|
||||
rent_status = "收部分"
|
||||
else:
|
||||
rent_status = "未收"
|
||||
# ========== 辅助函数:解析收费区间字符串 ==========
|
||||
def _parse_charging_range(range_str):
|
||||
"""
|
||||
解析收费区间字符串,提取起止日期
|
||||
|
||||
# 违约金信息
|
||||
penalty_amount = 0.0
|
||||
penalty_collected = 0.0
|
||||
penalty_uncollected = 0.0
|
||||
penalty_status = ""
|
||||
penalty_start_date = ""
|
||||
charging_range = ""
|
||||
remark = ""
|
||||
if penalty_rec:
|
||||
penalty_amount = round(penalty_rec.original_amount or 0.0, 2)
|
||||
penalty_collected = round(penalty_rec.delivered1 or 0.0, 2)
|
||||
penalty_uncollected = round(penalty_rec.submitted or 0.0, 2)
|
||||
penalty_start_date = penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else ""
|
||||
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
|
||||
penalty_rec.deposit_state, penalty_rec.deposit_state)
|
||||
charging_range = penalty_rec.charging_range or ""
|
||||
remark = penalty_rec.remark or ""
|
||||
|
||||
return {
|
||||
"来源": source_tag,
|
||||
"租金ID": rent_rec.id if rent_rec else "",
|
||||
"租金应收日": rent_due_date,
|
||||
"租金收款日期": rent_payout_date,
|
||||
"租金金额": rent_amount,
|
||||
"租金已收": rent_collected,
|
||||
"租金未收": rent_uncollected,
|
||||
"租金状态": rent_status,
|
||||
"违约金金额": penalty_amount,
|
||||
"违约金已收": penalty_collected,
|
||||
"违约金未收": penalty_uncollected,
|
||||
"违约金状态": penalty_status,
|
||||
"违约金应收开始": penalty_start_date,
|
||||
"实收金额": round(pay_amount, 2),
|
||||
"收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "",
|
||||
"收款备注": pay_remark or "",
|
||||
"收费区间": charging_range,
|
||||
"备注": remark,
|
||||
"合同编号": {
|
||||
'model': 'property.lease.contract',
|
||||
'id': contract_rec.id,
|
||||
'display_name': contract_rec.contract_code,
|
||||
} if contract_rec else "",
|
||||
"物业名称": _build_property_display(contract_rec, prop_name),
|
||||
"承租方名称": {
|
||||
'model': 'property.tenant.information',
|
||||
'id': lessee_rec.id,
|
||||
'display_name': lessee_rec.name,
|
||||
} if lessee_rec else "",
|
||||
"所属公司": company_name or "",
|
||||
}
|
||||
格式示例:"2025-10-01 到 2025-11-25共56"
|
||||
返回:(start_date, end_date) 或 (None, None)
|
||||
"""
|
||||
if not range_str:
|
||||
return None, None
|
||||
# 匹配 YYYY-MM-DD 到 YYYY-MM-DD 共NN 格式
|
||||
pattern = r'(\d{4}-\d{2}-\d{2})\s*到\s*(\d{4}-\d{2}-\d{2})'
|
||||
match = re.search(pattern, range_str)
|
||||
if match:
|
||||
try:
|
||||
start_d = datetime.datetime.strptime(match.group(1), '%Y-%m-%d').date()
|
||||
end_d = datetime.datetime.strptime(match.group(2), '%Y-%m-%d').date()
|
||||
return start_d, end_d
|
||||
except ValueError:
|
||||
pass
|
||||
return None, None
|
||||
|
||||
# ========== 初始化 ==========
|
||||
rows = []
|
||||
total_collected = 0.0
|
||||
rows = [] # 报表行数据列表
|
||||
total_collected = 0.0 # 实收违约金合计
|
||||
processed_keys = set() # 去重集合
|
||||
|
||||
# ========== 三、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ==========
|
||||
# 查出 is_batch=True 的违约金记录
|
||||
penalty_a_recs = self.sudo().search([
|
||||
# ========== 二、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ==========
|
||||
# 查出 is_batch=True 且收款类型为违约金的记录
|
||||
penalty_recs = self.sudo().search([
|
||||
('management_type_id.name', '=', '违约金'), # 收款类型为违约金
|
||||
('is_batch', '=', True), # 标记为批量修复的违约金数据
|
||||
('is_invalid', '!=', True), # 排除已作废
|
||||
])
|
||||
|
||||
# 查出这些违约金记录在查询月内的 modify.line
|
||||
if penalty_a_recs:
|
||||
modify_a_lines = self.env['receivable.modify.line'].sudo().search([
|
||||
('receivable_id', 'in', penalty_a_recs.ids), # 关联到 is_batch 违约金记录
|
||||
if penalty_recs:
|
||||
modify_lines = self.env['receivable.modify.line'].sudo().search([
|
||||
('receivable_id', 'in', penalty_recs.ids), # 关联到 is_batch 违约金记录
|
||||
('reconciliation_date', '!=', False), # 有对账日期
|
||||
('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始
|
||||
('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束
|
||||
])
|
||||
|
||||
for ml in modify_a_lines:
|
||||
pay_amount = ml.amount or 0.0
|
||||
for ml in modify_lines:
|
||||
pay_amount = ml.amount or 0.0 # 实收金额
|
||||
if pay_amount <= 0:
|
||||
continue
|
||||
|
||||
penalty_rec = ml.receivable_id # 这就是 is_batch 违约金记录
|
||||
rent_rec = penalty_rec.source_receivable_id # 通过 source_receivable_id 关联租金
|
||||
|
||||
# 物业名称
|
||||
prop_name = penalty_rec.property_name or (rent_rec.property_name if rent_rec else "")
|
||||
# 内存过滤
|
||||
if not _match_filters(penalty_rec, prop_name):
|
||||
continue
|
||||
|
||||
total_collected += pay_amount
|
||||
rows.append(_build_row(
|
||||
pay_amount=pay_amount,
|
||||
pay_date=ml.reconciliation_date,
|
||||
pay_remark=ml.remake1,
|
||||
rent_rec=rent_rec,
|
||||
penalty_rec=penalty_rec,
|
||||
contract_rec=penalty_rec.contract_id,
|
||||
prop_name=prop_name,
|
||||
lessee_rec=penalty_rec.lessee_id,
|
||||
company_name=penalty_rec.company_id.name if penalty_rec.company_id else "",
|
||||
source_tag="A",
|
||||
))
|
||||
# 去重 key
|
||||
key = ('A', ml.id)
|
||||
if key in processed_keys:
|
||||
continue
|
||||
processed_keys.add(key)
|
||||
|
||||
# ========== 四、来源B:receivable.modify.line 中 is_penalty=True 的记录 ==========
|
||||
# 查询月内 is_penalty=True 的付款行
|
||||
modify_b_lines = self.env['receivable.modify.line'].sudo().search([
|
||||
# --- 构建租金信息 ---
|
||||
rent_amount_val = 0.0
|
||||
rent_collected_amt = 0.0
|
||||
rent_uncollected_amt = 0.0
|
||||
rent_status = ""
|
||||
rent_due_date_str = ""
|
||||
rent_payout_date_str = ""
|
||||
if rent_rec:
|
||||
rent_amount_val = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2)
|
||||
rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2)
|
||||
rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2)
|
||||
rent_due_date_str = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else ""
|
||||
rent_payout_date_str = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else ""
|
||||
if rent_rec.deposit_state == 'paid':
|
||||
rent_status = "已收"
|
||||
elif rent_rec.deposit_state == 'no_paid':
|
||||
rent_status = "收部分"
|
||||
else:
|
||||
rent_status = "未收"
|
||||
|
||||
# --- 构建违约金信息 ---
|
||||
penalty_amount = round(penalty_rec.original_amount or 0.0, 2)
|
||||
penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2)
|
||||
penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2)
|
||||
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
|
||||
penalty_rec.deposit_state, penalty_rec.deposit_state)
|
||||
|
||||
# 累计统计
|
||||
total_collected += pay_amount
|
||||
|
||||
contract = penalty_rec.contract_id
|
||||
rows.append({
|
||||
"来源": "A",
|
||||
"租金ID": rent_rec.id if rent_rec else "",
|
||||
"租金应收日": rent_due_date_str,
|
||||
"租金收款日期": rent_payout_date_str,
|
||||
"租金金额": rent_amount_val,
|
||||
"租金已收": rent_collected_amt,
|
||||
"租金未收": rent_uncollected_amt,
|
||||
"租金状态": rent_status,
|
||||
"违约金金额": penalty_amount,
|
||||
"违约金已收": penalty_collected_amt,
|
||||
"违约金未收": penalty_uncollected_amt,
|
||||
"违约金状态": penalty_status,
|
||||
"违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "",
|
||||
"实收金额": round(pay_amount, 2),
|
||||
"收款日期": ml.reconciliation_date.strftime("%Y-%m-%d") if ml.reconciliation_date else "",
|
||||
"收款备注": ml.remake1 or "",
|
||||
"收费区间": penalty_rec.charging_range or "",
|
||||
"备注": penalty_rec.remark or "",
|
||||
"合同编号": {
|
||||
'model': 'property.lease.contract',
|
||||
'id': contract.id,
|
||||
'display_name': contract.contract_code,
|
||||
} if contract else "",
|
||||
"物业名称": _build_property_display(contract, prop_name),
|
||||
"承租方名称": {
|
||||
'model': 'property.tenant.information',
|
||||
'id': penalty_rec.lessee_id.id,
|
||||
'display_name': penalty_rec.lessee_id.name,
|
||||
} if penalty_rec.lessee_id else "",
|
||||
"所属公司": penalty_rec.company_id.name if penalty_rec.company_id else "",
|
||||
})
|
||||
|
||||
# ========== 三、来源B:receivable.modify.line 中 is_penalty=True 的记录 ==========
|
||||
# 通过 charging_range 反查关联租金
|
||||
# 步骤:取 is_penalty=True 的 modify.line → 读 receivable_id 的 charging_range →
|
||||
# 解析出最后付款日 → 在 modify.line 中找匹配的租金记录(同承租方+同物业+同合同+同收款日期)
|
||||
|
||||
source_b_domain = [
|
||||
('is_penalty', '=', True), # 标记为违约金付款
|
||||
('receivable_id', '!=', False), # 必须关联应收记录
|
||||
('reconciliation_date', '!=', False), # 有对账日期
|
||||
('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始
|
||||
('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束
|
||||
])
|
||||
]
|
||||
penalty_modify_lines = self.env['receivable.modify.line'].sudo().search(source_b_domain)
|
||||
|
||||
for ml in modify_b_lines:
|
||||
pay_amount = ml.amount or 0.0
|
||||
for pml in penalty_modify_lines:
|
||||
# 取违约金金额(delivered1 字段)
|
||||
pay_amount = round(pml.delivered1 or 0.0, 2)
|
||||
if pay_amount <= 0:
|
||||
continue
|
||||
|
||||
rent_rec = ml.receivable_id # receivable_id 就是关联的那笔租金
|
||||
prop_name = rent_rec.property_name or ""
|
||||
|
||||
# 内存过滤
|
||||
if not _match_filters(rent_rec, prop_name):
|
||||
# 获取违约金对应的应收单(property.accounts.receivable)
|
||||
penalty_rec = pml.receivable_id
|
||||
if not penalty_rec:
|
||||
continue
|
||||
|
||||
total_collected += pay_amount
|
||||
rows.append(_build_row(
|
||||
pay_amount=pay_amount,
|
||||
pay_date=ml.reconciliation_date,
|
||||
pay_remark=ml.remake1,
|
||||
rent_rec=rent_rec,
|
||||
penalty_rec=None, # 来源B没有单独的违约金记录
|
||||
contract_rec=rent_rec.contract_id,
|
||||
prop_name=prop_name,
|
||||
lessee_rec=rent_rec.lessee_id,
|
||||
company_name=rent_rec.company_id.name if rent_rec.company_id else "",
|
||||
source_tag="B",
|
||||
))
|
||||
# --- 读取 charging_range 并解析日期 ---
|
||||
charging_range = penalty_rec.charging_range or ""
|
||||
range_start, range_end = _parse_charging_range(charging_range)
|
||||
if not range_end:
|
||||
# 无法从收费区间解析出日期,跳过这条
|
||||
continue
|
||||
|
||||
# ========== 五、构建统计汇总数据 ==========
|
||||
# --- 通过 charging_range 的结束日期反查租金 modify.line ---
|
||||
# 匹配条件:同一承租方 + 同一合同 + 收款日期=range_end + 非违约金记录
|
||||
rent_ml_domain = [
|
||||
('id', '!=', pml.id), # 排除自身
|
||||
('is_penalty', '=', False), # 非违约金(即租金)
|
||||
('reconciliation_date', '=', range_end), # 收款日期 = 收费区间结束日期
|
||||
]
|
||||
if pml.lessee_id:
|
||||
rent_ml_domain.append(('lessee_id', '=', pml.lessee_id.id)) # 同一承租方
|
||||
if pml.contract_id:
|
||||
rent_ml_domain.append(('contract_id', '=', pml.contract_id.id)) # 同一合同
|
||||
|
||||
rent_mls = self.env['receivable.modify.line'].sudo().search(rent_ml_domain, limit=1)
|
||||
if not rent_mls:
|
||||
continue
|
||||
rent_ml = rent_mls[0]
|
||||
|
||||
# 获取租金对应的应收单
|
||||
rent_receivable = rent_ml.receivable_id
|
||||
|
||||
# 物业名称(优先从违约金应收单取)
|
||||
prop_name = penalty_rec.property_name or (rent_receivable.property_name if rent_receivable else "")
|
||||
|
||||
# 内存过滤(用违约金应收单的信息过滤)
|
||||
if not _match_filters_ml(pml, prop_name):
|
||||
continue
|
||||
|
||||
# 去重 key
|
||||
key = ('B', pml.id)
|
||||
if key in processed_keys:
|
||||
continue
|
||||
processed_keys.add(key)
|
||||
|
||||
# --- 构建租金信息(从租金 modify.line 和对应应收单获取)---
|
||||
rent_amount_val = 0.0
|
||||
rent_collected_amt = 0.0
|
||||
rent_uncollected_amt = 0.0
|
||||
rent_status = ""
|
||||
rent_due_date_str = ""
|
||||
rent_payout_date_str = ""
|
||||
|
||||
if rent_receivable:
|
||||
rent_amount_val = round(
|
||||
(rent_receivable.original_amount or 0.0) - (rent_receivable.discount_amount or 0.0), 2
|
||||
)
|
||||
rent_collected_amt = round(rent_receivable.delivered1 or 0.0, 2)
|
||||
rent_uncollected_amt = round(rent_receivable.submitted or 0.0, 2)
|
||||
rent_due_date_str = rent_receivable.due_date.strftime("%Y-%m-%d") if rent_receivable.due_date else ""
|
||||
rent_payout_date_str = rent_receivable.payout_date.strftime("%Y-%m-%d") if rent_receivable.payout_date else ""
|
||||
if rent_receivable.deposit_state == 'paid':
|
||||
rent_status = "已收"
|
||||
elif rent_receivable.deposit_state == 'no_paid':
|
||||
rent_status = "收部分"
|
||||
else:
|
||||
rent_status = "未收"
|
||||
|
||||
# --- 构建违约金信息 ---
|
||||
penalty_amount = round(penalty_rec.original_amount or 0.0, 2)
|
||||
penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2)
|
||||
penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2)
|
||||
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
|
||||
penalty_rec.deposit_state, penalty_rec.deposit_state)
|
||||
|
||||
# 累计统计
|
||||
total_collected += pay_amount
|
||||
|
||||
contract = penalty_rec.contract_id or pml.contract_id
|
||||
lessee_obj = penalty_rec.lessee_id or pml.lessee_id
|
||||
company_obj = penalty_rec.company_id or pml.company_id
|
||||
|
||||
rows.append({
|
||||
"来源": "B",
|
||||
"租金ID": rent_receivable.id if rent_receivable else "",
|
||||
"租金应收日": rent_due_date_str,
|
||||
"租金收款日期": rent_payout_date_str,
|
||||
"租金金额": rent_amount_val,
|
||||
"租金已收": rent_collected_amt,
|
||||
"租金未收": rent_uncollected_amt,
|
||||
"租金状态": rent_status,
|
||||
"违约金金额": penalty_amount,
|
||||
"违约金已收": penalty_collected_amt,
|
||||
"违约金未收": penalty_uncollected_amt,
|
||||
"违约金状态": penalty_status,
|
||||
"违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "",
|
||||
"实收金额": pay_amount,
|
||||
"收款日期": pml.reconciliation_date.strftime("%Y-%m-%d") if pml.reconciliation_date else "",
|
||||
"收款备注": pml.remake1 or "",
|
||||
"收费区间": charging_range,
|
||||
"备注": penalty_rec.remark or "",
|
||||
"合同编号": {
|
||||
'model': 'property.lease.contract',
|
||||
'id': contract.id,
|
||||
'display_name': contract.contract_code,
|
||||
} if contract else "",
|
||||
"物业名称": _build_property_display(contract, prop_name),
|
||||
"承租方名称": {
|
||||
'model': 'property.tenant.information',
|
||||
'id': lessee_obj.id,
|
||||
'display_name': lessee_obj.name,
|
||||
} if lessee_obj else "",
|
||||
"所属公司": company_obj.name if company_obj else "",
|
||||
})
|
||||
|
||||
# ========== 四、构建统计汇总数据 ==========
|
||||
stats = [
|
||||
{'name': '记录总数', 'amount': len(rows)},
|
||||
{'name': '实收违约金', 'amount': round(total_collected, 2)},
|
||||
]
|
||||
|
||||
_logger.info("[PENALTY_MONTH_RECEIVED] ds=%s, de=%s, rows=%d, total=%.2f",
|
||||
ds, de, len(rows), total_collected)
|
||||
_logger.info("[PENALTY_MONTH_RECEIVED] ds=%s, de=%s, source_a_rows=%d, source_b_rows=%d, total=%.2f",
|
||||
ds, de, sum(1 for r in rows if r.get('来源') == 'A'),
|
||||
sum(1 for r in rows if r.get('来源') == 'B'), total_collected)
|
||||
|
||||
return {"rows": rows, "stats": stats}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import datetime # 日期时间处理模块,用于日期解析和计算
|
||||
import datetime # 日期时间处理模块,用于日期解析、计算、格式化
|
||||
import logging # 日志模块,用于调试输出
|
||||
import re # 正则表达式模块,用于解析收费区间字符串
|
||||
|
||||
from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类
|
||||
|
||||
@@ -7,7 +8,7 @@ _logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
|
||||
|
||||
|
||||
class PenaltyYearCumReport(models.Model):
|
||||
_inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型
|
||||
_inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法
|
||||
|
||||
@api.model
|
||||
def get_penalty_year_cum_report(self, params):
|
||||
@@ -15,12 +16,14 @@ class PenaltyYearCumReport(models.Model):
|
||||
违约金本年累计报表
|
||||
|
||||
两路数据源:
|
||||
来源A:property.accounts.receivable 中 is_batch=True 的违约金记录
|
||||
来源A:property.accounts.receivable 中收款类型为违约金 且 is_batch=True 的记录
|
||||
通过 source_receivable_id 关联租金
|
||||
本年累计:取该违约金记录的 receivable.modify.line,reconciliation_date 在查询年内的行
|
||||
来源B:receivable.modify.line 中 is_penalty=True 的记录
|
||||
通过 receivable_id 关联租金(receivable_id 指向的就是那笔租金应收记录)
|
||||
本年累计:reconciliation_date 在查询年内
|
||||
通过其 receivable_id(违约金应收单)的 charging_range 反推租金的收款日期,
|
||||
在 receivable.modify.line 中找到匹配的租金记录(同承租方+同物业+同合同+同收款日期)
|
||||
|
||||
本年累计:来源A取 reconciliation_date 在查询年内的 modify.line;
|
||||
来源B取 is_penalty 且 reconciliation_date 在查询年内的 modify.line
|
||||
"""
|
||||
|
||||
# ========== 一、参数解析 ==========
|
||||
@@ -74,6 +77,30 @@ class PenaltyYearCumReport(models.Model):
|
||||
return False
|
||||
return True
|
||||
|
||||
# ========== 辅助函数:modify.line 内存过滤(来源B用)==========
|
||||
def _match_filters_ml(ml, prop_name_str):
|
||||
"""对 receivable.modify.line 记录做内存过滤"""
|
||||
if keyword:
|
||||
kw = keyword.lower()
|
||||
match = False
|
||||
if ml.contract_id and ml.contract_id.contract_code and kw in ml.contract_id.contract_code.lower():
|
||||
match = True
|
||||
if prop_name_str and kw in prop_name_str.lower():
|
||||
match = True
|
||||
if ml.lessee_id and ml.lessee_id.name and kw in ml.lessee_id.name.lower():
|
||||
match = True
|
||||
if not match:
|
||||
return False
|
||||
if partner_id and ml.precinct_id and ml.precinct_id.id != int(partner_id):
|
||||
return False
|
||||
if company_id and ml.company_id and ml.company_id.id != int(company_id):
|
||||
return False
|
||||
if lessee_id and (not ml.lessee_id or ml.lessee_id.id != int(lessee_id)):
|
||||
return False
|
||||
if contract_id and (not ml.contract_id or ml.contract_id.id != int(contract_id)):
|
||||
return False
|
||||
return True
|
||||
|
||||
# ========== 辅助函数:构建物业名称显示(穿透对象) ==========
|
||||
def _build_property_display(contract_rec, property_name_str):
|
||||
"""构建物业名称穿透对象"""
|
||||
@@ -85,172 +112,273 @@ class PenaltyYearCumReport(models.Model):
|
||||
}
|
||||
return property_name_str or ""
|
||||
|
||||
# ========== 辅助函数:构建行数据(共用) ==========
|
||||
def _build_row(pay_amount, pay_date, pay_remark, rent_rec, penalty_rec, contract_rec,
|
||||
prop_name, lessee_rec, company_name, source_tag):
|
||||
"""构建报表行字典"""
|
||||
# 租金信息
|
||||
rent_amount = 0.0
|
||||
rent_collected = 0.0
|
||||
rent_uncollected = 0.0
|
||||
rent_status = ""
|
||||
rent_due_date = ""
|
||||
rent_payout_date = ""
|
||||
if rent_rec:
|
||||
rent_amount = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2)
|
||||
rent_collected = round(rent_rec.delivered1 or 0.0, 2)
|
||||
rent_uncollected = round(rent_rec.submitted or 0.0, 2)
|
||||
rent_due_date = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else ""
|
||||
rent_payout_date = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else ""
|
||||
if rent_rec.deposit_state == 'paid':
|
||||
rent_status = "已收"
|
||||
elif rent_rec.deposit_state == 'no_paid':
|
||||
rent_status = "收部分"
|
||||
else:
|
||||
rent_status = "未收"
|
||||
# ========== 辅助函数:解析收费区间字符串 ==========
|
||||
def _parse_charging_range(range_str):
|
||||
"""
|
||||
解析收费区间字符串,提取起止日期
|
||||
|
||||
# 违约金信息
|
||||
penalty_amount = 0.0
|
||||
penalty_collected = 0.0
|
||||
penalty_uncollected = 0.0
|
||||
penalty_status = ""
|
||||
penalty_start_date = ""
|
||||
charging_range = ""
|
||||
remark = ""
|
||||
if penalty_rec:
|
||||
penalty_amount = round(penalty_rec.original_amount or 0.0, 2)
|
||||
penalty_collected = round(penalty_rec.delivered1 or 0.0, 2)
|
||||
penalty_uncollected = round(penalty_rec.submitted or 0.0, 2)
|
||||
penalty_start_date = penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else ""
|
||||
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
|
||||
penalty_rec.deposit_state, penalty_rec.deposit_state)
|
||||
charging_range = penalty_rec.charging_range or ""
|
||||
remark = penalty_rec.remark or ""
|
||||
|
||||
return {
|
||||
"来源": source_tag,
|
||||
"租金ID": rent_rec.id if rent_rec else "",
|
||||
"租金应收日": rent_due_date,
|
||||
"租金收款日期": rent_payout_date,
|
||||
"租金金额": rent_amount,
|
||||
"租金已收": rent_collected,
|
||||
"租金未收": rent_uncollected,
|
||||
"租金状态": rent_status,
|
||||
"违约金金额": penalty_amount,
|
||||
"违约金已收": penalty_collected,
|
||||
"违约金未收": penalty_uncollected,
|
||||
"违约金状态": penalty_status,
|
||||
"违约金应收开始": penalty_start_date,
|
||||
"实收金额": round(pay_amount, 2),
|
||||
"收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "",
|
||||
"收款备注": pay_remark or "",
|
||||
"收费区间": charging_range,
|
||||
"备注": remark,
|
||||
"合同编号": {
|
||||
'model': 'property.lease.contract',
|
||||
'id': contract_rec.id,
|
||||
'display_name': contract_rec.contract_code,
|
||||
} if contract_rec else "",
|
||||
"物业名称": _build_property_display(contract_rec, prop_name),
|
||||
"承租方名称": {
|
||||
'model': 'property.tenant.information',
|
||||
'id': lessee_rec.id,
|
||||
'display_name': lessee_rec.name,
|
||||
} if lessee_rec else "",
|
||||
"所属公司": company_name or "",
|
||||
}
|
||||
格式示例:"2025-10-01 到 2025-11-25共56"
|
||||
返回:(start_date, end_date) 或 (None, None)
|
||||
"""
|
||||
if not range_str:
|
||||
return None, None
|
||||
# 匹配 YYYY-MM-DD 到 YYYY-MM-DD 共NN 格式
|
||||
pattern = r'(\d{4}-\d{2}-\d{2})\s*到\s*(\d{4}-\d{2}-\d{2})'
|
||||
match = re.search(pattern, range_str)
|
||||
if match:
|
||||
try:
|
||||
start_d = datetime.datetime.strptime(match.group(1), '%Y-%m-%d').date()
|
||||
end_d = datetime.datetime.strptime(match.group(2), '%Y-%m-%d').date()
|
||||
return start_d, end_d
|
||||
except ValueError:
|
||||
pass
|
||||
return None, None
|
||||
|
||||
# ========== 初始化 ==========
|
||||
rows = []
|
||||
total_collected = 0.0
|
||||
rows = [] # 报表行数据列表
|
||||
total_collected = 0.0 # 实收违约金合计
|
||||
processed_keys = set() # 去重集合
|
||||
|
||||
# ========== 三、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ==========
|
||||
# 查出 is_batch=True 的违约金记录
|
||||
penalty_a_recs = self.sudo().search([
|
||||
# ========== 二、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ==========
|
||||
# 查出 is_batch=True 且收款类型为违约金的记录
|
||||
penalty_recs = self.sudo().search([
|
||||
('management_type_id.name', '=', '违约金'), # 收款类型为违约金
|
||||
('is_batch', '=', True), # 标记为批量修复的违约金数据
|
||||
('is_invalid', '!=', True), # 排除已作废
|
||||
])
|
||||
|
||||
# 查出这些违约金记录在查询年内的 modify.line
|
||||
if penalty_a_recs:
|
||||
modify_a_lines = self.env['receivable.modify.line'].sudo().search([
|
||||
('receivable_id', 'in', penalty_a_recs.ids), # 关联到 is_batch 违约金记录
|
||||
if penalty_recs:
|
||||
modify_lines = self.env['receivable.modify.line'].sudo().search([
|
||||
('receivable_id', 'in', penalty_recs.ids), # 关联到 is_batch 违约金记录
|
||||
('reconciliation_date', '!=', False), # 有对账日期
|
||||
('reconciliation_date', '>=', ds), # 对账日期 >= 当年1月1日
|
||||
('reconciliation_date', '<=', de), # 对账日期 <= 当年12月31日
|
||||
])
|
||||
|
||||
for ml in modify_a_lines:
|
||||
pay_amount = ml.amount or 0.0
|
||||
for ml in modify_lines:
|
||||
pay_amount = ml.amount or 0.0 # 实收金额
|
||||
if pay_amount <= 0:
|
||||
continue
|
||||
|
||||
penalty_rec = ml.receivable_id # 这就是 is_batch 违约金记录
|
||||
rent_rec = penalty_rec.source_receivable_id # 通过 source_receivable_id 关联租金
|
||||
|
||||
# 物业名称
|
||||
prop_name = penalty_rec.property_name or (rent_rec.property_name if rent_rec else "")
|
||||
# 内存过滤
|
||||
if not _match_filters(penalty_rec, prop_name):
|
||||
continue
|
||||
|
||||
total_collected += pay_amount
|
||||
rows.append(_build_row(
|
||||
pay_amount=pay_amount,
|
||||
pay_date=ml.reconciliation_date,
|
||||
pay_remark=ml.remake1,
|
||||
rent_rec=rent_rec,
|
||||
penalty_rec=penalty_rec,
|
||||
contract_rec=penalty_rec.contract_id,
|
||||
prop_name=prop_name,
|
||||
lessee_rec=penalty_rec.lessee_id,
|
||||
company_name=penalty_rec.company_id.name if penalty_rec.company_id else "",
|
||||
source_tag="A",
|
||||
))
|
||||
# 去重 key
|
||||
key = ('A', ml.id)
|
||||
if key in processed_keys:
|
||||
continue
|
||||
processed_keys.add(key)
|
||||
|
||||
# ========== 四、来源B:receivable.modify.line 中 is_penalty=True 的记录 ==========
|
||||
# 查询年内 is_penalty=True 的付款行
|
||||
modify_b_lines = self.env['receivable.modify.line'].sudo().search([
|
||||
# --- 构建租金信息 ---
|
||||
rent_amount_val = 0.0
|
||||
rent_collected_amt = 0.0
|
||||
rent_uncollected_amt = 0.0
|
||||
rent_status = ""
|
||||
rent_due_date_str = ""
|
||||
rent_payout_date_str = ""
|
||||
if rent_rec:
|
||||
rent_amount_val = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2)
|
||||
rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2)
|
||||
rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2)
|
||||
rent_due_date_str = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else ""
|
||||
rent_payout_date_str = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else ""
|
||||
if rent_rec.deposit_state == 'paid':
|
||||
rent_status = "已收"
|
||||
elif rent_rec.deposit_state == 'no_paid':
|
||||
rent_status = "收部分"
|
||||
else:
|
||||
rent_status = "未收"
|
||||
|
||||
# --- 构建违约金信息 ---
|
||||
penalty_amount = round(penalty_rec.original_amount or 0.0, 2)
|
||||
penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2)
|
||||
penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2)
|
||||
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
|
||||
penalty_rec.deposit_state, penalty_rec.deposit_state)
|
||||
|
||||
# 累计统计
|
||||
total_collected += pay_amount
|
||||
|
||||
contract = penalty_rec.contract_id
|
||||
rows.append({
|
||||
"来源": "A",
|
||||
"租金ID": rent_rec.id if rent_rec else "",
|
||||
"租金应收日": rent_due_date_str,
|
||||
"租金收款日期": rent_payout_date_str,
|
||||
"租金金额": rent_amount_val,
|
||||
"租金已收": rent_collected_amt,
|
||||
"租金未收": rent_uncollected_amt,
|
||||
"租金状态": rent_status,
|
||||
"违约金金额": penalty_amount,
|
||||
"违约金已收": penalty_collected_amt,
|
||||
"违约金未收": penalty_uncollected_amt,
|
||||
"违约金状态": penalty_status,
|
||||
"违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "",
|
||||
"实收金额": round(pay_amount, 2),
|
||||
"收款日期": ml.reconciliation_date.strftime("%Y-%m-%d") if ml.reconciliation_date else "",
|
||||
"收款备注": ml.remake1 or "",
|
||||
"收费区间": penalty_rec.charging_range or "",
|
||||
"备注": penalty_rec.remark or "",
|
||||
"合同编号": {
|
||||
'model': 'property.lease.contract',
|
||||
'id': contract.id,
|
||||
'display_name': contract.contract_code,
|
||||
} if contract else "",
|
||||
"物业名称": _build_property_display(contract, prop_name),
|
||||
"承租方名称": {
|
||||
'model': 'property.tenant.information',
|
||||
'id': penalty_rec.lessee_id.id,
|
||||
'display_name': penalty_rec.lessee_id.name,
|
||||
} if penalty_rec.lessee_id else "",
|
||||
"所属公司": penalty_rec.company_id.name if penalty_rec.company_id else "",
|
||||
})
|
||||
|
||||
# ========== 三、来源B:receivable.modify.line 中 is_penalty=True 的记录 ==========
|
||||
# 通过 charging_range 反查关联租金
|
||||
source_b_domain = [
|
||||
('is_penalty', '=', True), # 标记为违约金付款
|
||||
('receivable_id', '!=', False), # 必须关联应收记录
|
||||
('reconciliation_date', '!=', False), # 有对账日期
|
||||
('reconciliation_date', '>=', ds), # 对账日期 >= 当年1月1日
|
||||
('reconciliation_date', '<=', de), # 对账日期 <= 当年12月31日
|
||||
])
|
||||
]
|
||||
penalty_modify_lines = self.env['receivable.modify.line'].sudo().search(source_b_domain)
|
||||
|
||||
for ml in modify_b_lines:
|
||||
pay_amount = ml.amount or 0.0
|
||||
for pml in penalty_modify_lines:
|
||||
# 取违约金金额(delivered1 字段)
|
||||
pay_amount = round(pml.delivered1 or 0.0, 2)
|
||||
if pay_amount <= 0:
|
||||
continue
|
||||
|
||||
rent_rec = ml.receivable_id # receivable_id 就是关联的那笔租金
|
||||
prop_name = rent_rec.property_name or ""
|
||||
|
||||
# 内存过滤
|
||||
if not _match_filters(rent_rec, prop_name):
|
||||
# 获取违约金对应的应收单(property.accounts.receivable)
|
||||
penalty_rec = pml.receivable_id
|
||||
if not penalty_rec:
|
||||
continue
|
||||
|
||||
total_collected += pay_amount
|
||||
rows.append(_build_row(
|
||||
pay_amount=pay_amount,
|
||||
pay_date=ml.reconciliation_date,
|
||||
pay_remark=ml.remake1,
|
||||
rent_rec=rent_rec,
|
||||
penalty_rec=None, # 来源B没有单独的违约金记录
|
||||
contract_rec=rent_rec.contract_id,
|
||||
prop_name=prop_name,
|
||||
lessee_rec=rent_rec.lessee_id,
|
||||
company_name=rent_rec.company_id.name if rent_rec.company_id else "",
|
||||
source_tag="B",
|
||||
))
|
||||
# --- 读取 charging_range 并解析日期 ---
|
||||
charging_range = penalty_rec.charging_range or ""
|
||||
range_start, range_end = _parse_charging_range(charging_range)
|
||||
if not range_end:
|
||||
# 无法从收费区间解析出日期,跳过这条
|
||||
continue
|
||||
|
||||
# ========== 五、构建统计汇总数据 ==========
|
||||
# --- 通过 charging_range 的结束日期反查租金 modify.line ---
|
||||
# 匹配条件:同一承租方 + 同一合同 + 收款日期=range_end + 非违约金记录
|
||||
rent_ml_domain = [
|
||||
('id', '!=', pml.id), # 排除自身
|
||||
('is_penalty', '=', False), # 非违约金(即租金)
|
||||
('reconciliation_date', '=', range_end), # 收款日期 = 收费区间结束日期
|
||||
]
|
||||
if pml.lessee_id:
|
||||
rent_ml_domain.append(('lessee_id', '=', pml.lessee_id.id)) # 同一承租方
|
||||
if pml.contract_id:
|
||||
rent_ml_domain.append(('contract_id', '=', pml.contract_id.id)) # 同一合同
|
||||
|
||||
rent_mls = self.env['receivable.modify.line'].sudo().search(rent_ml_domain, limit=1)
|
||||
if not rent_mls:
|
||||
continue
|
||||
rent_ml = rent_mls[0]
|
||||
|
||||
# 获取租金对应的应收单
|
||||
rent_receivable = rent_ml.receivable_id
|
||||
|
||||
# 物业名称(优先从违约金应收单取)
|
||||
prop_name = penalty_rec.property_name or (rent_receivable.property_name if rent_receivable else "")
|
||||
|
||||
# 内存过滤(用违约金 modify.line 的信息过滤)
|
||||
if not _match_filters_ml(pml, prop_name):
|
||||
continue
|
||||
|
||||
# 去重 key
|
||||
key = ('B', pml.id)
|
||||
if key in processed_keys:
|
||||
continue
|
||||
processed_keys.add(key)
|
||||
|
||||
# --- 构建租金信息(从租金 modify.line 和对应应收单获取)---
|
||||
rent_amount_val = 0.0
|
||||
rent_collected_amt = 0.0
|
||||
rent_uncollected_amt = 0.0
|
||||
rent_status = ""
|
||||
rent_due_date_str = ""
|
||||
rent_payout_date_str = ""
|
||||
|
||||
if rent_receivable:
|
||||
rent_amount_val = round(
|
||||
(rent_receivable.original_amount or 0.0) - (rent_receivable.discount_amount or 0.0), 2
|
||||
)
|
||||
rent_collected_amt = round(rent_receivable.delivered1 or 0.0, 2)
|
||||
rent_uncollected_amt = round(rent_receivable.submitted or 0.0, 2)
|
||||
rent_due_date_str = rent_receivable.due_date.strftime("%Y-%m-%d") if rent_receivable.due_date else ""
|
||||
rent_payout_date_str = rent_receivable.payout_date.strftime("%Y-%m-%d") if rent_receivable.payout_date else ""
|
||||
if rent_receivable.deposit_state == 'paid':
|
||||
rent_status = "已收"
|
||||
elif rent_receivable.deposit_state == 'no_paid':
|
||||
rent_status = "收部分"
|
||||
else:
|
||||
rent_status = "未收"
|
||||
|
||||
# --- 构建违约金信息 ---
|
||||
penalty_amount = round(penalty_rec.original_amount or 0.0, 2)
|
||||
penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2)
|
||||
penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2)
|
||||
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
|
||||
penalty_rec.deposit_state, penalty_rec.deposit_state)
|
||||
|
||||
# 累计统计
|
||||
total_collected += pay_amount
|
||||
|
||||
contract = penalty_rec.contract_id or pml.contract_id
|
||||
lessee_obj = penalty_rec.lessee_id or pml.lessee_id
|
||||
company_obj = penalty_rec.company_id or pml.company_id
|
||||
|
||||
rows.append({
|
||||
"来源": "B",
|
||||
"租金ID": rent_receivable.id if rent_receivable else "",
|
||||
"租金应收日": rent_due_date_str,
|
||||
"租金收款日期": rent_payout_date_str,
|
||||
"租金金额": rent_amount_val,
|
||||
"租金已收": rent_collected_amt,
|
||||
"租金未收": rent_uncollected_amt,
|
||||
"租金状态": rent_status,
|
||||
"违约金金额": penalty_amount,
|
||||
"违约金已收": penalty_collected_amt,
|
||||
"违约金未收": penalty_uncollected_amt,
|
||||
"违约金状态": penalty_status,
|
||||
"违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "",
|
||||
"实收金额": pay_amount,
|
||||
"收款日期": pml.reconciliation_date.strftime("%Y-%m-%d") if pml.reconciliation_date else "",
|
||||
"收款备注": pml.remake1 or "",
|
||||
"收费区间": charging_range,
|
||||
"备注": penalty_rec.remark or "",
|
||||
"合同编号": {
|
||||
'model': 'property.lease.contract',
|
||||
'id': contract.id,
|
||||
'display_name': contract.contract_code,
|
||||
} if contract else "",
|
||||
"物业名称": _build_property_display(contract, prop_name),
|
||||
"承租方名称": {
|
||||
'model': 'property.tenant.information',
|
||||
'id': lessee_obj.id,
|
||||
'display_name': lessee_obj.name,
|
||||
} if lessee_obj else "",
|
||||
"所属公司": company_obj.name if company_obj else "",
|
||||
})
|
||||
|
||||
# ========== 四、构建统计汇总数据 ==========
|
||||
stats = [
|
||||
{'name': '记录总数', 'amount': len(rows)},
|
||||
{'name': '实收违约金', 'amount': round(total_collected, 2)},
|
||||
]
|
||||
|
||||
_logger.info("[PENALTY_YEAR_CUM] year=%d, ds=%s, de=%s, rows=%d, total=%.2f",
|
||||
year, ds, de, len(rows), total_collected)
|
||||
_logger.info("[PENALTY_YEAR_CUM] year=%d, ds=%s, de=%s, source_a_rows=%d, source_b_rows=%d, total=%.2f",
|
||||
year, ds, de, sum(1 for r in rows if r.get('来源') == 'A'),
|
||||
sum(1 for r in rows if r.get('来源') == 'B'), total_collected)
|
||||
|
||||
return {"rows": rows, "stats": stats}
|
||||
|
||||
Reference in New Issue
Block a user