Files
gzth/yuthon_report/models/receivable_flow_report.py
2026-06-17 18:01:58 +08:00

392 lines
19 KiB
Python

import datetime
import re
from odoo import api, models
from .report_utils import _id_list # 导入工具函数:将参数转换为列表格式,支持多选筛选
class ReceivableFlowReport(models.Model):
_inherit = 'property.accounts.receivable'
@api.model
def get_receivable_filter_domains(self, params):
"""根据当前筛选条件,返回三个下拉框的候选 ID 集合
- 保证三者之间互相过滤、一致
- 不应用 due_date 等业务日期过滤,因为不同报表的日期逻辑不同
(如应收流水用 due_date,押金报表用 payout_date/has_date),
统一用 due_date 过滤会导致下拉候选项缺失
"""
domain = [
('is_invalid', '!=', True),
]
company_id = _id_list(params.get('company_id'))
property_id = params.get('property_id')
lessee_id = params.get('lessee_id')
contract_id = params.get('contract_id')
if company_id:
domain.append(('company_id', 'in', 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)
property_ids = set(receivables.mapped('property_ids').ids)
lessee_ids = set(receivables.mapped('lessee_id').ids)
contract_ids = set(receivables.mapped('contract_id').ids)
return {
'property_ids': list(property_ids),
'lessee_ids': list(lessee_ids),
'contract_ids': list(contract_ids),
}
@api.model
def get_receivable_flow_report(self, params):
"""
应收流水中间报表:
- 显示租金的完整流水信息
- 包括基本信息、收款情况和违约金相关信息
"""
domain = [
('is_invalid', '!=', True),
]
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 = _id_list(params.get('partner_id')) # 将partner_id转换为列表,用于多选筛选
company_id = _id_list(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', 'in', partner_id)) # 使用in操作符支持多选片区筛选
if company_id:
domain.append(('company_id', 'in', 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查询 =====
all_modify_lines = self.env['receivable.modify.line'].sudo().search([
('receivable_id', 'in', rec_ids),
('reconciliation_date', '!=', False),
])
modify_map = {}
for ml in all_modify_lines:
modify_map.setdefault(ml.receivable_id.id, []).append(ml)
penalty_receivables = self.env['property.accounts.receivable'].sudo().search([
('contract_id', 'in', receivables.mapped('contract_id').ids),
('is_invalid', '!=', True),
('management_type_id.name', '=', '违约金'),
])
penalty_source_map = {}
penalty_receivable_ids = []
for penalty_rec in penalty_receivables:
source_receivable = penalty_rec.source_receivable_id or penalty_rec._resolve_source_receivable()
if source_receivable and source_receivable.id in rec_ids:
penalty_source_map[penalty_rec.id] = source_receivable.id
penalty_receivable_ids.append(penalty_rec.id)
penalty_modify_map = {}
if penalty_receivable_ids:
penalty_modify_lines = self.env['receivable.modify.line'].sudo().search([
('receivable_id', 'in', penalty_receivable_ids),
('reconciliation_date', '!=', False),
])
for ml in penalty_modify_lines:
source_id = penalty_source_map.get(ml.receivable_id.id)
if source_id and (
ml.is_penalty
or (ml.receivable_id.management_type_id and ml.receivable_id.management_type_id.name == '违约金')
):
penalty_modify_map.setdefault(source_id, []).append(ml)
# ===== 预加载结束 =====
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 or 0.0) - (r.discount_amount or 0.0) 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)
# 取应收日期最新的租金行状态
latest_rec = max(contract_recs, key=lambda r: r.due_date or datetime.date.min)
latest_status = "已收" if latest_rec.submitted == 0 else "未收"
# 取最新租金行的收款子行(最后一笔)
latest_modify_lines = sorted(
modify_map.get(latest_rec.id, []),
key=lambda l: l.reconciliation_date or datetime.date.min
)
latest_last_pay = latest_modify_lines[-1] if latest_modify_lines else None
latest_pay_date = latest_last_pay.reconciliation_date.strftime("%Y-%m-%d") if latest_last_pay and latest_last_pay.reconciliation_date else ""
latest_pay_amount = round(latest_last_pay.amount, 2) if latest_last_pay else ""
latest_pay_remark = latest_last_pay.bank_id.remake if (latest_last_pay and latest_last_pay.bank_id) else ""
# 取最新租金行的违约金数据
latest_penalty_payment_lines = sorted(
penalty_modify_map.get(latest_rec.id, []),
key=lambda l: l.reconciliation_date or datetime.date.min
) if latest_rec.management_type_id.name != '违约金' else []
latest_penalty_collected = round(sum((l.amount or 0.0) for l in latest_penalty_payment_lines), 2)
latest_penalty_pay_date = latest_penalty_payment_lines[-1].reconciliation_date.strftime("%Y-%m-%d") if latest_penalty_payment_lines and latest_penalty_payment_lines[-1].reconciliation_date else ""
# 取最新租金行的违约金计算时间、逾期天数、预计金额
latest_penalty_range_str = ""
latest_penalty_days = ""
latest_penalty_amount_calc = ""
if latest_rec.is_liquidated_damages and latest_rec.submitted > 0:
lnon_penalty_lines = [l for l in latest_modify_lines if not l.is_penalty]
today = datetime.date.today()
if lnon_penalty_lines:
llast_pay_date = max(l.reconciliation_date for l in lnon_penalty_lines)
lbase_date = llast_pay_date + datetime.timedelta(days=1)
else:
if latest_rec.due_date:
lnm = latest_rec.due_date.month + 1 if latest_rec.due_date.month < 12 else 1
lny = latest_rec.due_date.year if latest_rec.due_date.month < 12 else latest_rec.due_date.year + 1
lbase_date = datetime.date(lny, lnm, 1)
else:
lbase_date = None
if lbase_date and lbase_date <= today:
latest_penalty_range_str = f"{lbase_date.strftime('%Y-%m-%d')}至{today.strftime('%Y-%m-%d')}"
latest_penalty_days = (today - lbase_date).days + 1
latest_penalty_amount_calc = round(latest_rec.submitted * latest_penalty_days * 0.001, 2)
seq += 1
contract_key = contract.contract_code if (contract and contract.contract_code) else "contract_{}".format(seq)
# ==== 合同头行 ====
rows.append({
"row_type": "合同",
"orgHierarchy": [contract_key],
"序号": 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 "",
"应收日期": latest_rec.due_date.strftime("%Y-%m-%d") if latest_rec.due_date 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', '!=', True)],
},
"收款总金额": {
'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),
"状态": latest_status,
"收款日期": latest_pay_date,
"收款金额": latest_pay_amount,
"收款备注": latest_pay_remark,
"收款时间状态": "",
"违约金计算时间": latest_penalty_range_str,
"违约金逾期天数": latest_penalty_days,
"违约金预计金额": latest_penalty_amount_calc,
"违约金明细": {
'model': 'property.accounts.receivable',
'display_name': '违约金明细',
'method': 'penalty_wizard',
'id': latest_rec.id,
} if latest_rec else "",
"违约金收款时间": latest_penalty_pay_date,
"违约金收款金额": latest_penalty_collected if latest_penalty_collected else "",
})
# ==== 每条应收记录 ====
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 = "已收"
penalty_payment_lines = []
if rec.management_type_id.name != '违约金':
penalty_payment_lines = sorted(
penalty_modify_map.get(rec.id, []),
key=lambda l: l.reconciliation_date or datetime.date.min
)
total_penalty_collected = round(
sum((line.amount or 0.0) for line in penalty_payment_lines),
2,
)
penalty_payment_date = penalty_payment_lines[-1].reconciliation_date if penalty_payment_lines else None
# 复用 _onchange_calculation_date 逻辑计算违约金时间区间、逆期天数、违约金
penalty_range_str = ""
penalty_days = 0
penalty_amount_calc = 0.0
if rec.is_liquidated_damages and rec.submitted > 0:
non_penalty_lines = [l for l in modify_lines if not l.is_penalty]
today = datetime.date.today()
if non_penalty_lines:
last_pay_date = max(l.reconciliation_date for l in non_penalty_lines)
base_date = last_pay_date + datetime.timedelta(days=1)
else:
if rec.due_date:
nm = rec.due_date.month + 1 if rec.due_date.month < 12 else 1
ny = rec.due_date.year if rec.due_date.month < 12 else rec.due_date.year + 1
base_date = datetime.date(ny, nm, 1)
else:
base_date = None
if base_date and base_date <= today:
penalty_range_str = f"{base_date.strftime('%Y-%m-%d')}至{today.strftime('%Y-%m-%d')}"
penalty_days = (today - base_date).days + 1
penalty_amount_calc = round(rec.submitted * penalty_days * 0.001, 2)
# ==== 应收租金行 ====
rows.append({
"row_type": "主",
"orgHierarchy": [contract_key, str(rec.id)],
"序号": "",
"租金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 or 0.0) - (rec.discount_amount or 0.0), 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,
"违约金逾期天数": penalty_days if penalty_days else "",
"违约金预计金额": penalty_amount_calc if penalty_amount_calc else "",
"违约金明细": {
'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:
# 计算收款时间状态
payment_time_status = ""
if ml.reconciliation_date and rec.due_date:
if ml.reconciliation_date > rec.due_date:
payment_time_status = "补缴"
elif ml.reconciliation_date < rec.due_date:
payment_time_status = "预收"
rows.append({
"row_type": "收款",
"orgHierarchy": [contract_key, str(rec.id), "pay_{}".format(ml.id)],
"序号": "",
"租金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 "",
"收款时间状态": payment_time_status,
"违约金明细": "",
"违约金收款时间": "",
"违约金收款金额": "",
"违约金计算时间": "",
"违约金逾期天数": "",
"违约金预计金额": "",
})
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}