Files
gzth/yuthon_report/models/receivable_flow_report.py
T
2026-03-24 11:48:27 +08:00

267 lines
12 KiB
Python

import datetime
import re
from odoo import api, models
class ReceivableFlowReport(models.Model):
_inherit = 'property.accounts.receivable'
@api.model
def get_receivable_flow_report(self, params):
"""
应收流水中间报表:
- 显示租金的完整流水信息
- 包括基本信息、收款情况和违约金相关信息
"""
domain = [
('is_invalid', '=', False),
]
date_start = params.get("date_start")
date_end = params.get("date_end")
if date_start:
domain.append(('due_date', '>=', datetime.datetime.fromisoformat(date_start).date()))
if date_end:
domain.append(('due_date', '<=', datetime.datetime.fromisoformat(date_end).date()))
keyword = params.get("keyword")
if keyword:
domain += [
'|', '|',
('contract_id.contract_code', 'ilike', keyword),
('property_name', 'ilike', keyword),
('lessee_id.name', 'ilike', keyword),
]
condition = params.get('condition')
partner_id = params.get('partner_id')
company_id = params.get('company_id')
property_id = params.get('property_id')
lessee_id = params.get('lessee_id')
contract_id = params.get('contract_id')
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))
receivables = self.sudo().search(domain, order="contract_id asc, id asc")
rec_ids = receivables.ids
# ===== 批量预加载,避免N+1查询 =====
# 1. 批量查收款明细行(sudo跳过多公司权限)
all_modify_lines = self.env['receivable.modify.line'].sudo().search([
('receivable_id', 'in', rec_ids),
('reconciliation_date', '!=', False),
])
# 按 receivable_id 分组
modify_map = {} # {receivable_id: [lines]}
for ml in all_modify_lines:
modify_map.setdefault(ml.receivable_id.id, []).append(ml)
# 2. 批量查违约金wizard(sudo跳过多公司权限)
all_wizards = self.env['penalty.line.wizard'].sudo().search([
('receivable_id', 'in', rec_ids),
])
# 按 receivable_id 分组,并预取 pay_line_ids
wizard_map = {} # {receivable_id: [wizards]}
for wz in all_wizards:
wizard_map.setdefault(wz.receivable_id.id, []).append(wz)
# 预取所有 pay_line_ids 和 penalty_line_ids
all_pay_lines = all_wizards.mapped('pay_line_ids')
all_wizards.mapped('penalty_line_ids') # 预加载违约金计算明细
# 预先触发 compute 字段,避免循环内惰性计算
all_wizards.mapped('days_overdue')
all_wizards.mapped('total_penalty')
# 按 wizard_id 分组
pay_line_map = {} # {wizard_id: [pay_lines]}
for pl in all_pay_lines:
pay_line_map.setdefault(pl.penalty_id.id, []).append(pl)
# 3. 批量查违约金应收记录(按合同分组)
contract_ids = list(set(rec.contract_id.id for rec in receivables if rec.contract_id))
penalty_type = self.env['yuthon.management.type'].sudo().search([('name', '=', '违约金')], limit=1)
all_penalty_recs = self.env['property.accounts.receivable'].sudo().search([
('contract_id', 'in', contract_ids),
('management_type_id', '=', penalty_type.id),
('is_invalid', '=', False),
], order='due_date asc') if penalty_type else self.env['property.accounts.receivable']
# 按 contract_id 分组 {contract_id: [charging_range, ...]}
penalty_range_map = {} # {contract_id: [charging_range]}
for pr in all_penalty_recs:
if pr.charging_range:
penalty_range_map.setdefault(pr.contract_id.id, []).append(pr.charging_range)
# ===== 预加载结束 =====
rows = []
seq = 0
# 按合同分组
from itertools import groupby
from operator import attrgetter
for contract, contract_recs in groupby(receivables, key=lambda r: r.contract_id):
contract_recs = list(contract_recs)
first_rec = contract_recs[0]
# 合同的汇总数据
contract_total_receivable = sum(r.original_amount for r in contract_recs)
contract_total_collected = sum(
sum(l.amount for l in modify_map.get(r.id, []))
for r in contract_recs
)
contract_total_unpaid = sum(r.submitted for r in contract_recs)
seq += 1
# ==== 合同头行 ====
rows.append({
"row_type": "合同",
"序号": seq,
"租金id": "",
"合同编号": {
'model': 'property.lease.contract',
'id': contract.id if contract else None,
'display_name': contract.contract_code if contract else "",
} if contract else "",
"物业名称": {
'model': 'yuthon.property',
'id': contract.lease_ids[0].address_id.id if contract and contract.lease_ids and contract.lease_ids[0].address_id else None,
'display_name': first_rec.property_name or "",
} if contract and contract.lease_ids and contract.lease_ids[0].address_id else first_rec.property_name or "",
"承租方名称": {
'model': 'property.tenant.information',
'id': first_rec.lessee_id.id if first_rec.lessee_id else None,
'display_name': first_rec.lessee_id.name if first_rec.lessee_id else "",
} if first_rec.lessee_id else "",
"应收日期": "",
"应收金额": {
'model': 'property.accounts.receivable',
'display_name': str(round(contract_total_receivable, 2)),
'view_mode': 'tree',
'domain': [('contract_id', '=', contract.id if contract else 0), ('is_invalid', '=', False)],
},
"收款总金额": {
'model': 'receivable.modify.line',
'display_name': str(round(contract_total_collected, 2)),
'view_mode': 'tree',
'domain': [('receivable_id.contract_id', '=', contract.id if contract else 0), ('reconciliation_date', '!=', False)],
},
"未收": round(contract_total_unpaid, 2),
"状态": "",
"收款日期": "",
"收款金额": "",
"收款备注": "",
"违约金明细": "",
"违约金收款时间": "",
"违约金收款金额": "",
"违约金计算时间": "",
})
# ==== 每条应收记录 ====
for rec in contract_recs:
modify_lines = sorted(
modify_map.get(rec.id, []),
key=lambda l: l.reconciliation_date or datetime.date.min
)
total_collected = sum(line.amount for line in modify_lines)
charge_status = "未收"
if rec.submitted == 0:
charge_status = "已收"
wz_list = wizard_map.get(rec.id, [])
penalty_pay_lines = []
for wz in wz_list:
penalty_pay_lines.extend(pay_line_map.get(wz.id, []))
total_penalty_collected = sum(line.amount for line in penalty_pay_lines)
latest_penalty_payment = sorted(
[l for l in penalty_pay_lines if l.date],
key=lambda l: l.date, reverse=True
)
penalty_payment_date = latest_penalty_payment[0].date if latest_penalty_payment else None
# 取该合同下所有违约金记录的 charging_range
penalty_ranges = penalty_range_map.get(rec.contract_id.id, []) if rec.contract_id else []
penalty_range_str = "\n".join(penalty_ranges) if penalty_ranges else ""
# ==== 应收租金行 ====
rows.append({
"row_type": "主",
"序号": "",
"租金id": rec.id,
"合同编号": "",
"物业名称": "",
"承租方名称": "",
"应收日期": rec.due_date.strftime("%Y-%m-%d") if rec.due_date else "",
"应收金额": {
'model': 'property.accounts.receivable',
'display_name': str(round(rec.original_amount, 2)),
'view_mode': 'tree',
'domain': [('id', '=', rec.id)],
},
"收款总金额": {
'model': 'receivable.modify.line',
'display_name': str(round(total_collected, 2)),
'view_mode': 'tree',
'domain': [('receivable_id', '=', rec.id), ('reconciliation_date', '!=', False)],
},
"未收": round(rec.submitted, 2),
"状态": charge_status,
"收款日期": "",
"收款金额": "",
"收款备注": "",
"违约金计算时间": penalty_range_str,
"违约金明细": {
'model': 'property.accounts.receivable',
'display_name': '违约金明细',
'method': 'penalty_wizard',
'id': rec.id,
},
"违约金收款时间": penalty_payment_date.strftime("%Y-%m-%d") if penalty_payment_date else "",
"违约金收款金额": round(total_penalty_collected, 2),
})
# ==== 每笔收款展开为子行 ====
for ml in modify_lines:
rows.append({
"row_type": "收款",
"序号": "",
"租金id": rec.id,
"合同编号": "",
"物业名称": "",
"承租方名称": "",
"应收日期": "",
"应收金额": "",
"收款总金额": "",
"未收": "",
"状态": "",
"收款日期": ml.reconciliation_date.strftime("%Y-%m-%d") if ml.reconciliation_date else "",
"收款金额": round(ml.amount, 2),
"收款备注": ml.bank_id.remake if ml.bank_id else "",
"违约金明细": "",
"违约金收款时间": "",
"违约金收款金额": "",
"违约金计算时间": "",
})
contract_rows = [r for r in rows if r.get('row_type') == '合同']
main_rows = [r for r in rows if r.get('row_type') == '主']
stats = [
{'name': '合同数', 'amount': len(contract_rows)},
{'name': '记录总数', 'amount': len(main_rows)},
{'name': '应收总额', 'amount': round(sum(float(row["应收金额"]['display_name']) for row in main_rows), 2)},
{'name': '已收总额', 'amount': round(sum(float(row["收款总金额"]['display_name']) for row in main_rows), 2)},
{'name': '未收总额', 'amount': round(sum(row["未收"] for row in main_rows), 2)},
]
return {"rows": rows, "stats": stats}