违约金

This commit is contained in:
李鹏宇
2026-06-13 17:48:07 +08:00
parent fdf5d8193e
commit a61cde389c
+135 -3
View File
@@ -373,7 +373,139 @@ class PenaltyArrearsReport(models.Model):
"所属公司": 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
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)},
@@ -381,7 +513,7 @@ class PenaltyArrearsReport(models.Model):
{'name': '违约金未收', 'amount': round(total_uncollected, 2)},
]
_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))
_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}