From 840674145964c700661e3287bf59e585008bef43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Fri, 27 Mar 2026 16:33:49 +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/__manifest__.py | 1 + .../models/receivable_flow_report.py | 6 +- .../yuthon_report/receivable_flow_report.js | 475 ++++++++++++++++++ .../yuthon_report/receivable_report.js | 14 - .../views/receivable_flow_report.xml | 3 +- 5 files changed, 482 insertions(+), 17 deletions(-) create mode 100644 yuthon_report/static/src/components/yuthon_report/receivable_flow_report.js diff --git a/yuthon_report/__manifest__.py b/yuthon_report/__manifest__.py index f7ea90084..87b40b247 100644 --- a/yuthon_report/__manifest__.py +++ b/yuthon_report/__manifest__.py @@ -65,6 +65,7 @@ 'yuthon_report/static/src/components/yuthon_report/yuthon_report.scss', 'yuthon_report/static/src/components/yuthon_report/receivable_report.js', 'yuthon_report/static/src/components/yuthon_report/receivable_report.xml', + 'yuthon_report/static/src/components/yuthon_report/receivable_flow_report.js', ], } diff --git a/yuthon_report/models/receivable_flow_report.py b/yuthon_report/models/receivable_flow_report.py index d8f73520a..d60e15554 100644 --- a/yuthon_report/models/receivable_flow_report.py +++ b/yuthon_report/models/receivable_flow_report.py @@ -114,6 +114,10 @@ class ReceivableFlowReport(models.Model): ) 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 "未收" + seq += 1 contract_key = contract.contract_code if (contract and contract.contract_code) else "contract_{}".format(seq) # ==== 合同头行 ==== @@ -151,7 +155,7 @@ class ReceivableFlowReport(models.Model): 'domain': [('receivable_id.contract_id', '=', contract.id if contract else 0), ('reconciliation_date', '!=', False)], }, "未收": round(contract_total_unpaid, 2), - "状态": "", + "状态": latest_status, "收款日期": "", "收款金额": "", "收款备注": "", 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 new file mode 100644 index 000000000..655fb2de3 --- /dev/null +++ b/yuthon_report/static/src/components/yuthon_report/receivable_flow_report.js @@ -0,0 +1,475 @@ +/** @odoo-module **/ + + +import {registry} from "@web/core/registry"; +import {Component, onMounted, useRef, useState} from "@odoo/owl"; +import {useBus, useService} from "@web/core/utils/hooks"; +import { + standardActionServiceProps +} from "@web/webclient/actions/action_service"; +import {DateTimeInput} from "@web/core/datetime/datetime_input"; +import {CheckBox} from "@web/core/checkbox/checkbox"; +import {Many2XAutocomplete} from "@web/views/fields/relational_utils"; +import {formatDate} from "@web/core/l10n/dates" + +const {DateTime} = luxon; + +class ReceivableFlowReport extends Component { + setup() { + this.gridRef = useRef("grid"); + console.log(this.props.action.context.report_config) + + this.serviceOrm = useService("orm") + this.actionService = useService("action"); // 获取 actionService 用于跳转 + + // 计算 本月的开始,结束 + + this.state = useState({ + startDate: DateTime.now().startOf("month"), + endDate: DateTime.now().endOf("month"), + singleDate: DateTime.now().startOf("month"), + keyword: "", + booleanValue: true, + selectPartner: {}, + selectCompany: {}, + selectManagementType: {}, + selectProperty: {}, + selectLessee: {}, + selectContract: {}, + condition: false, + conditions: [ + {'name': '', 'value': ''}, + {'name': '自有', 'value': 'self'}, + {'name': '代管', 'value': 'agency'} + ], + domainUsers: [], + stats: [] // 新增统计数据 + }); + + this.config = this.props.action.context.report_config || {}; + // dateMode: 'range'(默认,日期范围)或 'single'(单日期) + this.dateMode = this.config.dateMode || 'range'; + + onMounted(async () => { + // 统一处理 columns:给所有列添加穿透渲染器 + const columns = this.config.columns || []; + columns.forEach((col) => { + col.cellRenderer = this.linkRender.bind(this); + // 对固定宽度列(如序号列)单独设置 minWidth,避免被 defaultColDef 覆盖 + if (col.width && col.flex === 0) { + col.minWidth = col.width; + col.maxWidth = col.width; + } + // 为"收款时间状态"列添加动态样式 + if (col.field === '收款时间状态') { + col.cellClass = (params) => { + if (!params.value) return ''; + if (params.value === '交晚') { + return 'payment-status-late'; + } else if (params.value === '交早') { + return 'payment-status-early'; + } + return ''; + }; + } + }); + + this.gridOptions = { + localeText: AG_GRID_LOCALE_CN, + theme: agGrid.themeMaterial, + columnDefs: columns, + rowData: [], + // ===== Tree Data 折叠展开(应收流水中间报表专用)===== + treeData: true, + getDataPath: (data) => data.orgHierarchy || [], + groupDefaultExpanded: 0, // 默认全部折叠 + autoGroupColumnDef: { + headerName: '合同/明细', + minWidth: 250, + flex: 1.5, + pinned: 'left', + cellRendererParams: { + suppressCount: true, + innerRenderer: (params) => { + if (!params.data) return ''; + const rowType = params.data['row_type']; + if (rowType === '合同') { + // 合同行:序号 + 合同编号 + const seq = params.data['序号'] || ''; + const contractVal = params.data['合同编号']; + const contractName = typeof contractVal === 'object' ? (contractVal.display_name || '') : (contractVal || ''); + return seq ? `${seq} ${contractName}` : contractName; + } else if (rowType === '主') { + // 租金行:显示应收日期 + return params.data['应收日期'] || ''; + } + // 收款子行:显示收款日期 + return params.data['收款日期'] || ''; + }, + }, + }, + // ===== 行样式 ===== + rowClassRules: { + '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 === '收款'; + } + }, + sideBar: { + toolPanels: [ + { + id: 'columns', + labelDefault: '列', + labelKey: 'columns', + iconKey: 'columns', + toolPanel: 'agColumnsToolPanel', + toolPanelParams: { + suppressValues: true, + suppressPivots: true, + suppressPivotMode: true, + } + }, + { + id: 'filters', + labelDefault: '过滤器', + labelKey: 'filters', + iconKey: 'filter', + toolPanel: 'agFiltersToolPanel' + } + ], + defaultToolPanel: '', + }, + enableCharts: true, + cellSelection: true, + defaultColDef: { + sortable: true, + filter: true, + width: 200, + minWidth: 150, + flex: false, + resizable: false, + wrapText: false, + autoHeight: false, + valueFormatter: (params) => { + // 如果值为false或null或undefined,返回空字符串 + if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') { + return ''; + } + return params.value; + } + }, + getContextMenuItems(params) { + const items = params.defaultItems.slice(); + items.push("chartRange"); + return items; + }, + context: { + actionService: this.actionService, // 传递 actionService 用于穿透跳转 + orm: this.serviceOrm, // 传递 orm 用于调用模型方法 + }, + onFilterChanged: () => { + // 过滤后重新编号序号列(仅合同头行编号) + let num = 1; + this.grid.forEachNodeAfterFilter((node) => { + if (node.data && node.data['row_type'] === '合同') { + node.data['序号'] = num++; + } else { + node.data['序号'] = ''; + } + }); + this.grid.refreshCells({ columns: ['序号'], force: true }); + } + }; + this.grid = new agGrid.createGrid(this.gridRef.el, this.gridOptions); + + }); + } + + /** + * 穿透渲染器:将对象格式的字段渲染为可点击的链接 + */ + linkRender(params) { + if (!params.value) return ''; + + if (typeof params.value === 'object' && params.value.model) { + const { model, id, display_name, view_mode, domain, view_id, method } = params.value; + + const el = document.createElement('div'); + el.className = 'o_record_cell'; + el.innerText = display_name || ''; + el.classList.add('btn'); + el.classList.add('btn-link'); + el.onclick = async () => { + if (method) { + const action = await params.context.orm.call(model, method, [[id]]); + if (action && !action.views) { + action.views = [[false, 'form']]; + } + params.context.actionService.doAction(action); + } else if (view_mode === 'tree') { + params.context.actionService.doAction({ + type: 'ir.actions.act_window', + res_model: model, + views: [[view_id || false, 'tree']], + domain: domain || [], + target: 'new', + }); + } else { + params.context.actionService.doAction({ + type: 'ir.actions.act_window', + res_model: model, + res_id: id, + views: [[false, 'form'], [false, 'search']], + target: 'new' + }); + } + }; + + return el; + } + + return params.value; + } + + async onChangeBoolean(v) { + this.state.booleanValue = v + } + + getDomainCompany() { + return [] + } + + getDomainManagementType() { + return []; + } + + getDomain() { + const isCompanyEmpty = Object.keys(this.state.selectCompany).length === 0; + if (isCompanyEmpty) { + return []; + } else { + return [['id', 'in', this.state.domainUsers]]; + } + } + + async onUpdatePartner(v) { + this.state.selectPartner = v[0] + await this.onSearch() + } + + async searchUserDomain() { + this.state.selectPartner = {} + let startDate = formatDate(this.state.startDate, {format: "yyyy-MM-dd"}) + let endDate = formatDate(this.state.endDate, {format: "yyyy-MM-dd"}) + + let results = await this.serviceOrm.call( + 'property.accounts.receivable', + 'get_domain', + [], + { + company_id: this.state.selectCompany?.id, + start_date: startDate, + end_date: endDate, + } + ) + this.state.domainUsers = results + } + + async onUpdateCompany(v) { + if (!v || !v[0]) { + this.state.selectCompany = {}; + this.state.selectPartner = {}; + this.state.domainUsers = []; + await this.onSearch(); + return; + } + let values = await this.serviceOrm.call( + 'yuthon.property.company', + 'read', + [[v[0].id]], + { + fields: ['display_name'], + } + ) + this.state.selectCompany = values[0] || {} + await this.searchUserDomain() + await this.onSearch() + } + + onClickPreMonth(){ + this.state.startDate = this.state.startDate.minus({months: 1}); + this.state.endDate = this.state.startDate.endOf("month"); + this.state.singleDate = this.state.startDate; + this.onSearch() + } + + onClickNextMonth() { + this.state.startDate = this.state.startDate.plus({months: 1}); + this.state.endDate = this.state.startDate.endOf("month"); + this.state.singleDate = this.state.startDate; + this.onSearch() + } + + async onChangeSingleDate(date) { + if (date) { + this.state.singleDate = Array.isArray(date) ? date[0] : date; + await this.onSearch(); + } + } + + async onSearch() { + let params; + if (this.dateMode === 'single') { + let singleDate = formatDate(this.state.singleDate, {format: "yyyy-MM-dd"}); + params = { + date_start: `${singleDate} 00:00:00`, + date_end: `${singleDate} 23:59:59`, + keyword: this.state.keyword || false, + condition: this.state.condition || false, + partner_id: this.state.selectPartner?.id, + company_id: this.state.selectCompany?.id, + management_type_id: this.state.selectManagementType?.id || false, + property_id: this.state.selectProperty?.id || false, + lessee_id: this.state.selectLessee?.id || false, + contract_id: this.state.selectContract?.id || false, + }; + } else { + let startDate = formatDate(this.state.startDate, {format: "yyyy-MM-dd"}) + let endDate = formatDate(this.state.endDate, {format: "yyyy-MM-dd"}) + params = { + date_start: `${startDate} 00:00:00`, + date_end: `${endDate} 23:59:59`, + keyword: this.state.keyword || false, + condition: this.state.condition || false, + partner_id: this.state.selectPartner?.id, + company_id: this.state.selectCompany?.id, + management_type_id: this.state.selectManagementType?.id || false, + property_id: this.state.selectProperty?.id || false, + lessee_id: this.state.selectLessee?.id || false, + contract_id: this.state.selectContract?.id || false, + }; + } + const result = await this.serviceOrm.call( + this.config.model, + this.config.method, + [params] + ); + + if (result && typeof result === 'object') { + if (result.rows) { + this.grid.setGridOption('rowData', result.rows); + this.grid.setGridOption('pinnedBottomRowData', []); + this.state.stats = result.stats || []; + } else { + this.grid.setGridOption('rowData', result); + this.grid.setGridOption('pinnedBottomRowData', []); + this.state.stats = []; + } + } + this._applyManagementTypeFilter(); + } + + onExportExcel() { + this.grid.exportDataAsExcel({ + fileName: 'my-data.xlsx', + sheetName: 'Sheet1', + processCellCallback(params) { + const value = params.value + if (value === undefined || value === null || value === '') { + return '' + } + if (typeof value === 'object') { + return value?.display_name ?? '' + } + return value + }, + }); + } + + async onChangeStartDate(date) { + if (Array.isArray(date)) { + this.state.startDate = date[0]; + this.state.endDate = date[1] + } + } + + _getFeeColumnByTypeName(typeName) { + if (!typeName) return null; + if (typeName.includes('租')) return '租金'; + if (typeName.includes('管理') || typeName.includes('管')) return '管理费'; + if (typeName.includes('水')) return '水费'; + if (typeName.includes('电')) return '电费'; + if (typeName.includes('违约')) return '违约金'; + return '其他费用'; + } + + async onUpdateManagementType(v) { + this.state.selectManagementType = v[0] || {}; + this._applyManagementTypeFilter(); + } + + async onUpdateProperty(v) { + this.state.selectProperty = v[0] || {}; + await this.onSearch(); + } + + async onUpdateLessee(v) { + this.state.selectLessee = v[0] || {}; + await this.onSearch(); + } + + async onUpdateContract(v) { + this.state.selectContract = v[0] || {}; + await this.onSearch(); + } + + _applyManagementTypeFilter() { + if (!this.grid) return; + const typeName = this.state.selectManagementType?.display_name || ''; + if (!typeName) { + this.grid.setGridOption('isExternalFilterPresent', () => false); + this.grid.setGridOption('doesExternalFilterPass', () => true); + } else { + const hasPaymentTypeField = this.config.columns.some(col => col.field === '收款类型'); + this.grid.setGridOption('isExternalFilterPresent', () => true); + this.grid.setGridOption('doesExternalFilterPass', (node) => { + if (!node.data) return true; + if (hasPaymentTypeField) { + const val = node.data['收款类型']; + return val === typeName; + } else { + const col = this._getFeeColumnByTypeName(typeName); + const val = node.data[col]; + return val !== '' && val !== null && val !== undefined && val !== 0; + } + }); + } + this.grid.onFilterChanged(); + } + + onChangeCondition() { + this.onSearch() + } + + async onChangeEndDate(date) { + if (Array.isArray(date)) { + this.state.startDate = date[0]; + this.state.endDate = date[1] + } + } +} + +ReceivableFlowReport.props = {...standardActionServiceProps}; +ReceivableFlowReport.template = "youthon_report.ReceivableReport"; +ReceivableFlowReport.components = { + DateTimeInput, + CheckBox, + Many2XAutocomplete, +}; + +registry.category("actions").add("receivable_flow_report_client", ReceivableFlowReport); 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 34bedc369..434ef7b5f 100644 --- a/yuthon_report/static/src/components/yuthon_report/receivable_report.js +++ b/yuthon_report/static/src/components/yuthon_report/receivable_report.js @@ -82,20 +82,6 @@ class ReceivableReport extends Component { theme: agGrid.themeMaterial, columnDefs: columns, rowData: [], - // ===== Tree Data 折叠展开 ===== - treeData: true, - getDataPath: (data) => data.orgHierarchy || [], - groupDefaultExpanded: 0, // 默认全部折叠 - autoGroupColumnDef: { - headerName: '合同/明细', - minWidth: 200, - flex: 1.5, - pinned: 'left', - cellRendererParams: { - suppressCount: true, // 不显示子行数量 - }, - }, - // ===== 行样式 ===== rowClassRules: { 'ag-row-new-rent': function(params) { return params.data && params.data.acc_type === 'new'; diff --git a/yuthon_report/views/receivable_flow_report.xml b/yuthon_report/views/receivable_flow_report.xml index 5d341fdfa..acc9ece95 100644 --- a/yuthon_report/views/receivable_flow_report.xml +++ b/yuthon_report/views/receivable_flow_report.xml @@ -2,7 +2,7 @@ 应收流水中间报表 - receivable_report_client + receivable_flow_report_client { "sideBar": true, @@ -11,7 +11,6 @@ "method": "get_receivable_flow_report", "dateMode": "range", "columns": [ - {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"}, {"headerName": "租金id", "field": "租金id", "flex": 1}, {"headerName": "合同编号", "field": "合同编号", "flex": 1.2, "cellRenderer": "linkRender"}, {"headerName": "物业名称", "field": "物业名称", "flex": 2, "cellRenderer": "linkRender"},