From 937d95e0ed6b15201e8f28e9f0a62fe0ad649074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Thu, 18 Jun 2026 15:35:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_report/models/pre_arrears_report.py | 18 +++-- yuthon_report/models/yuthon_dashboard.py | 75 +++++++++++++++++++ .../yuthon_dashboard/yuthon_dashboard.xml | 42 +++++++++++ .../components/yuthon_report/att_report.js | 17 ++++- .../yuthon_report/receivable_flow_report.js | 26 ++++++- .../receivable_message_report.js | 17 ++++- .../yuthon_report/receivable_report.js | 28 ++++++- .../yuthon_report/simple_bank_report.js | 15 ++++ .../yuthon_report/yuthon_contract_report.js | 17 ++++- .../components/yuthon_report/yuthon_report.js | 24 +++++- 10 files changed, 262 insertions(+), 17 deletions(-) diff --git a/yuthon_report/models/pre_arrears_report.py b/yuthon_report/models/pre_arrears_report.py index b6d13a49b..2125832bb 100644 --- a/yuthon_report/models/pre_arrears_report.py +++ b/yuthon_report/models/pre_arrears_report.py @@ -15,10 +15,10 @@ class PreArrearsReport(models.Model): """ 前期欠收报表(数据源:receivable.modify.line): - 查询条件的日期对应应收账单的 due_date(应收日期在查询月之前,不包含查询月) - - 欠收条件: + - 欠收条件(二选一): - 收款日期(payout_date)为空: 从未收款 → 欠收 - - 收款日期在查询月之后: 查询月时点还没收到钱 → 欠收 - - 收款日期 <= 查询月末: 已经交过了 → 不算欠收 + - 收款日期所在月 >= 查询月 且 收款日期所在月 > 应收日期所在月: 收款跨月且查询月才收到 → 欠收 + - 同月收款不算欠收(如1月15日收1月1日应收的款) - 金额取值: - 如果 not_collected > 0,取 not_collected (当前还欠的金额) - 否则取 amount (已对金额) @@ -91,10 +91,16 @@ class PreArrearsReport(models.Model): lines = self.sudo().search(domain, order="receivable_id asc, payout_date asc") _logger.info("Part1 search found %s modify.line records", len(lines)) - # 欠收条件:收款日期为空(从未收款) 或 收款日期在查询月之后(查询月时点还没收到钱) - # 如果 payout_date <= query_month_end,说明查询月及之前已经交过了,不算欠收 + # 欠收条件(二选一): + # 1. 没有收款日期(从未收款)→ 欠收 + # 2. 收款日期所在月 >= 查询月 且 收款日期所在月 > 应收日期所在月 → 欠收 + # 即:收款跨月(交晚了)且收款在查询月或之后才到 + # 同月收款不算欠收(如1月15日收1月1日应收的款) lines = lines.filtered( - lambda l: not l.payout_date or l.payout_date > query_month_end + lambda l: not l.payout_date or ( + l.payout_date.replace(day=1) >= query_month_start and + l.payout_date.replace(day=1) > l.receivable_id.due_date.replace(day=1) + ) ) _logger.info("Part1 after欠收filter: %s records remain", len(lines)) diff --git a/yuthon_report/models/yuthon_dashboard.py b/yuthon_report/models/yuthon_dashboard.py index 3d47d8772..19d4a4e86 100644 --- a/yuthon_report/models/yuthon_dashboard.py +++ b/yuthon_report/models/yuthon_dashboard.py @@ -273,8 +273,83 @@ class YuthonDashboard(models.Model): return { 'filters': filters, 'sections': sections, + 'notes': self._get_notes_data(base_params, query_date), } + # ------------------------------------------------------- + # 说明板块数据:仅当选择片区管理员时返回数据 + # 直接调用已有报表方法拿 rows,从 rows 提取物业名和金额 + # ------------------------------------------------------- + + def _get_notes_data(self, base_params, query_date): + """ + 获取说明板块内容(仅当选择片区管理员时有数据) + 直接复用已有报表方法的 rows 数据,不重复写查询逻辑 + """ + partner_id = _id_list(base_params.get('partner_id')) + # 没有选片区管理员时不返回任何说明 + if not partner_id: + return {'rent_arrears': [], 'monthly_precollection': [], 'expense': []} + + try: + # 前期欠收:调用 get_pre_arrears_report,从 rows 取物业名和金额 + rent_arrears = [] + arrears_result = self.env['receivable.modify.line'].get_pre_arrears_report(base_params) + for idx, row in enumerate(arrears_result.get('rows', []), start=1): + # 物业名称可能是对象 {display_name} 或字符串 + prop = row.get('物业名称', '') + prop_name = prop.get('display_name', '') if isinstance(prop, dict) else str(prop) + amount = row.get('金额', 0) + payout = row.get('收款日期', '') + # 有收款日期的标注 + suffix = f"({payout}已交)" if payout else '' + rent_arrears.append({'text': f"{idx}. {prop_name}欠{self._format_amount(amount)}元{suffix};"}) + + # 本月预收:调用 get_monthly_precollection_report,从 rows 取物业名和金额 + monthly_precollection = [] + precollection_result = self.env['receivable.modify.line'].get_monthly_precollection_report(base_params) + for idx, row in enumerate(precollection_result.get('rows', []), start=1): + prop = row.get('物业名称', '') + prop_name = prop.get('display_name', '') if isinstance(prop, dict) else str(prop) + amount = row.get('金额', 0) + monthly_precollection.append({'text': f"{idx}. {prop_name}本月预收:{self._format_amount(amount)}元。"}) + + # 支出:调用三个费用报表方法,从 rows 取标题和实付金额 + expense = [] + fee_methods = [ + ('管理费', 'property.bills.list', 'get_management_fee_monthly_report'), + ('维修费', 'property.bills.list', 'get_maintenance_fee_monthly_report'), + ('评估费', 'property.bills.list', 'get_assessment_fee_monthly_report'), + ] + for fee_label, model_name, method_name in fee_methods: + fee_result = getattr(self.env[model_name], method_name)(base_params) + for row in fee_result.get('rows', []): + title = row.get('标题', '') + amount_actual = row.get('实付', 0) + if amount_actual > 0: + expense.append(f"{title}{fee_label}支出{self._format_amount(amount_actual)}元。") + + _logger.info("[NOTES] 结果: rent_arrears=%d条, monthly_precollection=%d条, expense=%d条", + len(rent_arrears), len(monthly_precollection), len(expense)) + + return { + 'rent_arrears': rent_arrears, + 'monthly_precollection': monthly_precollection, + 'expense': expense, + } + except Exception as e: + _logger.error("[DASHBOARD_NOTES_ERROR] %s", e, exc_info=True) + return {'rent_arrears': [], 'monthly_precollection': [], 'expense': []} + + def _format_amount(self, value): + """格式化金额,去掉尾随的 .00 但保留非零小数""" + if value is None: + return "0" + rounded = round(float(value), 2) + if rounded == int(rounded): + return str(int(rounded)) + return f"{rounded:.2f}" + # ------------------------------------------------------- # 私有辅助方法 # ------------------------------------------------------- diff --git a/yuthon_report/static/src/components/yuthon_dashboard/yuthon_dashboard.xml b/yuthon_report/static/src/components/yuthon_dashboard/yuthon_dashboard.xml index d0af935b0..307b34a0a 100644 --- a/yuthon_report/static/src/components/yuthon_dashboard/yuthon_dashboard.xml +++ b/yuthon_report/static/src/components/yuthon_dashboard/yuthon_dashboard.xml @@ -124,6 +124,48 @@ + +
+
+ + 说 明 + + ▶ 展开 + ▼ 折叠 + +
+
+ + + + +
+
【租金欠收】
+ +
+ +
+ +
+
【本月预收】
+ +
+ +
+ +
+
【支出】
+ +
+ +
+
+
+
+
diff --git a/yuthon_report/static/src/components/yuthon_report/att_report.js b/yuthon_report/static/src/components/yuthon_report/att_report.js index a07304e20..198d0f641 100644 --- a/yuthon_report/static/src/components/yuthon_report/att_report.js +++ b/yuthon_report/static/src/components/yuthon_report/att_report.js @@ -78,7 +78,22 @@ class ReceivableReport extends Component { width: 200, minWidth: 150, flex: false, - resizable: false + resizable: false, + // valueGetter:对象值自动取display_name字符串,解决AG Grid 34 error #48 + valueGetter: (params) => { + if (!params.data || !params.colDef.field) return undefined; + const val = params.data[params.colDef.field]; + if (val && typeof val === 'object' && val.display_name !== undefined) { + return val.display_name; + } + return val; + }, + // 中文拼音排序,空值排末尾 + comparator: (valueA, valueB, nodeA, nodeB, isInverted) => { + if (valueA == null || valueA === '') return 1; + if (valueB == null || valueB === '') return -1; + return String(valueA).localeCompare(String(valueB), 'zh-CN'); + }, }, getContextMenuItems(params) { const items = params.defaultItems.slice(); diff --git a/yuthon_report/static/src/components/yuthon_report/receivable_flow_report.js b/yuthon_report/static/src/components/yuthon_report/receivable_flow_report.js index 67d8d1c3b..d75963f73 100644 --- a/yuthon_report/static/src/components/yuthon_report/receivable_flow_report.js +++ b/yuthon_report/static/src/components/yuthon_report/receivable_flow_report.js @@ -167,6 +167,22 @@ class ReceivableFlowReport extends Component { resizable: false, wrapText: false, autoHeight: false, + // valueGetter:对象值自动取display_name字符串,解决AG Grid 34 error #48(对象类型排序被拦截) + valueGetter: (params) => { + if (!params.data || !params.colDef.field) return undefined; + const val = params.data[params.colDef.field]; + // 对象值取display_name,供排序和显示使用 + if (val && typeof val === 'object' && val.display_name !== undefined) { + return val.display_name; + } + return val; + }, + // 中文拼音排序,空值排末尾 + comparator: (valueA, valueB, nodeA, nodeB, isInverted) => { + if (valueA == null || valueA === '') return 1; + if (valueB == null || valueB === '') return -1; + return String(valueA).localeCompare(String(valueB), 'zh-CN'); + }, valueFormatter: (params) => { // 如果值为false或null或undefined,返回空字符串 if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') { @@ -206,10 +222,14 @@ class ReceivableFlowReport extends Component { * 穿透渲染器:将对象格式的字段渲染为可点击的链接 */ linkRender(params) { - if (!params.value) return ''; + // valueGetter已将params.value转为字符串,需从params.data取原始对象值 + const rawField = params.colDef.field; + const rawValue = params.data ? params.data[rawField] : null; - if (typeof params.value === 'object' && params.value.model) { - const { model, id, display_name, view_mode, domain, view_id, method } = params.value; + if (!rawValue) return ''; + + if (typeof rawValue === 'object' && rawValue.model) { + const { model, id, display_name, view_mode, domain, view_id, method } = rawValue; const el = document.createElement('div'); el.className = 'o_record_cell'; diff --git a/yuthon_report/static/src/components/yuthon_report/receivable_message_report.js b/yuthon_report/static/src/components/yuthon_report/receivable_message_report.js index 1ca2ede7d..a9e02399c 100644 --- a/yuthon_report/static/src/components/yuthon_report/receivable_message_report.js +++ b/yuthon_report/static/src/components/yuthon_report/receivable_message_report.js @@ -49,7 +49,22 @@ class ReceiptLedgerReport extends Component { width: 200, minWidth: 150, flex: false, - resizable: false + resizable: false, + // valueGetter:对象值自动取display_name字符串,解决AG Grid 34 error #48 + valueGetter: (params) => { + if (!params.data || !params.colDef.field) return undefined; + const val = params.data[params.colDef.field]; + if (val && typeof val === 'object' && val.display_name !== undefined) { + return val.display_name; + } + return val; + }, + // 中文拼音排序,空值排末尾 + comparator: (valueA, valueB, nodeA, nodeB, isInverted) => { + if (valueA == null || valueA === '') return 1; + if (valueB == null || valueB === '') return -1; + return String(valueA).localeCompare(String(valueB), 'zh-CN'); + }, }, getContextMenuItems(params) { const items = params.defaultItems.slice(); 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 725daae3d..e69cddfd8 100644 --- a/yuthon_report/static/src/components/yuthon_report/receivable_report.js +++ b/yuthon_report/static/src/components/yuthon_report/receivable_report.js @@ -187,6 +187,22 @@ class ReceivableReport extends Component { resizable: false, wrapText: false, autoHeight: false, + // valueGetter:对象值自动取display_name字符串,解决AG Grid 34 error #48(对象类型排序被拦截) + valueGetter: (params) => { + if (!params.data || !params.colDef.field) return undefined; + const val = params.data[params.colDef.field]; + // 对象值取display_name,供排序和显示使用 + if (val && typeof val === 'object' && val.display_name !== undefined) { + return val.display_name; + } + return val; + }, + // 中文拼音排序,空值排末尾 + comparator: (valueA, valueB, nodeA, nodeB, isInverted) => { + if (valueA == null || valueA === '') return 1; + if (valueB == null || valueB === '') return -1; + return String(valueA).localeCompare(String(valueB), 'zh-CN'); + }, valueFormatter: (params) => { // 如果值为false或null或undefined,返回空字符串 if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') { @@ -239,11 +255,15 @@ class ReceivableReport extends Component { * 支持 view_mode: 'form' 或默认打开 form 弹窗 */ linkRender(params) { - if (!params.value) return ''; + // valueGetter已将params.value转为字符串,需从params.data取原始对象值 + const rawField = params.colDef.field; + const rawValue = params.data ? params.data[rawField] : null; - // 如果是对象格式(包含 model),渲染为可点击链接 - if (typeof params.value === 'object' && params.value.model) { - const { model, id, display_name, view_mode, domain, view_id, method } = params.value; + if (!rawValue) return ''; + + // 如果原始值是对象格式(包含 model),渲染为可点击链接 + if (typeof rawValue === 'object' && rawValue.model) { + const { model, id, display_name, view_mode, domain, view_id, method } = rawValue; const el = document.createElement('div'); el.className = 'o_record_cell'; diff --git a/yuthon_report/static/src/components/yuthon_report/simple_bank_report.js b/yuthon_report/static/src/components/yuthon_report/simple_bank_report.js index e630a5ee5..ad7c257e9 100644 --- a/yuthon_report/static/src/components/yuthon_report/simple_bank_report.js +++ b/yuthon_report/static/src/components/yuthon_report/simple_bank_report.js @@ -80,6 +80,21 @@ class SimpleBankReport extends Component { resizable: false, wrapText: false, autoHeight: false, + // valueGetter:对象值自动取display_name字符串,解决AG Grid 34 error #48 + valueGetter: (params) => { + if (!params.data || !params.colDef.field) return undefined; + const val = params.data[params.colDef.field]; + if (val && typeof val === 'object' && val.display_name !== undefined) { + return val.display_name; + } + return val; + }, + // 中文拼音排序,空值排末尾 + comparator: (valueA, valueB, nodeA, nodeB, isInverted) => { + if (valueA == null || valueA === '') return 1; + if (valueB == null || valueB === '') return -1; + return String(valueA).localeCompare(String(valueB), 'zh-CN'); + }, valueFormatter: (params) => { if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') { return ''; diff --git a/yuthon_report/static/src/components/yuthon_report/yuthon_contract_report.js b/yuthon_report/static/src/components/yuthon_report/yuthon_contract_report.js index 7446e761d..f69499eb8 100644 --- a/yuthon_report/static/src/components/yuthon_report/yuthon_contract_report.js +++ b/yuthon_report/static/src/components/yuthon_report/yuthon_contract_report.js @@ -40,7 +40,22 @@ class ContractInfoReport extends Component { width: 200, minWidth: 150, flex: false, - resizable: false + resizable: false, + // valueGetter:对象值自动取display_name字符串,解决AG Grid 34 error #48 + valueGetter: (params) => { + if (!params.data || !params.colDef.field) return undefined; + const val = params.data[params.colDef.field]; + if (val && typeof val === 'object' && val.display_name !== undefined) { + return val.display_name; + } + return val; + }, + // 中文拼音排序,空值排末尾 + comparator: (valueA, valueB, nodeA, nodeB, isInverted) => { + if (valueA == null || valueA === '') return 1; + if (valueB == null || valueB === '') return -1; + return String(valueA).localeCompare(String(valueB), 'zh-CN'); + }, }, getContextMenuItems: (params) => { diff --git a/yuthon_report/static/src/components/yuthon_report/yuthon_report.js b/yuthon_report/static/src/components/yuthon_report/yuthon_report.js index 62b0bf8df..eaf3c23fb 100644 --- a/yuthon_report/static/src/components/yuthon_report/yuthon_report.js +++ b/yuthon_report/static/src/components/yuthon_report/yuthon_report.js @@ -88,7 +88,29 @@ class YouthonReport extends Component { groupDefaultExpanded:1, // 分组展开 rowData: [], sideBar: true, - defaultColDef: { sortable: true, filter: true, width: 200,minWidth: 150,flex: false, resizable: false }, + defaultColDef: { + sortable: true, + filter: true, + width: 200, + minWidth: 150, + flex: false, + resizable: false, + // valueGetter:对象值自动取display_name字符串,解决AG Grid 34 error #48 + valueGetter: (params) => { + if (!params.data || !params.colDef.field) return undefined; + const val = params.data[params.colDef.field]; + if (val && typeof val === 'object' && val.display_name !== undefined) { + return val.display_name; + } + return val; + }, + // 中文拼音排序,空值排末尾 + comparator: (valueA, valueB, nodeA, nodeB, isInverted) => { + if (valueA == null || valueA === '') return 1; + if (valueB == null || valueB === '') return -1; + return String(valueA).localeCompare(String(valueB), 'zh-CN'); + }, + }, }; this.grid = new agGrid.createGrid(this.gridRef.el, this.gridOptions);