392 lines
19 KiB
Python
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}
|