违约金

This commit is contained in:
李鹏宇
2026-06-13 19:47:27 +08:00
parent a61cde389c
commit d0ef60d355
2 changed files with 519 additions and 260 deletions
@@ -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}
+258 -130
View File
@@ -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}