违约金
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user