From fdf5d8193e36c0375070ab22cc20a628db2ebc24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sat, 13 Jun 2026 17:11:16 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9D=E7=BA=A6=E9=87=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/penalty_arrears_report.py | 565 ++++++++---------- .../models/penalty_month_received_report.py | 290 ++++----- .../models/penalty_year_cum_report.py | 292 ++++----- 3 files changed, 553 insertions(+), 594 deletions(-) diff --git a/yuthon_report/models/penalty_arrears_report.py b/yuthon_report/models/penalty_arrears_report.py index e11da087c..98fca983a 100644 --- a/yuthon_report/models/penalty_arrears_report.py +++ b/yuthon_report/models/penalty_arrears_report.py @@ -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} diff --git a/yuthon_report/models/penalty_month_received_report.py b/yuthon_report/models/penalty_month_received_report.py index 5618dae1e..f034df2dd 100644 --- a/yuthon_report/models/penalty_month_received_report.py +++ b/yuthon_report/models/penalty_month_received_report.py @@ -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 = [ diff --git a/yuthon_report/models/penalty_year_cum_report.py b/yuthon_report/models/penalty_year_cum_report.py index 43cc13b3a..1daed0095 100644 --- a/yuthon_report/models/penalty_year_cum_report.py +++ b/yuthon_report/models/penalty_year_cum_report.py @@ -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 = [