diff --git a/yuthon_report/models/receivable_flow_report.py b/yuthon_report/models/receivable_flow_report.py index 8357276f1..431e9ccb9 100644 --- a/yuthon_report/models/receivable_flow_report.py +++ b/yuthon_report/models/receivable_flow_report.py @@ -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)}, diff --git a/yuthon_report/static/src/components/yuthon_report/receivable_report.js b/yuthon_report/static/src/components/yuthon_report/receivable_report.js index 7f09d547d..ef41d9798 100644 --- a/yuthon_report/static/src/components/yuthon_report/receivable_report.js +++ b/yuthon_report/static/src/components/yuthon_report/receivable_report.js @@ -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['序号'] = ''; } }); diff --git a/yuthon_report/static/src/components/yuthon_report/yuthon_report.scss b/yuthon_report/static/src/components/yuthon_report/yuthon_report.scss index d10e4140c..c39b7a9af 100644 --- a/yuthon_report/static/src/components/yuthon_report/yuthon_report.scss +++ b/yuthon_report/static/src/components/yuthon_report/yuthon_report.scss @@ -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;