267 lines
12 KiB
Python
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}
|