This commit is contained in:
李鹏宇
2026-06-18 15:35:56 +08:00
parent 4a4223a8b0
commit 937d95e0ed
10 changed files with 262 additions and 17 deletions
+12 -6
View File
@@ -15,10 +15,10 @@ class PreArrearsReport(models.Model):
""" """
前期欠收报表(数据源:receivable.modify.line): 前期欠收报表(数据源:receivable.modify.line):
- 查询条件的日期对应应收账单的 due_date(应收日期在查询月之前,不包含查询月) - 查询条件的日期对应应收账单的 due_date(应收日期在查询月之前,不包含查询月)
- 欠收条件: - 欠收条件(二选一):
- 收款日期(payout_date)为空: 从未收款 → 欠收 - 收款日期(payout_date)为空: 从未收款 → 欠收
- 收款日期在查询月之后: 查询月时点还没收到钱 → 欠收 - 收款日期所在月 >= 查询月 且 收款日期所在月 > 应收日期所在月: 收款跨月且查询月才收到 → 欠收
- 收款日期 <= 查询月末: 已经交过了 → 不算欠收 - 同月收款不算欠收(如1月15日收1月1日应收的款)
- 金额取值: - 金额取值:
- 如果 not_collected > 0,取 not_collected (当前还欠的金额) - 如果 not_collected > 0,取 not_collected (当前还欠的金额)
- 否则取 amount (已对金额) - 否则取 amount (已对金额)
@@ -91,10 +91,16 @@ class PreArrearsReport(models.Model):
lines = self.sudo().search(domain, order="receivable_id asc, payout_date asc") lines = self.sudo().search(domain, order="receivable_id asc, payout_date asc")
_logger.info("Part1 search found %s modify.line records", len(lines)) _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( 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)) _logger.info("Part1 after欠收filter: %s records remain", len(lines))
+75
View File
@@ -273,8 +273,83 @@ class YuthonDashboard(models.Model):
return { return {
'filters': filters, 'filters': filters,
'sections': sections, '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}"
# ------------------------------------------------------- # -------------------------------------------------------
# 私有辅助方法 # 私有辅助方法
# ------------------------------------------------------- # -------------------------------------------------------
@@ -124,6 +124,48 @@
</div> <!-- /section-cards --> </div> <!-- /section-cards -->
<!-- 说明板块:仅在经营收入(income)之后、其他收入之前显示 -->
<div t-if="section.id === 'income'" class="notes-section">
<div class="section-header" style="display: flex; align-items: center; margin: 16px 0 8px 0; cursor: pointer;"
t-on-click="onToggleSection"
data-section-id="notes">
<span style="display: inline-block; width: 4px; height: 20px; background: #1890ff; margin-right: 8px; border-radius: 2px;"></span>
<strong style="font-size: 16px; color: #1a1a1a;">说 明</strong>
<span style="margin-left: 8px; font-size: 12px; color: #999;">
<t t-if="state.collapsedSections['notes']">▶ 展开</t>
<t t-else="">▼ 折叠</t>
</span>
</div>
<div t-if="!state.collapsedSections['notes']"
style="border: 1px solid #e8e8e8; border-radius: 6px; padding: 12px 16px; background: #fff;">
<!-- 有片区管理员且有说明数据时展示 -->
<t t-if="state.dashboardData and state.dashboardData.notes">
<t t-set="notes" t-value="state.dashboardData.notes"/>
<!-- 租金欠收 -->
<div t-if="notes.rent_arrears and notes.rent_arrears.length > 0" style="margin-bottom: 10px;">
<div style="font-weight: bold; font-size: 14px; color: #1a1a1a; margin-bottom: 4px;">【租金欠收】</div>
<t t-foreach="notes.rent_arrears" t-as="item" t-key="item_index">
<div style="font-size: 13px; color: #333; line-height: 1.8;" t-esc="item.text"/>
</t>
</div>
<!-- 本月预收 -->
<div t-if="notes.monthly_precollection and notes.monthly_precollection.length > 0" style="margin-bottom: 10px;">
<div style="font-weight: bold; font-size: 14px; color: #1a1a1a; margin-bottom: 4px;">【本月预收】</div>
<t t-foreach="notes.monthly_precollection" t-as="item" t-key="item_index">
<div style="font-size: 13px; color: #333; line-height: 1.8;" t-esc="item.text"/>
</t>
</div>
<!-- 支出 -->
<div t-if="notes.expense and notes.expense.length > 0">
<div style="font-weight: bold; font-size: 14px; color: #1a1a1a; margin-bottom: 4px;">【支出】</div>
<t t-foreach="notes.expense" t-as="exp_text" t-key="exp_index">
<div style="font-size: 13px; color: #333; line-height: 1.8;" t-esc="exp_text"/>
</t>
</div>
</t>
</div>
</div>
</t> </t>
</div> </div>
@@ -78,7 +78,22 @@ class ReceivableReport extends Component {
width: 200, width: 200,
minWidth: 150, minWidth: 150,
flex: false, 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) { getContextMenuItems(params) {
const items = params.defaultItems.slice(); const items = params.defaultItems.slice();
@@ -167,6 +167,22 @@ class ReceivableFlowReport extends Component {
resizable: false, resizable: false,
wrapText: false, wrapText: false,
autoHeight: 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) => { valueFormatter: (params) => {
// 如果值为false或null或undefined,返回空字符串 // 如果值为false或null或undefined,返回空字符串
if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') { if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') {
@@ -206,10 +222,14 @@ class ReceivableFlowReport extends Component {
* 穿透渲染器:将对象格式的字段渲染为可点击的链接 * 穿透渲染器:将对象格式的字段渲染为可点击的链接
*/ */
linkRender(params) { 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) { if (!rawValue) return '';
const { model, id, display_name, view_mode, domain, view_id, method } = params.value;
if (typeof rawValue === 'object' && rawValue.model) {
const { model, id, display_name, view_mode, domain, view_id, method } = rawValue;
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'o_record_cell'; el.className = 'o_record_cell';
@@ -49,7 +49,22 @@ class ReceiptLedgerReport extends Component {
width: 200, width: 200,
minWidth: 150, minWidth: 150,
flex: false, 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) { getContextMenuItems(params) {
const items = params.defaultItems.slice(); const items = params.defaultItems.slice();
@@ -187,6 +187,22 @@ class ReceivableReport extends Component {
resizable: false, resizable: false,
wrapText: false, wrapText: false,
autoHeight: 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) => { valueFormatter: (params) => {
// 如果值为false或null或undefined,返回空字符串 // 如果值为false或null或undefined,返回空字符串
if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') { 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 弹窗 * 支持 view_mode: 'form' 或默认打开 form 弹窗
*/ */
linkRender(params) { 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 (!rawValue) return '';
if (typeof params.value === 'object' && params.value.model) {
const { model, id, display_name, view_mode, domain, view_id, method } = params.value; // 如果原始值是对象格式(包含 model),渲染为可点击链接
if (typeof rawValue === 'object' && rawValue.model) {
const { model, id, display_name, view_mode, domain, view_id, method } = rawValue;
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'o_record_cell'; el.className = 'o_record_cell';
@@ -80,6 +80,21 @@ class SimpleBankReport extends Component {
resizable: false, resizable: false,
wrapText: false, wrapText: false,
autoHeight: 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) => { valueFormatter: (params) => {
if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') { if (params.value === false || params.value === null || params.value === undefined || params.value === 'false') {
return ''; return '';
@@ -40,7 +40,22 @@ class ContractInfoReport extends Component {
width: 200, width: 200,
minWidth: 150, minWidth: 150,
flex: false, 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) => { getContextMenuItems: (params) => {
@@ -88,7 +88,29 @@ class YouthonReport extends Component {
groupDefaultExpanded:1, // 分组展开 groupDefaultExpanded:1, // 分组展开
rowData: [], rowData: [],
sideBar: true, 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); this.grid = new agGrid.createGrid(this.gridRef.el, this.gridOptions);