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);