This commit is contained in:
李鹏宇
2026-03-23 18:03:15 +08:00
parent 1b3f55843e
commit 77ea007f24
3 changed files with 124 additions and 89 deletions
+112 -86
View File
@@ -54,7 +54,7 @@ class ReceivableFlowReport(models.Model):
if contract_id:
domain.append(('contract_id', '=', contract_id))
receivables = self.sudo().search(domain, order="id asc")
receivables = self.sudo().search(domain, order="contract_id asc, id asc")
rec_ids = receivables.ids
# ===== 批量预加载,避免N+1查询 =====
@@ -91,126 +91,152 @@ class ReceivableFlowReport(models.Model):
rows = []
seq = 0
for rec in receivables:
# 获取收款明细(按对账日期升序)
modify_lines = sorted(
modify_map.get(rec.id, []),
key=lambda l: l.reconciliation_date or datetime.date.min
# 按合同分组
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
)
# 收款汇总
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, [])
billing_period = ""
days_diff = 0
penalty_amount = 0.0
if wz_list:
wz = wz_list[0]
pl_all = list(wz.penalty_line_ids)
days_diff = sum(pl.days_overdue for pl in pl_all)
penalty_amount = round(sum(pl.penalty for pl in pl_all), 2)
penalty_lines = [pl for pl in pl_all if pl.penalty > 0]
if penalty_lines:
all_dates = []
for pl in penalty_lines:
for d in re.findall(r'(\d{4}-\d{2}-\d{2})', pl.remake or ''):
try:
all_dates.append(datetime.datetime.strptime(d, '%Y-%m-%d').date())
except ValueError:
pass
if all_dates:
billing_period = f"{min(all_dates).strftime('%Y-%m-%d')} ~ {max(all_dates).strftime('%Y-%m-%d')}"
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
contract_total_unpaid = sum(r.submitted for r in contract_recs)
seq += 1
# ==== 应收主行 ====
# ==== 合同头行 ====
rows.append({
"row_type": "主",
"row_type": "合同",
"序号": seq,
"租金id": rec.id,
"租金id": "",
"合同编号": {
'model': 'property.lease.contract',
'id': rec.contract_id.id if rec.contract_id else None,
'display_name': rec.contract_id.contract_code if rec.contract_id else "",
} if rec.contract_id else "",
'id': contract.id if contract else None,
'display_name': contract.contract_code if contract else "",
} if contract else "",
"物业名称": {
'model': 'yuthon.property',
'id': rec.contract_id.lease_ids[0].address_id.id if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else None,
'display_name': rec.property_name or "",
} if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "",
'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': rec.lessee_id.id if rec.lessee_id else None,
'display_name': rec.lessee_id.name if rec.lessee_id else "",
} if rec.lessee_id else "",
"应收日期": rec.due_date.strftime("%Y-%m-%d") if rec.due_date else "",
'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(rec.original_amount, 2)),
'display_name': str(round(contract_total_receivable, 2)),
'view_mode': 'tree',
'domain': [('id', '=', rec.id)],
'domain': [('contract_id', '=', contract.id if contract else 0), ('is_invalid', '=', False)],
},
"收款总金额": {
'model': 'receivable.modify.line',
'display_name': str(round(total_collected, 2)),
'display_name': str(round(contract_total_collected, 2)),
'view_mode': 'tree',
'domain': [('receivable_id', '=', rec.id), ('reconciliation_date', '!=', False)],
'domain': [('receivable_id.contract_id', '=', contract.id if contract else 0), ('reconciliation_date', '!=', False)],
},
"未收": round(rec.submitted, 2),
"状态": charge_status,
"未收": round(contract_total_unpaid, 2),
"状态": "",
"收款日期": "",
"收款金额": "",
"收款备注": "",
"违约金明细": {
'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:
# ==== 每条应收记录 ====
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
# ==== 应收租金行 ====
rows.append({
"row_type": "收款",
"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 "",
"违约金明细": "",
"违约金收款时间": "",
"违约金收款金额": "",
"应收日期": 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,
"收款日期": "",
"收款金额": "",
"收款备注": "",
"违约金明细": {
'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)},
@@ -74,6 +74,9 @@ class ReceivableReport extends Component {
'ag-row-new-rent': function(params) {
return params.data && params.data.acc_type === 'new';
},
'ag-row-contract-header': function(params) {
return params.data && params.data.row_type === '合同';
},
'ag-row-payment-line': function(params) {
return params.data && params.data.row_type === '收款';
}
@@ -129,12 +132,12 @@ class ReceivableReport extends Component {
orm: this.serviceOrm, // 传递 orm 用于调用模型方法
},
onFilterChanged: () => {
// 过滤后重新编号序号列(仅主行编号)
// 过滤后重新编号序号列(仅合同头行编号)
let num = 1;
this.grid.forEachNodeAfterFilter((node) => {
if (node.data && node.data['row_type'] === '主') {
if (node.data && node.data['row_type'] === '合同') {
node.data['序号'] = num++;
} else if (node.data && node.data['row_type'] === '收款') {
} else {
node.data['序号'] = '';
}
});
@@ -37,6 +37,12 @@
font-weight: bold;
}
/* 合同头行样式 */
.ag-row-contract-header {
background-color: #e8f4fd !important;
font-weight: bold;
}
/* 收款子行样式 */
.ag-row-payment-line {
background-color: #f0f7ff !important;