Merge pull request !10323 from 李鹏宇/dev
This commit is contained in:
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user