违约金

This commit is contained in:
李鹏宇
2026-06-13 17:11:16 +08:00
parent d4f7757e59
commit fdf5d8193e
3 changed files with 553 additions and 594 deletions
+254 -311
View File
@@ -12,18 +12,24 @@ class PenaltyArrearsReport(models.Model):
@api.model
def get_penalty_arrears_report(self, params):
"""
违约金累计欠收报表(反查模式)
违约金累计欠收报表
核心逻辑:
1. Track1 — 仍有欠款的租金记录 → 分段动态计算违约金到查询日期
2. Track2 — 已结清租金的未收违约金记录 → 取违约金记录的固定金额
3. 关联方式:反查 — 同一合同+同一承租方,违约金.due_date == 租金.payout_date
无payout_date的租金 = 一直没交钱 → 动态违约金(实时计算到查询月)
4. 不再使用 source_receivable_id / rent_id 作为主要关联方式
5. 计算规则:
a) 应收月的次月1号开始计算违约金
b) 违约金按每日千分之0.1%(0.001)计算
c) 租金部分支付后,剩余部分从前一次缴费日期次日开始重新计算
两路数据源:
来源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) 租金部分支付后,剩余部分从前一次缴费日期次日开始重新计算
"""
# ========== 一、参数解析 ==========
@@ -41,121 +47,244 @@ class PenaltyArrearsReport(models.Model):
lessee_id = params.get('lessee_id') # 承租方 ID
contract_id = params.get('contract_id') # 合同 ID
# 公共 domain 片段生成函数(租金和违约金共用)
def _append_common_filters(domain):
"""将公共筛选条件追加到 domain 中"""
if keyword: # 关键字:合同编号、物业名称、承租方三选一
domain += ['|', '|',
('contract_id.contract_code', 'ilike', keyword),
('property_name', 'ilike', keyword),
('lessee_id.name', 'ilike', keyword),
]
if condition: # 租赁类型
domain.append(('tenure_type', '=', condition))
if partner_id: # 片区
domain.append(('precinct_id', '=', partner_id))
if company_id: # 公司
domain.append(('company_id', '=', company_id))
if property_id: # 物业
domain.append(('property_ids', 'in', [property_id]))
if lessee_id: # 承租方
domain.append(('lessee_id', '=', lessee_id))
if contract_id: # 合同
domain.append(('contract_id', '=', contract_id))
return domain
# ========== 辅助函数:内存过滤(关键字、条件等) ==========
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 != int(partner_id):
return False
if company_id and rec.company_id.id != int(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
# ========== 三、搜索仍有欠款的租金记录(Track1 数据源)==========
rent_domain = [
('management_type_id.name', '=', '租金'), # 费用类型为租金
('deposit_state', 'in', ['unpaid', 'no_paid']), # 状态为未收或收部分
('is_liquidated_damages', '=', True), # 允许生成违约金标记
('is_invalid', '!=', True), # 排除已作废记录
]
rent_domain = _append_common_filters(rent_domain) # 追加公共筛选条件
# ========== 辅助函数:构建物业名称显示(穿透对象) ==========
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 ""
# 搜索租金记录,按合同+应收日期升序
rent_lines = self.sudo().search(rent_domain, order="contract_id asc, due_date asc")
# ========== 辅助函数:获取违约金计算比例 ==========
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
# 内存时间过滤:只保留 due_date <= 查询日期的租金记录
if query_date:
rent_lines = rent_lines.filtered(lambda l: l.due_date and l.due_date <= query_date)
# ========== 四、预查违约金记录,建立反查索引 ==========
# 一次性查出所有非作废的违约金记录
all_penalties = self.sudo().search([
('management_type_id.name', '=', '违约金'), # 类型为违约金
('is_invalid', '!=', True), # 排除已作废
])
# 反查索引:(contract_id, lessee_id, due_date) → penalty_rec
# 规则:同一合同+同一承租方,违约金的due_date == 租金的payout_date
# 即违约金.due_date 就是 产生该违约金的那笔租金的收款日期
penalty_index = {} # 初始化反查索引
for p in all_penalties:
if not p.contract_id or not p.lessee_id or not p.due_date:
continue
key = (p.contract_id.id, p.lessee_id.id, p.due_date) # 合同+承租方+应收开始日期
if key not in penalty_index: # 取第一条匹配(避免重复)
penalty_index[key] = p
# ========== 五、初始化报表数据 ==========
# ========== 初始化报表数据 ==========
rows = [] # 报表行数据列表
total_penalty = 0.0 # 违约金总额累计
total_collected = 0.0 # 违约金已收累计
total_uncollected = 0.0 # 违约金未收累计
processed_penalty_ids = set() # 已处理的违约金记录 ID 集合(避免 Track2 重复计算)
# 用于去重:记录已经处理过的(来源, 主键)组合
processed_keys = set()
# ========== 六、Track1:仍有欠款的租金记录 → 分段动态计算违约金 ==========
for rent_rec in rent_lines:
due_date = rent_rec.due_date # 租金应收日期
# ========== 三、来源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
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())
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
# --- 6.1 计算违约金起始日期:应收月的次月1号 ---
# 内存时间过滤: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) # 次年1月1日
penalty_start = datetime.date(due_date.year + 1, 1, 1)
else:
penalty_start = datetime.date(due_date.year, due_date.month + 1, 1) # 次月1日
penalty_start = datetime.date(due_date.year, due_date.month + 1, 1)
# 违约金起始日期 > 查询日期,还没开始产生违约金
if query_date and penalty_start > query_date:
continue
# --- 6.2 获取违约金计算比例 ---
penalty_rate = 0.001 # 默认每日千分之0.1%
contract = rent_rec.contract_id
if contract and contract.percentage_liquidated_damages:
try:
rate_val = float(contract.percentage_liquidated_damages.name)
if rate_val > 0:
penalty_rate = rate_val
except (ValueError, TypeError):
pass
# --- 获取违约金计算比例 ---
penalty_rate = _get_penalty_rate(rent_rec.contract_id)
# --- 6.3 获取租金的支付历史(非违约金的 modify lines) ---
# --- 获取该租金的支付历史(非违约金的 modify lines) ---
modify_lines = self.env['receivable.modify.line'].sudo().search([
('receivable_id', '=', rent_rec.id),
('reconciliation_date', '!=', False),
('is_penalty', '=', False),
('is_penalty', '=', False), # 非违约金收款行
], order='reconciliation_date asc')
# --- 6.4 分段计算违约金 ---
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 = [] # 计算区间片段列表
# --- 分段计算违约金总额 ---
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 # 本次支付金额
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
# 计算从 current_start 到 pay_date 的违约金
days = (pay_date - current_start).days + 1
if days > 0 and current_principal > 0:
segment_penalty = current_principal * days * penalty_rate
@@ -165,11 +294,10 @@ class PenaltyArrearsReport(models.Model):
f"({days}天×{current_principal:.2f}×{penalty_rate})"
)
# 规则3:支付后,剩余本金从次日重新计算
current_principal = max(0, current_principal - pay_amount)
current_start = pay_date + datetime.timedelta(days=1)
# --- 6.5 最后一段:从最后一次支付(或起始日)到查询日期 ---
# 最后一段:从最后一次支付(或起始日)到查询日期
if current_principal > 0 and query_date and current_start <= query_date:
days = (query_date - current_start).days + 1
if days > 0:
@@ -181,86 +309,42 @@ class PenaltyArrearsReport(models.Model):
)
total_calc_penalty = round(total_calc_penalty, 2)
if total_calc_penalty <= 0:
continue
# --- 6.6 反查关联违约金记录 ---
# 规则:同一合同+同一承租方,违约金.due_date == 租金.payout_date
# 有payout_date的租金 → 通过 payout_date 反查违约金记录
# 无payout_date的租金 → 一直没交钱 → 动态违约金
penalty_rec = None
if rent_rec.payout_date and rent_rec.contract_id and rent_rec.lessee_id:
# 用租金的 payout_date 作为 key 查找违约金记录
lookup_key = (rent_rec.contract_id.id, rent_rec.lessee_id.id, rent_rec.payout_date)
penalty_rec = penalty_index.get(lookup_key)
# 已收违约金(来自 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_rec:
processed_penalty_ids.add(penalty_rec.id) # 标记已处理
# 仅展示有未收违约金的记录(累计欠收报表)
if penalty_uncollected_amt <= 0:
continue
# --- 6.7 获取已收违约金 ---
penalty_collected = 0.0
if penalty_rec:
p_modify = self.env['receivable.modify.line'].sudo().search([
('receivable_id', '=', penalty_rec.id),
('reconciliation_date', '!=', False),
])
penalty_collected = round(sum(l.amount or 0.0 for l in p_modify), 2)
# 去重 key(避免与来源A重复)
key = ('B', rent_rec.id)
if key in processed_keys:
continue
processed_keys.add(key)
penalty_uncollected = round(max(0, total_calc_penalty - penalty_collected), 2)
# --- 6.8 租金信息 ---
# 租金信息
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.submitted == 0:
if rent_rec.deposit_state == 'paid':
rent_status = "已收"
elif rent_rec.delivered1 and rent_rec.delivered1 > 0:
elif rent_rec.deposit_state == 'no_paid':
rent_status = "收部分"
else:
rent_status = "未收"
# --- 6.9 违约金状态 ---
penalty_status = ""
if penalty_rec:
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
penalty_rec.deposit_state, penalty_rec.deposit_state)
else:
# 无payout_date = 一直没交钱 = 动态违约金
penalty_status = "动态"
# --- 6.10 违约金应收开始日期 ---
if penalty_rec and penalty_rec.due_date:
penalty_due_date_str = penalty_rec.due_date.strftime("%Y-%m-%d")
else:
penalty_due_date_str = penalty_start.strftime("%Y-%m-%d")
# --- 6.11 累计统计 ---
# 累计统计
total_penalty += total_calc_penalty
total_collected += penalty_collected
total_uncollected += penalty_uncollected
total_collected += penalty_collected_amt
total_uncollected += penalty_uncollected_amt
# --- 6.12 构建物业名称显示 ---
property_display = ""
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
property_display = {
'model': 'yuthon.property',
'id': contract.lease_ids[0].address_id.id,
'display_name': rent_rec.property_name or "",
}
else:
property_display = rent_rec.property_name or ""
# --- 6.13 构建收费区间显示 ---
if calc_range_parts:
charging_range_display = "\n".join(calc_range_parts)
elif penalty_rec and penalty_rec.charging_range:
charging_range_display = penalty_rec.charging_range
else:
charging_range_display = ""
# --- 6.14 构建行数据 ---
contract = rent_rec.contract_id
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 "",
@@ -269,18 +353,18 @@ class PenaltyArrearsReport(models.Model):
"租金未收": rent_uncollected_amt,
"租金状态": rent_status,
"违约金金额": total_calc_penalty,
"违约金已收": penalty_collected,
"违约金未收": penalty_uncollected,
"违约金状态": penalty_status,
"违约金应收开始": penalty_due_date_str,
"收费区间": charging_range_display,
"备注": penalty_rec.remark if penalty_rec else "",
"违约金已收": 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 "",
"物业名称": property_display,
"物业名称": _build_property_display(contract, prop_name),
"承租方名称": {
'model': 'property.tenant.information',
'id': rent_rec.lessee_id.id,
@@ -289,147 +373,7 @@ class PenaltyArrearsReport(models.Model):
"所属公司": rent_rec.company_id.name if rent_rec.company_id else "",
})
# ========== 七、Track2:已结清租金的未收违约金记录 ==========
# 已结清租金对应的违约金金额是固定的(取违约金记录的 original_amount)
penalty_domain_2 = [
('management_type_id.name', '=', '违约金'),
('deposit_state', 'in', ['unpaid', 'no_paid']),
('is_invalid', '!=', True),
]
penalty_domain_2 = _append_common_filters(penalty_domain_2)
# 搜索未收违约金记录
uncollected_penalties = self.sudo().search(penalty_domain_2, order="contract_id asc, due_date asc")
# 内存时间过滤:只保留 due_date <= 查询日期的违约金记录
if query_date:
uncollected_penalties = uncollected_penalties.filtered(
lambda l: l.due_date and l.due_date <= query_date)
# 预查所有租金记录,建立反查索引:payout_date → rent_rec
# 同一合同+同一承租方,违约金.due_date == 租金.payout_date
# 收集 Track2 涉及的合同+承租方组合
track2_contracts = set()
for p in uncollected_penalties:
if p.contract_id and p.lessee_id:
track2_contracts.add((p.contract_id.id, p.lessee_id.id))
# 查询这些合同+承租方下的所有已结清租金
rent_lookup = {} # (contract_id, lessee_id, payout_date) → rent_rec
if track2_contracts:
for cid, lid in track2_contracts:
rents = self.sudo().search([
('contract_id', '=', cid),
('lessee_id', '=', lid),
('management_type_id.name', '=', '租金'),
('deposit_state', '=', 'paid'), # 已结清
('is_invalid', '!=', True),
('payout_date', '!=', False),
])
for r in rents:
if r.payout_date:
key = (cid, lid, r.payout_date)
if key not in rent_lookup:
rent_lookup[key] = r
for penalty_rec in uncollected_penalties:
if penalty_rec.id in processed_penalty_ids:
continue
# --- 7.1 反查关联租金:违约金.due_date == 租金.payout_date ---
rent_rec = None
if penalty_rec.contract_id and penalty_rec.lessee_id and penalty_rec.due_date:
lookup_key = (penalty_rec.contract_id.id, penalty_rec.lessee_id.id, penalty_rec.due_date)
rent_rec = rent_lookup.get(lookup_key)
# 如果租金仍有欠款,说明应该在 Track1 中被处理
if rent_rec and rent_rec.deposit_state in ['unpaid', 'no_paid']:
continue
# --- 7.2 使用违约金记录的金额(固定违约金,租金已结清) ---
penalty_amount = round(penalty_rec.original_amount or 0.0, 2)
# --- 7.3 获取已收违约金 ---
p_modify = self.env['receivable.modify.line'].sudo().search([
('receivable_id', '=', penalty_rec.id),
('reconciliation_date', '!=', False),
])
penalty_collected = round(sum(l.amount or 0.0 for l in p_modify), 2)
penalty_uncollected = round(max(0, penalty_amount - penalty_collected), 2)
# --- 7.4 租金信息 ---
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.submitted == 0:
rent_status = "已收"
elif rent_rec.delivered1 and rent_rec.delivered1 > 0:
rent_status = "收部分"
else:
rent_status = "未收"
# --- 7.5 违约金状态 ---
penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
penalty_rec.deposit_state, penalty_rec.deposit_state)
# --- 7.6 累计统计 ---
total_penalty += penalty_amount
total_collected += penalty_collected
total_uncollected += penalty_uncollected
# --- 7.7 构建物业名称显示 ---
contract = penalty_rec.contract_id
property_display = ""
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
property_display = {
'model': 'yuthon.property',
'id': contract.lease_ids[0].address_id.id,
'display_name': penalty_rec.property_name or "",
}
else:
property_display = penalty_rec.property_name or ""
# --- 7.8 构建行数据 ---
rows.append({
"租金ID": rent_rec.id if rent_rec else 0,
"租金应收日": rent_due_date_str,
"租金收款日期": rent_payout_date_str,
"租金金额": rent_amount_val,
"租金已收": rent_collected_amt,
"租金未收": rent_uncollected_amt,
"租金状态": rent_status,
"违约金金额": penalty_amount,
"违约金已收": penalty_collected,
"违约金未收": penalty_uncollected,
"违约金状态": 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 "",
"物业名称": property_display,
"承租方名称": {
'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 "",
})
# ========== 八、构建统计汇总数据 ==========
# ========== 五、构建统计汇总数据 ==========
stats = [
{'name': '记录总数', 'amount': len(rows)},
{'name': '违约金总额', 'amount': round(total_penalty, 2)},
@@ -437,8 +381,7 @@ class PenaltyArrearsReport(models.Model):
{'name': '违约金未收', 'amount': round(total_uncollected, 2)},
]
_logger.info("[PENALTY_ARREARS] params=%s, track1_rents=%d, track2_penalties=%d, total_rows=%d, stats=%s",
params, len(rent_lines), len(uncollected_penalties) if 'uncollected_penalties' in dir() else 0,
len(rows), stats)
_logger.info("[PENALTY_ARREARS] query_date=%s, source_a=%d, source_b_rents=%d, total_rows=%d",
query_date, len(penalty_a_recs), len(rent_recs_b), len(rows))
return {"rows": rows, "stats": stats}
@@ -12,13 +12,15 @@ class PenaltyMonthReceivedReport(models.Model):
@api.model
def get_penalty_month_received_report(self, params):
"""
违约金本月实收报表(反查模式)
违约金本月实收报表
核心逻辑:
1. 数据源:receivable.modify.line(is_penalty=True, reconciliation_date在查询月内)
2. 每笔付款单独一行,不合并,展示每笔的收款日期和金额
3. 关联租金:反查模式 — 同一合同+同一承租方,违约金.due_date == 租金.payout_date
4. 不再使用 source_receivable_id 作为主要关联方式
两路数据源:
来源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 在查询月内
"""
# ========== 一、参数解析 ==========
@@ -30,8 +32,8 @@ class PenaltyMonthReceivedReport(models.Model):
return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
# 解析查询时间范围为 date 对象
ds = datetime.datetime.fromisoformat(date_start).date()
de = datetime.datetime.fromisoformat(date_end).date()
ds = datetime.datetime.fromisoformat(date_start).date() # 查询月开始
de = datetime.datetime.fromisoformat(date_end).date() # 查询月结束
# 其他筛选条件参数
keyword = params.get("keyword")
@@ -42,123 +44,56 @@ class PenaltyMonthReceivedReport(models.Model):
lessee_id = params.get('lessee_id')
contract_id = params.get('contract_id')
# ========== 二、搜索查询月内违约金付款记录 ==========
modify_lines = self.env['receivable.modify.line'].sudo().search([
('is_penalty', '=', True), # 标记为违约金付款
('reconciliation_date', '!=', False), # 有对账日期
('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始
('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束
])
if not modify_lines:
return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
# ========== 三、预建反查索引 ==========
# 收集所有涉及的合同+承租方组合
contract_lessee_pairs = set()
for line in modify_lines:
rec = line.receivable_id
if rec.contract_id and rec.lessee_id:
contract_lessee_pairs.add((rec.contract_id.id, rec.lessee_id.id))
# 预查这些合同+承租方下的所有租金记录(有 payout_date 的),建立反查索引
# 反查规则:违约金.due_date == 租金.payout_date
rent_lookup = {} # (contract_id, lessee_id, payout_date) → rent_rec
if contract_lessee_pairs:
for cid, lid in contract_lessee_pairs:
rents = self.sudo().search([
('contract_id', '=', cid),
('lessee_id', '=', lid),
('management_type_id.name', '=', '租金'),
('is_invalid', '!=', True),
('payout_date', '!=', False),
])
for r in rents:
if r.payout_date:
key = (cid, lid, r.payout_date)
if key not in rent_lookup:
rent_lookup[key] = r
# ========== 四、构建报表行(每笔付款单独一行) ==========
rows = []
total_collected = 0.0
for line in modify_lines:
receivable = line.receivable_id
pay_amount = round(line.amount or 0.0, 2)
pay_date = line.reconciliation_date
# 收款金额为0或负数则跳过
if pay_amount <= 0:
continue
# --- 4.1 内存过滤:应用关键字和条件筛选 ---
contract = receivable.contract_id
lessee = receivable.lessee_id
# 关键字过滤
# ========== 辅助函数:内存过滤 ==========
def _match_filters(rec, property_name_str):
"""对 property.accounts.receivable 记录做内存过滤"""
if keyword:
kw = keyword.lower()
match = False
if contract and contract.contract_code and kw in contract.contract_code.lower():
if rec.contract_id and rec.contract_id.contract_code and kw in rec.contract_id.contract_code.lower():
match = True
if receivable.property_name and kw in receivable.property_name.lower():
if property_name_str and kw in property_name_str.lower():
match = True
if lessee and lessee.name and kw in lessee.name.lower():
if rec.lessee_id and rec.lessee_id.name and kw in rec.lessee_id.name.lower():
match = True
if not match:
continue
return False
if condition and rec.tenure_type != condition:
return False
if partner_id and rec.precinct_id.id != int(partner_id):
return False
if company_id and rec.company_id.id != int(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
# 租赁类型过滤
if condition and receivable.tenure_type != condition:
continue
# ========== 辅助函数:构建物业名称显示(穿透对象) ==========
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 ""
# 片区过滤
if partner_id and receivable.precinct_id.id != partner_id:
continue
# 公司过滤
if company_id and receivable.company_id.id != company_id:
continue
# 物业过滤
if property_id and property_id not in (receivable.property_ids.ids or []):
continue
# 承租方过滤
if lessee_id and (not lessee or lessee.id != lessee_id):
continue
# 合同过滤
if contract_id and (not contract or contract.id != contract_id):
continue
# --- 4.2 反查关联租金 ---
# receivable 可能是违约金记录或租金记录
is_penalty_rec = receivable.management_type_id.name == '违约金'
rent_rec = None
penalty_rec = None
if is_penalty_rec:
# receivable 本身就是违约金记录
penalty_rec = receivable
# 反查租金:同一合同+同一承租方,违约金.due_date == 租金.payout_date
if penalty_rec.contract_id and penalty_rec.lessee_id and penalty_rec.due_date:
lookup_key = (penalty_rec.contract_id.id, penalty_rec.lessee_id.id, penalty_rec.due_date)
rent_rec = rent_lookup.get(lookup_key)
else:
# receivable 是租金记录
rent_rec = receivable
# --- 4.3 租金信息 ---
# ========== 辅助函数:构建行数据(共用) ==========
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)
@@ -172,13 +107,14 @@ class PenaltyMonthReceivedReport(models.Model):
else:
rent_status = "未收"
# --- 4.4 违约金信息 ---
# 违约金信息
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)
@@ -186,23 +122,11 @@ class PenaltyMonthReceivedReport(models.Model):
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 ""
# --- 4.5 累计统计 ---
total_collected += pay_amount
# --- 4.6 构建物业名称显示 ---
property_display = ""
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
property_display = {
'model': 'yuthon.property',
'id': contract.lease_ids[0].address_id.id,
'display_name': receivable.property_name or "",
}
else:
property_display = receivable.property_name or ""
# --- 4.7 构建行数据 ---
rows.append({
return {
"来源": source_tag,
"租金ID": rent_rec.id if rent_rec else "",
"租金应收日": rent_due_date,
"租金收款日期": rent_payout_date,
@@ -215,24 +139,108 @@ class PenaltyMonthReceivedReport(models.Model):
"违约金未收": penalty_uncollected,
"违约金状态": penalty_status,
"违约金应收开始": penalty_start_date,
"实收金额": pay_amount,
"实收金额": round(pay_amount, 2),
"收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "",
"收款备注": line.remake1 or "",
"收费区间": receivable.charging_range or "",
"备注": receivable.remark or "",
"收款备注": pay_remark or "",
"收费区间": charging_range,
"备注": remark,
"合同编号": {
'model': 'property.lease.contract',
'id': contract.id,
'display_name': contract.contract_code,
} if contract else "",
"物业名称": property_display,
'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.id,
'display_name': lessee.name,
} if lessee else "",
"所属公司": receivable.company_id.name if receivable.company_id else "",
})
'id': lessee_rec.id,
'display_name': lessee_rec.name,
} if lessee_rec else "",
"所属公司": company_name or "",
}
# ========== 初始化 ==========
rows = []
total_collected = 0.0
# ========== 三、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ==========
# 查出 is_batch=True 的违约金记录
penalty_a_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 违约金记录
('reconciliation_date', '!=', False), # 有对账日期
('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始
('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束
])
for ml in modify_a_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",
))
# ========== 四、来源B:receivable.modify.line 中 is_penalty=True 的记录 ==========
# 查询月内 is_penalty=True 的付款行
modify_b_lines = self.env['receivable.modify.line'].sudo().search([
('is_penalty', '=', True), # 标记为违约金付款
('receivable_id', '!=', False), # 必须关联应收记录
('reconciliation_date', '!=', False), # 有对账日期
('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始
('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束
])
for ml in modify_b_lines:
pay_amount = ml.amount or 0.0
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):
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",
))
# ========== 五、构建统计汇总数据 ==========
stats = [
+150 -142
View File
@@ -12,14 +12,15 @@ class PenaltyYearCumReport(models.Model):
@api.model
def get_penalty_year_cum_report(self, params):
"""
违约金本年累计报表(反查模式)
违约金本年累计报表
核心逻辑:
1. 数据源:receivable.modify.line(is_penalty=True, reconciliation_date在查询年内)
2. 查询年范围:当年1月1日 到 当年12月31日(整年)
3. 每笔付款单独一行,不合并,展示每笔的收款日期和金额
4. 关联租金:反查模式 — 同一合同+同一承租方,违约金.due_date == 租金.payout_date
5. 不再使用 source_receivable_id 作为主要关联方式
两路数据源:
来源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 在查询年内
"""
# ========== 一、参数解析 ==========
@@ -31,9 +32,9 @@ class PenaltyYearCumReport(models.Model):
return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
# 从参数中解析年份,确定本年的完整时间范围
de = datetime.datetime.fromisoformat(date_end).date()
year = de.year
ds = datetime.date(year, 1, 1) # 当年1月1日
de_raw = datetime.datetime.fromisoformat(date_end).date()
year = de_raw.year
ds = datetime.date(year, 1, 1) # 当年1月1日
de = datetime.date(year, 12, 31) # 当年12月31日
# 其他筛选条件参数
@@ -45,122 +46,56 @@ class PenaltyYearCumReport(models.Model):
lessee_id = params.get('lessee_id')
contract_id = params.get('contract_id')
# ========== 二、搜索查询年内违约金付款记录 ==========
modify_lines = self.env['receivable.modify.line'].sudo().search([
('is_penalty', '=', True), # 标记为违约金付款
('reconciliation_date', '!=', False), # 有对账日期
('reconciliation_date', '>=', ds),
('reconciliation_date', '<=', de),
])
if not modify_lines:
return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
# ========== 三、预建反查索引 ==========
# 收集所有涉及的合同+承租方组合
contract_lessee_pairs = set()
for line in modify_lines:
rec = line.receivable_id
if rec.contract_id and rec.lessee_id:
contract_lessee_pairs.add((rec.contract_id.id, rec.lessee_id.id))
# 预查这些合同+承租方下的所有租金记录(有 payout_date 的),建立反查索引
# 反查规则:违约金.due_date == 租金.payout_date
rent_lookup = {} # (contract_id, lessee_id, payout_date) → rent_rec
if contract_lessee_pairs:
for cid, lid in contract_lessee_pairs:
rents = self.sudo().search([
('contract_id', '=', cid),
('lessee_id', '=', lid),
('management_type_id.name', '=', '租金'),
('is_invalid', '!=', True),
('payout_date', '!=', False),
])
for r in rents:
if r.payout_date:
key = (cid, lid, r.payout_date)
if key not in rent_lookup:
rent_lookup[key] = r
# ========== 四、构建报表行(每笔付款单独一行) ==========
rows = []
total_collected = 0.0
for line in modify_lines:
receivable = line.receivable_id
pay_amount = round(line.amount or 0.0, 2)
pay_date = line.reconciliation_date
# 收款金额为0或负数则跳过
if pay_amount <= 0:
continue
# --- 4.1 内存过滤:应用关键字和条件筛选 ---
contract = receivable.contract_id
lessee = receivable.lessee_id
# 关键字过滤
# ========== 辅助函数:内存过滤 ==========
def _match_filters(rec, property_name_str):
"""对 property.accounts.receivable 记录做内存过滤"""
if keyword:
kw = keyword.lower()
match = False
if contract and contract.contract_code and kw in contract.contract_code.lower():
if rec.contract_id and rec.contract_id.contract_code and kw in rec.contract_id.contract_code.lower():
match = True
if receivable.property_name and kw in receivable.property_name.lower():
if property_name_str and kw in property_name_str.lower():
match = True
if lessee and lessee.name and kw in lessee.name.lower():
if rec.lessee_id and rec.lessee_id.name and kw in rec.lessee_id.name.lower():
match = True
if not match:
continue
return False
if condition and rec.tenure_type != condition:
return False
if partner_id and rec.precinct_id.id != int(partner_id):
return False
if company_id and rec.company_id.id != int(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
# 租赁类型过滤
if condition and receivable.tenure_type != condition:
continue
# ========== 辅助函数:构建物业名称显示(穿透对象) ==========
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 ""
# 片区过滤
if partner_id and receivable.precinct_id.id != partner_id:
continue
# 公司过滤
if company_id and receivable.company_id.id != company_id:
continue
# 物业过滤
if property_id and property_id not in (receivable.property_ids.ids or []):
continue
# 承租方过滤
if lessee_id and (not lessee or lessee.id != lessee_id):
continue
# 合同过滤
if contract_id and (not contract or contract.id != contract_id):
continue
# --- 4.2 反查关联租金 ---
is_penalty_rec = receivable.management_type_id.name == '违约金'
rent_rec = None
penalty_rec = None
if is_penalty_rec:
# receivable 本身就是违约金记录
penalty_rec = receivable
# 反查租金:同一合同+同一承租方,违约金.due_date == 租金.payout_date
if penalty_rec.contract_id and penalty_rec.lessee_id and penalty_rec.due_date:
lookup_key = (penalty_rec.contract_id.id, penalty_rec.lessee_id.id, penalty_rec.due_date)
rent_rec = rent_lookup.get(lookup_key)
else:
# receivable 是租金记录
rent_rec = receivable
# --- 4.3 租金信息 ---
# ========== 辅助函数:构建行数据(共用) ==========
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)
@@ -174,13 +109,14 @@ class PenaltyYearCumReport(models.Model):
else:
rent_status = "未收"
# --- 4.4 违约金信息 ---
# 违约金信息
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)
@@ -188,23 +124,11 @@ class PenaltyYearCumReport(models.Model):
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 ""
# --- 4.5 累计统计 ---
total_collected += pay_amount
# --- 4.6 构建物业名称显示 ---
property_display = ""
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
property_display = {
'model': 'yuthon.property',
'id': contract.lease_ids[0].address_id.id,
'display_name': receivable.property_name or "",
}
else:
property_display = receivable.property_name or ""
# --- 4.7 构建行数据 ---
rows.append({
return {
"来源": source_tag,
"租金ID": rent_rec.id if rent_rec else "",
"租金应收日": rent_due_date,
"租金收款日期": rent_payout_date,
@@ -217,24 +141,108 @@ class PenaltyYearCumReport(models.Model):
"违约金未收": penalty_uncollected,
"违约金状态": penalty_status,
"违约金应收开始": penalty_start_date,
"实收金额": pay_amount,
"实收金额": round(pay_amount, 2),
"收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "",
"收款备注": line.remake1 or "",
"收费区间": receivable.charging_range or "",
"备注": receivable.remark or "",
"收款备注": pay_remark or "",
"收费区间": charging_range,
"备注": remark,
"合同编号": {
'model': 'property.lease.contract',
'id': contract.id,
'display_name': contract.contract_code,
} if contract else "",
"物业名称": property_display,
'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.id,
'display_name': lessee.name,
} if lessee else "",
"所属公司": receivable.company_id.name if receivable.company_id else "",
})
'id': lessee_rec.id,
'display_name': lessee_rec.name,
} if lessee_rec else "",
"所属公司": company_name or "",
}
# ========== 初始化 ==========
rows = []
total_collected = 0.0
# ========== 三、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ==========
# 查出 is_batch=True 的违约金记录
penalty_a_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 违约金记录
('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
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",
))
# ========== 四、来源B:receivable.modify.line 中 is_penalty=True 的记录 ==========
# 查询年内 is_penalty=True 的付款行
modify_b_lines = self.env['receivable.modify.line'].sudo().search([
('is_penalty', '=', True), # 标记为违约金付款
('receivable_id', '!=', False), # 必须关联应收记录
('reconciliation_date', '!=', False), # 有对账日期
('reconciliation_date', '>=', ds), # 对账日期 >= 当年1月1日
('reconciliation_date', '<=', de), # 对账日期 <= 当年12月31日
])
for ml in modify_b_lines:
pay_amount = ml.amount or 0.0
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):
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",
))
# ========== 五、构建统计汇总数据 ==========
stats = [