diff --git a/yuthon_report/models/penalty_arrears_report.py b/yuthon_report/models/penalty_arrears_report.py index 98fca983a..982d143e5 100644 --- a/yuthon_report/models/penalty_arrears_report.py +++ b/yuthon_report/models/penalty_arrears_report.py @@ -373,7 +373,139 @@ class PenaltyArrearsReport(models.Model): "所属公司": rent_rec.company_id.name if rent_rec.company_id else "", }) - # ========== 五、构建统计汇总数据 ========== + # ========== 五、来源C:从未收/收部分租金直接出发,动态计算违约金 ========== + # 覆盖场景:租金一直没交钱,既没有 is_batch 违约金应收单(来源A), + # 也没有任何 modify.line 收款记录(来源B),但产生了动态违约金 + # 入口:property.accounts.receivable 中 deposit_state in [unpaid, no_paid] 的租金记录 + # 且不在来源A、来源B中出现过的 + + # --- 5.1 构建已处理的租金ID集合 --- + # 来源A中关联到的租金ID + source_a_rent_ids = set() + for penalty_rec in penalty_a_recs: + if penalty_rec.source_receivable_id: + source_a_rent_ids.add(penalty_rec.source_receivable_id.id) + # 来源B中涉及到的租金ID(即 rent_penalty_collected 的 key 集合) + source_b_rent_ids = set(rent_penalty_collected.keys()) + + # --- 5.2 查询未收/收部分的租金记录(排除已处理过的)--- + # 收款类型为"租金"的记录(非违约金),且状态是未收或收部分 + rent_domain = [ + ('management_type_id.name', '!=', '违约金'), # 收款类型非违约金(即租金) + ('deposit_state', 'in', ['unpaid', 'no_paid']), # 未收或收部分 + ('is_invalid', '!=', True), # 排除作废 + ] + if query_date: + rent_domain.append(('due_date', '<=', query_date)) # 应收日已过,才开始产生违约金 + + unpaid_rent_recs = self.sudo().search(rent_domain, order="contract_id asc, due_date asc") + + for rent_rec in unpaid_rent_recs: + # 跳过已经在来源A、B中处理过的租金 + if rent_rec.id in source_a_rent_ids or rent_rec.id in source_b_rent_ids: + continue + + # 内存过滤 + prop_name = rent_rec.property_name or "" + if not _match_filters(rent_rec, prop_name): + continue + + due_date = rent_rec.due_date + if not due_date: + continue + + # --- 计算违约金起始日期:应收月的次月1号 --- + if due_date.month == 12: + penalty_start = datetime.date(due_date.year + 1, 1, 1) + else: + penalty_start = datetime.date(due_date.year, due_date.month + 1, 1) + + # 违约金起始日期 > 查询日期,还没开始产生违约金 + if query_date and penalty_start > query_date: + continue + + # --- 获取违约金计算比例 --- + penalty_rate = _get_penalty_rate(rent_rec.contract_id) + + # --- 动态计算违约金总额 --- + # 这笔租金没有任何 modify.line 收款记录,所以本金就是全额 + rent_amount = (rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0) + current_principal = rent_amount + total_calc_penalty = 0.0 + calc_range_parts = [] + + # 从 penalty_start 到查询日期,整段计算 + if current_principal > 0 and query_date and penalty_start <= query_date: + days = (query_date - penalty_start).days + 1 + if days > 0: + segment_penalty = current_principal * days * penalty_rate + total_calc_penalty += segment_penalty + calc_range_parts.append( + f"{penalty_start.strftime('%Y-%m-%d')}至{query_date.strftime('%Y-%m-%d')} " + f"({days}天×{current_principal:.2f}×{penalty_rate})" + ) + + total_calc_penalty = round(total_calc_penalty, 2) + if total_calc_penalty <= 0: + continue + + # 来源C:没有收到过任何钱,所以已收=0,未收=总额 + penalty_collected_amt = 0.0 + penalty_uncollected_amt = total_calc_penalty + + # 去重 key + key = ('C', rent_rec.id) + if key in processed_keys: + continue + processed_keys.add(key) + + # 租金信息 + rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2) + rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2) + rent_status = "" + if rent_rec.deposit_state == 'paid': + rent_status = "已收" + elif rent_rec.deposit_state == 'no_paid': + rent_status = "收部分" + else: + rent_status = "未收" + + # 累计统计 + total_penalty += total_calc_penalty + total_uncollected += penalty_uncollected_amt + + contract = rent_rec.contract_id + rows.append({ + "来源": "C", + "租金ID": rent_rec.id, + "租金应收日": due_date.strftime("%Y-%m-%d"), + "租金收款日期": rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "", + "租金金额": round(rent_amount, 2), + "租金已收": rent_collected_amt, + "租金未收": rent_uncollected_amt, + "租金状态": rent_status, + "违约金金额": total_calc_penalty, + "违约金已收": penalty_collected_amt, + "违约金未收": penalty_uncollected_amt, + "违约金状态": "动态", # 纯动态计算,无任何收款记录 + "违约金应收开始": penalty_start.strftime("%Y-%m-%d"), + "收费区间": "\n".join(calc_range_parts) if calc_range_parts else "", + "备注": "未收租金,动态计算违约金", + "合同编号": { + 'model': 'property.lease.contract', + 'id': contract.id, + 'display_name': contract.contract_code, + } if contract else "", + "物业名称": _build_property_display(contract, prop_name), + "承租方名称": { + 'model': 'property.tenant.information', + 'id': rent_rec.lessee_id.id, + 'display_name': rent_rec.lessee_id.name, + } if rent_rec.lessee_id else "", + "所属公司": rent_rec.company_id.name if rent_rec.company_id else "", + }) + + # ========== 六、构建统计汇总数据 ========== stats = [ {'name': '记录总数', 'amount': len(rows)}, {'name': '违约金总额', 'amount': round(total_penalty, 2)}, @@ -381,7 +513,7 @@ class PenaltyArrearsReport(models.Model): {'name': '违约金未收', 'amount': round(total_uncollected, 2)}, ] - _logger.info("[PENALTY_ARREARS] query_date=%s, source_a=%d, source_b_rents=%d, total_rows=%d", - query_date, len(penalty_a_recs), len(rent_recs_b), len(rows)) + _logger.info("[PENALTY_ARREARS] query_date=%s, source_a=%d, source_b_rents=%d, source_c_rents=%d, total_rows=%d", + query_date, len(penalty_a_recs), len(rent_recs_b), len(unpaid_rent_recs), len(rows)) return {"rows": rows, "stats": stats}