!10323 !10322 Merge branch 'dev' of <a href="https://gitee.com/jinan-pengyuse…

Merge pull request !10323 from 李鹏宇/dev
This commit is contained in:
李鹏宇
2026-03-27 08:34:59 +00:00
committed by Gitee
5 changed files with 482 additions and 17 deletions
+1
View File
@@ -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',
],
}
@@ -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,
"收款日期": "",
"收款金额": "",
"收款备注": "",
@@ -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);
@@ -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';
@@ -2,7 +2,7 @@
<odoo>
<record id="action_receivable_flow_report" model="ir.actions.client">
<field name="name">应收流水中间报表</field>
<field name="tag">receivable_report_client</field>
<field name="tag">receivable_flow_report_client</field>
<field name="context">
{
"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"},