537 lines
26 KiB
Python
537 lines
26 KiB
Python
import datetime # 日期时间处理模块,用于日期解析、计算、格式化
|
||
import logging # 日志模块,用于调试输出
|
||
|
||
from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类
|
||
from .report_utils import _id_list # 导入 ID 列表解析工具函数,支持多选参数
|
||
|
||
_logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
|
||
|
||
|
||
class PenaltyArrearsReport(models.Model):
|
||
_inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法
|
||
|
||
@api.model
|
||
def get_penalty_arrears_report(self, params):
|
||
"""
|
||
违约金累计欠收报表
|
||
|
||
两路数据源:
|
||
来源A:property.accounts.receivable 中收款类型为违约金 且 is_batch=True 的记录
|
||
通过 source_receivable_id 关联租金
|
||
来源B:receivable.modify.line 中 is_penalty=True 的记录
|
||
通过 receivable_id 关联租金(receivable_id 指向的就是那笔租金应收记录)
|
||
|
||
累计欠收逻辑:
|
||
- 来源A(is_batch违约金记录):取 deposit_state in [unpaid, no_paid] 的记录,
|
||
展示违约金金额、已收、未收
|
||
- 来源B(modify.line is_penalty):按租金分组,取 reconciliation_date <= 查询日期 的实收,
|
||
与违约金记录关联,展示未收部分
|
||
|
||
计算规则(用于动态违约金计算,无 source_receivable_id 时):
|
||
a) 应收月的次月1号开始计算违约金
|
||
b) 违约金按每日千分之0.1%(0.001)计算
|
||
c) 租金部分支付后,剩余部分从前一次缴费日期次日开始重新计算
|
||
"""
|
||
|
||
# ========== 一、参数解析 ==========
|
||
date_end = params.get("date_end") # 查询截止日期(如 "2026-01-01 23:59:59")
|
||
summary_only = params.get("summary_only", False)
|
||
query_date = None # 查询日期 date 对象,初始化为空
|
||
if date_end: # 如果有截止日期参数
|
||
query_date = datetime.datetime.fromisoformat(date_end).date() # 将字符串转为 date 对象
|
||
|
||
# ========== 二、构建公共筛选条件 ==========
|
||
keyword = params.get("keyword") # 关键字搜索
|
||
condition = params.get('condition') # 租赁类型
|
||
partner_id = _id_list(params.get('partner_id')) # 解析片区 ID 为整数列表(多选支持)
|
||
company_id = _id_list(params.get('company_id')) # 公司 ID
|
||
property_id = params.get('property_id') # 物业 ID
|
||
lessee_id = params.get('lessee_id') # 承租方 ID
|
||
contract_id = params.get('contract_id') # 合同 ID
|
||
|
||
# ========== 辅助函数:内存过滤(关键字、条件等) ==========
|
||
def _match_filters(rec, property_name_str):
|
||
"""对 property.accounts.receivable 记录做内存过滤,返回 True 表示通过"""
|
||
if keyword:
|
||
kw = keyword.lower()
|
||
match = False
|
||
if rec.contract_id and rec.contract_id.contract_code and kw in rec.contract_id.contract_code.lower():
|
||
match = True
|
||
if property_name_str and kw in property_name_str.lower():
|
||
match = True
|
||
if rec.lessee_id and rec.lessee_id.name and kw in rec.lessee_id.name.lower():
|
||
match = True
|
||
if not match:
|
||
return False
|
||
if condition and rec.tenure_type != condition:
|
||
return False
|
||
if partner_id and rec.precinct_id.id not in partner_id: # 片区过滤(多选支持)
|
||
return False
|
||
if company_id and rec.company_id.id not in company_id:
|
||
return False
|
||
if property_id and int(property_id) not in (rec.property_ids.ids or []):
|
||
return False
|
||
if lessee_id and (not rec.lessee_id or rec.lessee_id.id != int(lessee_id)):
|
||
return False
|
||
if contract_id and (not rec.contract_id or rec.contract_id.id != int(contract_id)):
|
||
return False
|
||
return True
|
||
|
||
# ========== 辅助函数:构建物业名称显示(穿透对象) ==========
|
||
def _build_property_display(contract_rec, property_name_str):
|
||
"""构建物业名称穿透对象"""
|
||
if contract_rec and contract_rec.lease_ids and contract_rec.lease_ids[0].address_id:
|
||
return {
|
||
'model': 'yuthon.property',
|
||
'id': contract_rec.lease_ids[0].address_id.id,
|
||
'display_name': property_name_str or "",
|
||
}
|
||
return property_name_str or ""
|
||
|
||
# ========== 辅助函数:获取违约金计算比例 ==========
|
||
def _get_penalty_rate(contract_rec):
|
||
"""从合同配置中获取违约金日利率,默认 0.001"""
|
||
if contract_rec and contract_rec.percentage_liquidated_damages:
|
||
try:
|
||
rate_val = float(contract_rec.percentage_liquidated_damages.name)
|
||
if rate_val > 0:
|
||
return rate_val
|
||
except (ValueError, TypeError):
|
||
pass
|
||
return 0.001
|
||
|
||
# ========== 初始化报表数据 ==========
|
||
rows = [] # 报表行数据列表
|
||
total_penalty = 0.0 # 违约金总额累计
|
||
total_collected = 0.0 # 违约金已收累计
|
||
total_uncollected = 0.0 # 违约金未收累计
|
||
# 用于去重:记录已经处理过的(来源, 主键)组合
|
||
processed_keys = set()
|
||
|
||
# ========== 三、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ==========
|
||
# 搜索 is_batch=True 且收款类型为违约金的未收/收部分记录
|
||
source_a_domain = [
|
||
('management_type_id.name', '=', '违约金'), # 收款类型为违约金
|
||
('is_batch', '=', True), # 标记为批量修复的违约金数据
|
||
('deposit_state', 'in', ['unpaid', 'no_paid']), # 未收或收部分
|
||
('is_invalid', '!=', True), # 排除已作废
|
||
]
|
||
if query_date:
|
||
source_a_domain.append(('due_date', '<=', query_date)) # due_date <= 查询日期
|
||
|
||
penalty_a_recs = self.sudo().search(source_a_domain, order="contract_id asc, due_date asc")
|
||
|
||
for penalty_rec in penalty_a_recs:
|
||
# 通过 source_receivable_id 关联租金
|
||
rent_rec = penalty_rec.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
|
||
|
||
# 去重 key
|
||
key = ('A', penalty_rec.id)
|
||
if key in processed_keys:
|
||
continue
|
||
processed_keys.add(key)
|
||
|
||
# 违约金金额、已收、未收
|
||
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)
|
||
|
||
# 租金信息
|
||
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 = "未收"
|
||
|
||
# 累计统计
|
||
total_penalty += penalty_amount
|
||
total_collected += penalty_collected_amt
|
||
total_uncollected += penalty_uncollected_amt
|
||
|
||
contract = penalty_rec.contract_id
|
||
if not summary_only:
|
||
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 "",
|
||
"收费区间": 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 的记录 ==========
|
||
# 取 is_penalty=True 的 modify.line,其 receivable_id 就是关联的那笔租金
|
||
# 累计欠收:需要找出有未收违约金的租金记录
|
||
# 思路:通过 is_penalty=True 的 modify.line 找到租金记录,
|
||
# 再查该租金对应的违约金收款情况(已收 vs 应收)
|
||
|
||
# 搜索所有 is_penalty=True 的 modify.line(不限日期,用于统计累计已收)
|
||
source_b_domain = [
|
||
('is_penalty', '=', True), # 标记为违约金付款
|
||
('receivable_id', '!=', False), # 必须关联应收记录
|
||
]
|
||
if query_date:
|
||
source_b_domain.append(('reconciliation_date', '<=', query_date)) # 截止查询日期
|
||
|
||
modify_b_lines = self.env['receivable.modify.line'].sudo().search(source_b_domain)
|
||
|
||
# 按 receivable_id(租金记录)分组,统计每笔租金已收到的违约金
|
||
# rent_id → 已收违约金金额合计
|
||
rent_penalty_collected = {} # rent_id → float
|
||
rent_modify_lines = {} # rent_id → [modify.line]
|
||
for ml in modify_b_lines:
|
||
rid = ml.receivable_id.id
|
||
if rid not in rent_penalty_collected:
|
||
rent_penalty_collected[rid] = 0.0
|
||
rent_modify_lines[rid] = []
|
||
rent_penalty_collected[rid] += (ml.amount or 0.0)
|
||
rent_modify_lines[rid].append(ml)
|
||
|
||
# 对这些租金记录,动态计算违约金总额,判断是否有未收
|
||
# 先查出涉及的租金记录
|
||
rent_ids_b = list(rent_penalty_collected.keys())
|
||
# ========== 优化:批量查询所有租金对应的非违约金收款行,消除N+1 ==========
|
||
all_modify_lines = self.env['receivable.modify.line'].sudo().search([
|
||
('receivable_id', 'in', rent_ids_b),
|
||
('reconciliation_date', '!=', False),
|
||
('is_penalty', '=', False),
|
||
], order='receivable_id, reconciliation_date asc')
|
||
# 按 receivable_id 分组
|
||
modify_lines_by_rent = {}
|
||
for ml in all_modify_lines:
|
||
rid = ml.receivable_id.id
|
||
if rid not in modify_lines_by_rent:
|
||
modify_lines_by_rent[rid] = []
|
||
modify_lines_by_rent[rid].append(ml)
|
||
# ======================================================================
|
||
|
||
rent_recs_b = self.sudo().browse(rent_ids_b) if rent_ids_b else self.sudo().browse([])
|
||
|
||
for rent_rec in rent_recs_b:
|
||
# 基础检查:排除已作废记录
|
||
if rent_rec.is_invalid:
|
||
continue
|
||
|
||
# 内存过滤
|
||
prop_name = rent_rec.property_name or ""
|
||
if not _match_filters(rent_rec, prop_name):
|
||
continue
|
||
|
||
due_date = rent_rec.due_date
|
||
if not due_date:
|
||
continue
|
||
|
||
# 内存时间过滤:due_date <= 查询日期
|
||
if query_date and due_date > query_date:
|
||
continue
|
||
|
||
# --- 计算违约金起始日期:应收月的次月1号 ---
|
||
if due_date.month == 12:
|
||
penalty_start = datetime.date(due_date.year + 1, 1, 1)
|
||
else:
|
||
penalty_start = datetime.date(due_date.year, due_date.month + 1, 1)
|
||
|
||
# 违约金起始日期 > 查询日期,还没开始产生违约金
|
||
if query_date and penalty_start > query_date:
|
||
continue
|
||
|
||
# --- 获取违约金计算比例 ---
|
||
penalty_rate = _get_penalty_rate(rent_rec.contract_id)
|
||
|
||
# --- 获取该租金的支付历史(非违约金的 modify lines) ---
|
||
# 从批量预分组字典中取,消除N+1(第233-245行已批量查询并分组)
|
||
modify_lines = modify_lines_by_rent.get(rent_rec.id, [])
|
||
|
||
# --- 分段计算违约金总额 ---
|
||
rent_amount = (rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0)
|
||
current_principal = rent_amount
|
||
current_start = penalty_start
|
||
total_calc_penalty = 0.0
|
||
calc_range_parts = []
|
||
|
||
for line in modify_lines:
|
||
pay_date = line.reconciliation_date
|
||
pay_amount = line.amount or 0.0
|
||
|
||
if pay_date < current_start:
|
||
current_principal = max(0, current_principal - pay_amount)
|
||
continue
|
||
|
||
days = (pay_date - current_start).days + 1
|
||
if days > 0 and current_principal > 0:
|
||
segment_penalty = current_principal * days * penalty_rate
|
||
total_calc_penalty += segment_penalty
|
||
calc_range_parts.append(
|
||
f"{current_start.strftime('%Y-%m-%d')}至{pay_date.strftime('%Y-%m-%d')} "
|
||
f"({days}天×{current_principal:.2f}×{penalty_rate})"
|
||
)
|
||
|
||
current_principal = max(0, current_principal - pay_amount)
|
||
current_start = pay_date + datetime.timedelta(days=1)
|
||
|
||
# 最后一段:从最后一次支付(或起始日)到查询日期
|
||
if current_principal > 0 and query_date and current_start <= query_date:
|
||
days = (query_date - current_start).days + 1
|
||
if days > 0:
|
||
segment_penalty = current_principal * days * penalty_rate
|
||
total_calc_penalty += segment_penalty
|
||
calc_range_parts.append(
|
||
f"{current_start.strftime('%Y-%m-%d')}至{query_date.strftime('%Y-%m-%d')} "
|
||
f"({days}天×{current_principal:.2f}×{penalty_rate})"
|
||
)
|
||
|
||
total_calc_penalty = round(total_calc_penalty, 2)
|
||
if total_calc_penalty <= 0:
|
||
continue
|
||
|
||
# 已收违约金(来自 modify.line is_penalty=True 的金额合计)
|
||
penalty_collected_amt = round(rent_penalty_collected.get(rent_rec.id, 0.0), 2)
|
||
penalty_uncollected_amt = round(max(0, total_calc_penalty - penalty_collected_amt), 2)
|
||
|
||
# 仅展示有未收违约金的记录(累计欠收报表)
|
||
if penalty_uncollected_amt <= 0:
|
||
continue
|
||
|
||
# 去重 key(避免与来源A重复)
|
||
key = ('B', rent_rec.id)
|
||
if key in processed_keys:
|
||
continue
|
||
processed_keys.add(key)
|
||
|
||
# 租金信息
|
||
rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2)
|
||
rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2)
|
||
rent_status = ""
|
||
if rent_rec.deposit_state == 'paid':
|
||
rent_status = "已收"
|
||
elif rent_rec.deposit_state == 'no_paid':
|
||
rent_status = "收部分"
|
||
else:
|
||
rent_status = "未收"
|
||
|
||
# 累计统计
|
||
total_penalty += total_calc_penalty
|
||
total_collected += penalty_collected_amt
|
||
total_uncollected += penalty_uncollected_amt
|
||
|
||
contract = rent_rec.contract_id
|
||
if not summary_only:
|
||
rows.append({
|
||
"来源": "B",
|
||
"租金ID": rent_rec.id,
|
||
"租金应收日": due_date.strftime("%Y-%m-%d"),
|
||
"租金收款日期": rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "",
|
||
"租金金额": round(rent_amount, 2),
|
||
"租金已收": rent_collected_amt,
|
||
"租金未收": rent_uncollected_amt,
|
||
"租金状态": rent_status,
|
||
"违约金金额": total_calc_penalty,
|
||
"违约金已收": penalty_collected_amt,
|
||
"违约金未收": penalty_uncollected_amt,
|
||
"违约金状态": "动态" if not rent_rec.payout_date else "未收",
|
||
"违约金应收开始": penalty_start.strftime("%Y-%m-%d"),
|
||
"收费区间": "\n".join(calc_range_parts) if calc_range_parts else "",
|
||
"备注": "",
|
||
"合同编号": {
|
||
'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': rent_rec.lessee_id.id,
|
||
'display_name': rent_rec.lessee_id.name,
|
||
} if rent_rec.lessee_id else "",
|
||
"所属公司": rent_rec.company_id.name if rent_rec.company_id else "",
|
||
})
|
||
|
||
# ========== 五、来源C:从未收/收部分租金直接出发,动态计算违约金 ==========
|
||
# 覆盖场景:租金一直没交钱,既没有 is_batch 违约金应收单(来源A),
|
||
# 也没有任何 modify.line 收款记录(来源B),但产生了动态违约金
|
||
# 入口:property.accounts.receivable 中 deposit_state in [unpaid, no_paid] 的租金记录
|
||
# 且不在来源A、来源B中出现过的
|
||
|
||
# --- 5.1 构建已处理的租金ID集合 ---
|
||
# 来源A中关联到的租金ID
|
||
source_a_rent_ids = set()
|
||
for penalty_rec in penalty_a_recs:
|
||
if penalty_rec.source_receivable_id:
|
||
source_a_rent_ids.add(penalty_rec.source_receivable_id.id)
|
||
# 来源B中涉及到的租金ID(即 rent_penalty_collected 的 key 集合)
|
||
source_b_rent_ids = set(rent_penalty_collected.keys())
|
||
|
||
# --- 5.2 查询未收/收部分的租金记录(排除已处理过的)---
|
||
# 收款类型为"租金"的记录(非违约金),且状态是未收或收部分
|
||
rent_domain = [
|
||
('management_type_id.name', '!=', '违约金'), # 收款类型非违约金(即租金)
|
||
('deposit_state', 'in', ['unpaid', 'no_paid']), # 未收或收部分
|
||
('is_invalid', '!=', True), # 排除作废
|
||
]
|
||
if query_date:
|
||
rent_domain.append(('due_date', '<=', query_date)) # 应收日已过,才开始产生违约金
|
||
|
||
unpaid_rent_recs = self.sudo().search(rent_domain, order="contract_id asc, due_date asc")
|
||
|
||
for rent_rec in unpaid_rent_recs:
|
||
# 跳过已经在来源A、B中处理过的租金
|
||
if rent_rec.id in source_a_rent_ids or rent_rec.id in source_b_rent_ids:
|
||
continue
|
||
|
||
# 内存过滤
|
||
prop_name = rent_rec.property_name or ""
|
||
if not _match_filters(rent_rec, prop_name):
|
||
continue
|
||
|
||
due_date = rent_rec.due_date
|
||
if not due_date:
|
||
continue
|
||
|
||
# --- 计算违约金起始日期:应收月的次月1号 ---
|
||
if due_date.month == 12:
|
||
penalty_start = datetime.date(due_date.year + 1, 1, 1)
|
||
else:
|
||
penalty_start = datetime.date(due_date.year, due_date.month + 1, 1)
|
||
|
||
# 违约金起始日期 > 查询日期,还没开始产生违约金
|
||
if query_date and penalty_start > query_date:
|
||
continue
|
||
|
||
# --- 获取违约金计算比例 ---
|
||
penalty_rate = _get_penalty_rate(rent_rec.contract_id)
|
||
|
||
# --- 动态计算违约金总额 ---
|
||
# 这笔租金没有任何 modify.line 收款记录,所以本金就是全额
|
||
rent_amount = (rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0)
|
||
current_principal = rent_amount
|
||
total_calc_penalty = 0.0
|
||
calc_range_parts = []
|
||
|
||
# 从 penalty_start 到查询日期,整段计算
|
||
if current_principal > 0 and query_date and penalty_start <= query_date:
|
||
days = (query_date - penalty_start).days + 1
|
||
if days > 0:
|
||
segment_penalty = current_principal * days * penalty_rate
|
||
total_calc_penalty += segment_penalty
|
||
calc_range_parts.append(
|
||
f"{penalty_start.strftime('%Y-%m-%d')}至{query_date.strftime('%Y-%m-%d')} "
|
||
f"({days}天×{current_principal:.2f}×{penalty_rate})"
|
||
)
|
||
|
||
total_calc_penalty = round(total_calc_penalty, 2)
|
||
if total_calc_penalty <= 0:
|
||
continue
|
||
|
||
# 来源C:没有收到过任何钱,所以已收=0,未收=总额
|
||
penalty_collected_amt = 0.0
|
||
penalty_uncollected_amt = total_calc_penalty
|
||
|
||
# 去重 key
|
||
key = ('C', rent_rec.id)
|
||
if key in processed_keys:
|
||
continue
|
||
processed_keys.add(key)
|
||
|
||
# 租金信息
|
||
rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2)
|
||
rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2)
|
||
rent_status = ""
|
||
if rent_rec.deposit_state == 'paid':
|
||
rent_status = "已收"
|
||
elif rent_rec.deposit_state == 'no_paid':
|
||
rent_status = "收部分"
|
||
else:
|
||
rent_status = "未收"
|
||
|
||
# 累计统计
|
||
total_penalty += total_calc_penalty
|
||
total_uncollected += penalty_uncollected_amt
|
||
|
||
contract = rent_rec.contract_id
|
||
if not summary_only:
|
||
rows.append({
|
||
"来源": "C",
|
||
"租金ID": rent_rec.id,
|
||
"租金应收日": due_date.strftime("%Y-%m-%d"),
|
||
"租金收款日期": rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "",
|
||
"租金金额": round(rent_amount, 2),
|
||
"租金已收": rent_collected_amt,
|
||
"租金未收": rent_uncollected_amt,
|
||
"租金状态": rent_status,
|
||
"违约金金额": total_calc_penalty,
|
||
"违约金已收": penalty_collected_amt,
|
||
"违约金未收": penalty_uncollected_amt,
|
||
"违约金状态": "动态", # 纯动态计算,无任何收款记录
|
||
"违约金应收开始": penalty_start.strftime("%Y-%m-%d"),
|
||
"收费区间": "\n".join(calc_range_parts) if calc_range_parts else "",
|
||
"备注": "未收租金,动态计算违约金",
|
||
"合同编号": {
|
||
'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': rent_rec.lessee_id.id,
|
||
'display_name': rent_rec.lessee_id.name,
|
||
} if rent_rec.lessee_id else "",
|
||
"所属公司": rent_rec.company_id.name if rent_rec.company_id else "",
|
||
})
|
||
|
||
# ========== 六、构建统计汇总数据 ==========
|
||
stats = [
|
||
{'name': '记录总数', 'amount': len(rows)},
|
||
{'name': '违约金总额', 'amount': round(total_penalty, 2)},
|
||
{'name': '违约金已收', 'amount': round(total_collected, 2)},
|
||
{'name': '违约金未收', 'amount': round(total_uncollected, 2)},
|
||
]
|
||
|
||
_logger.info("[PENALTY_ARREARS] query_date=%s, source_a=%d, source_b_rents=%d, source_c_rents=%d, total_rows=%d",
|
||
query_date, len(penalty_a_recs), len(rent_recs_b), len(unpaid_rent_recs), len(rows))
|
||
|
||
return {"rows": rows, "stats": stats}
|