报表
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
# -------------------------------------------------------
|
||||
# 私有辅助方法
|
||||
# -------------------------------------------------------
|
||||
|
||||
@@ -124,6 +124,48 @@
|
||||
|
||||
</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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user