Files
gzth/yuthon_report/models/yuthon_dashboard.py
T
2026-06-22 11:16:43 +08:00

573 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
import datetime
import calendar
import logging
from odoo import api, models
from .report_utils import _id_list
_logger = logging.getLogger(__name__)
class YuthonDashboard(models.Model):
_name = 'yuthon.dashboard'
_description = '经营汇总看板'
@api.model
def get_dashboard_data(self, params):
"""
聚合所有子报表的汇总数据,返回帆软风格的卡片布局数据。
:param params: { date_start, date_end, company_id, partner_id, keyword, condition }
:return: { filters, sections }
"""
today = datetime.date.today()
# 标准化日期参数 — 只有用户明确选了月份才传日期
date_start_raw = params.get("date_start")
has_date = bool(date_start_raw)
if has_date:
if isinstance(date_start_raw, str) and len(date_start_raw) == 7: # YYYY-MM
y, m = map(int, date_start_raw.split('-'))
query_date = datetime.date(y, m, 1)
else:
query_date = datetime.datetime.fromisoformat(date_start_raw).date()
else:
# 不选日期时用当月作为页面显示,但不强塞给子报表
query_date = today.replace(day=1)
# 构建各报表所需的标准化 params
# 注意:大部分子报表使用 dateMode="single",前端 ReceivableReport 传的是:
# date_start = "YYYY-MM-DD 00:00:00", date_end = "YYYY-MM-DD 23:59:59"
# 只有 receivable_flow_report 使用 dateMode="range"(但在本看板中不调用)
base_params = {}
if has_date:
qd_str = query_date.strftime("%Y-%m-%d")
base_params['date_start'] = f"{qd_str} 00:00:00"
base_params['date_end'] = f"{qd_str} 23:59:59"
for key in ('company_id', 'partner_id', 'keyword', 'condition',
'property_id', 'lessee_id', 'contract_id', 'management_type_id'):
if key in params and params[key]:
base_params[key] = params[key]
# 提取所有汇总金额(包装异常,单个报表失败不影响整体)
def safe_get(model_name, method_name, stats_index=1, overrides=None):
"""安全调用子报表方法,返回汇总金额
:param overrides: 可选 dict,覆盖 base_params 中的参数
看板只取 stats 不构建 rows,传 summary_only=True 减少遍历开销
"""
try:
params = dict(base_params)
if overrides:
params.update(overrides)
params['summary_only'] = True # 看板只取汇总,跳过 rows 构建
result = getattr(self.env[model_name], method_name)(params)
stats = result.get('stats', [])
if stats and len(stats) > stats_index:
return stats[stats_index].get('amount', 0.0)
elif stats:
return stats[0].get('amount', 0.0)
return 0.0
except Exception as e:
_logger.error("[DASHBOARD_SAFE_GET_ERROR] model=%s method=%s error=%s", model_name, method_name, e, exc_info=True)
return 0.0
# 全月范围参数(用于 dateMode="range" 的报表)
if has_date:
last_day = calendar.monthrange(query_date.year, query_date.month)[1]
range_params = {
'date_start': f"{query_date.strftime('%Y-%m-%d')} 00:00:00",
'date_end': f"{query_date.replace(day=last_day).strftime('%Y-%m-%d')} 23:59:59",
}
else:
range_params = {}
# ==================== 一、经营收入板块 ====================
# 当期应收(合同应收):receivable_report.py 的 get_complete_ledger_report
# stats: [租金, 管理费, 水费, 电费, 其他费用, 小计合计] → 取 index 5(违约金已移除)
# 注意:该报表使用 ReceivableReport 组件,默认 dateMode="range"(全月范围)
current_receivable = safe_get('property.accounts.receivable', 'get_complete_ledger_report', 5, overrides=range_params)
# 前期欠收
pre_arrears = safe_get('receivable.modify.line', 'get_pre_arrears_report', 1)
# 前期预收
pre_advance = safe_get('receivable.modify.line', 'get_pre_advance_report', 1)
# 本月应收 = 当期应收 + 前期欠收 - 前期预收(仅展示,不跳转)
month_receivable = current_receivable + pre_arrears - pre_advance
# 实收总额
total_received = safe_get('receivable.modify.line', 'get_total_received_report', 0)
# 归属本月
current_month_received = safe_get('receivable.modify.line', 'get_current_month_received_report', 0)
# 本月预收
monthly_precollection = safe_get('receivable.modify.line', 'get_monthly_precollection_report', 1)
# 累计欠收
cum_arrears = safe_get('receivable.modify.line', 'get_cum_arrears_report', 1)
# 累计收入
cum_income = safe_get('receivable.modify.line', 'get_cum_income_report', 1)
# ==================== 二、其他收入板块 ====================
# 违约金:调用三个独立报表方法
# 累计欠收:stats = [记录总数, 违约金总额, 违约金已收, 违约金未收] → 取 index 3(违约金未收)
# 时间过滤:用 range_params(月末),与报表页面保持一致
penalty_cum_arrears = safe_get(
'property.accounts.receivable', 'get_penalty_arrears_report', 3,
overrides=range_params
)
_logger.info("[DASHBOARD_PENALTY_ARREARS] range_params=%s, value=%s", range_params, penalty_cum_arrears)
# 本月实收:stats = [记录总数, 实收违约金] → 取 index 1
penalty_month_received = safe_get(
'property.accounts.receivable', 'get_penalty_month_received_report', 1,
overrides=range_params
)
# 本年累计:stats = [记录总数, 实收违约金] → 取 index 1,范围 1月1日 到 查询月末
penalty_year_cum_params = dict(base_params)
if has_date:
y = query_date.year
penalty_year_cum_params['date_start'] = f"{y}-01-01 00:00:00"
penalty_year_cum = safe_get(
'property.accounts.receivable', 'get_penalty_year_cum_report', 1,
overrides=penalty_year_cum_params
)
# 停车费
parking_month_receivable = safe_get('property.accounts.receivable', 'get_parking_fee_current_month_receivable', 0)
parking_month_received = safe_get('property.accounts.receivable', 'get_parking_fee_current_month_report', 0)
parking_year_cum = safe_get('property.accounts.receivable', 'get_parking_fee_yearly_report', 0)
# ==================== 三、押金板块 ====================
deposit_month_receivable = safe_get('property.accounts.receivable', 'get_deposit_month_receivable_report', 1)
deposit_pre_received = safe_get('property.accounts.receivable', 'get_deposit_pre_received_report', 1)
deposit_month_received = safe_get('property.accounts.receivable', 'get_deposit_month_received_report', 1)
deposit_cum_arrears = safe_get('property.accounts.receivable', 'get_deposit_cum_arrears_report', 1)
deposit_month_refund = safe_get('property.accounts.receivable', 'get_deposit_month_refund_report', 1)
deposit_total = safe_get('property.accounts.receivable', 'get_deposit_total_report', 1)
# ==================== 四、费用支出板块 ====================
# 直接查询本月费用支出(管理费)、维修费、评估费
fee_data = self._get_fee_expense_data(base_params, query_date)
# ==================== 五、面积/比率板块 ====================
area_data = self._get_area_data(base_params, query_date)
# ==================== 汇总信息 ====================
filters = {
'统计时间': f"{query_date.year}-{query_date.month:02d}",
# 多选:所属公司传ID列表
'所属公司': self._get_company_display(params.get('company_id')),
'管理员': self._get_admin_display(params.get('partner_id')),
}
# ==================== 构建筛选条件上下文(透传给子报表) ====================
# 子报表从 action.context.dashboard_filter 中读取并初始化筛选状态
# date_str: "YYYY-MM" 格式,子报表据此还原 DateTime 对象
# 多选:company_id/partner_id 透传ID列表,名称拼接
cid_list = _id_list(params.get('company_id'))
dashboard_filter = {
'date_str': f"{query_date.year}-{query_date.month:02d}" if has_date else '',
'company_id': cid_list or False,
'company_name': self._get_company_display(params.get('company_id')) if cid_list else '',
'partner_id': params.get('partner_id') or False,
'partner_name': self._get_admin_display(params.get('partner_id')) if params.get('partner_id') else '',
'condition': params.get('condition') or '',
}
def make_ctx(extra=None):
ctx = {'dashboard_filter': dashboard_filter}
if extra:
ctx.update(extra)
return ctx
sections = [
# 一、经营收入
{
'id': 'income',
'title': '一、经营收入',
'cards': [
{'label': '当期应收', 'value': current_receivable, 'is_negative': False,
'action': 'yuthon_report.action_property_accounts_receivable_report', 'action_context': make_ctx()},
{'label': '前期欠收', 'value': pre_arrears, 'is_negative': True,
'action': 'yuthon_report.action_pre_arrears_report', 'action_context': make_ctx()},
{'label': '前期预收', 'value': pre_advance, 'is_negative': False,
'action': 'yuthon_report.action_pre_advance_report', 'action_context': make_ctx()},
{'label': '本月应收', 'value': month_receivable, 'is_negative': False},
{'label': '实收总额', 'value': total_received, 'is_negative': False,
'action': 'yuthon_report.action_total_received_report', 'action_context': make_ctx()},
{'label': '归属本月', 'value': current_month_received, 'is_negative': False,
'action': 'yuthon_report.action_current_month_received_report', 'action_context': make_ctx()},
{'label': '本月预收', 'value': monthly_precollection, 'is_negative': False,
'action': 'yuthon_report.action_monthly_precollection_report', 'action_context': make_ctx()},
{'label': '累计欠收', 'value': cum_arrears, 'is_negative': True,
'action': 'yuthon_report.action_cum_arrears_report', 'action_context': make_ctx()},
{'label': '累计收入', 'value': cum_income, 'is_negative': False,
'action': 'yuthon_report.action_cum_income_report', 'action_context': make_ctx()},
],
'row_groups': [
['当期应收', '前期欠收', '前期预收', '本月应收', '实收总额'],
['归属本月', '本月预收', '累计欠收', '累计收入'],
],
},
# 二、其他收入
{
'id': 'other_income',
'title': '二、其他收入',
'cards': [
{'label': '违约金·累计欠收', 'value': penalty_cum_arrears, 'is_negative': True,
'action': 'yuthon_report.action_penalty_arrears_report',
'action_context': make_ctx({'date_end': range_params.get('date_end')})},
{'label': '违约金·本月实收', 'value': penalty_month_received, 'is_negative': False,
'action': 'yuthon_report.action_penalty_month_received_report',
'action_context': make_ctx({'date_start': range_params.get('date_start'), 'date_end': range_params.get('date_end')})},
{'label': '违约金·本年累计', 'value': penalty_year_cum, 'is_negative': False,
'action': 'yuthon_report.action_penalty_year_cum_report',
'action_context': make_ctx({'date_start': penalty_year_cum_params.get('date_start'), 'date_end': penalty_year_cum_params.get('date_end')})},
{'label': '停车费·本月应收', 'value': parking_month_receivable, 'is_negative': False,
'action': 'yuthon_report.action_parking_fee_current_month_receivable', 'action_context': make_ctx()},
{'label': '停车费·本月实收', 'value': parking_month_received, 'is_negative': False,
'action': 'yuthon_report.action_parking_fee_current_month_report', 'action_context': make_ctx()},
{'label': '停车费·本年累计', 'value': parking_year_cum, 'is_negative': False,
'action': 'yuthon_report.action_parking_fee_yearly_report', 'action_context': make_ctx()},
],
'row_groups': [
['违约金·累计欠收', '违约金·本月实收', '违约金·本年累计'],
['停车费·本月应收', '停车费·本月实收', '停车费·本年累计'],
],
},
# 三、押金
{
'id': 'deposit',
'title': '三、押金',
'cards': [
{'label': '本月应收', 'value': deposit_month_receivable, 'is_negative': False,
'action': 'yuthon_report.action_deposit_month_receivable_report', 'action_context': make_ctx()},
{'label': '前期实收', 'value': deposit_pre_received, 'is_negative': False,
'action': 'yuthon_report.action_deposit_pre_received_report', 'action_context': make_ctx()},
{'label': '本月实收', 'value': deposit_month_received, 'is_negative': False,
'action': 'yuthon_report.action_deposit_month_received_report', 'action_context': make_ctx()},
{'label': '累计欠收', 'value': deposit_cum_arrears, 'is_negative': True,
'action': 'yuthon_report.action_deposit_cum_arrears_report', 'action_context': make_ctx()},
{'label': '本月已退', 'value': deposit_month_refund, 'is_negative': False,
'action': 'yuthon_report.action_deposit_month_refund_report', 'action_context': make_ctx()},
{'label': '累计押金', 'value': deposit_total, 'is_negative': False,
'action': 'yuthon_report.action_deposit_total_report', 'action_context': make_ctx()},
],
'row_groups': [
['本月应收', '前期实收', '本月实收', '累计欠收', '本月已退', '累计押金'],
],
},
# 四、费用支出(cards由_get_fee_expense_data生成,需补充action_context)
{
'id': 'fee',
'title': '四、费用支出',
'cards': [dict(c, action_context=make_ctx()) for c in fee_data['cards']],
'row_groups': fee_data['row_groups'],
},
# 五、面积/比率
{
'id': 'area',
'title': '五、面积/比率',
'cards': [dict(c, action_context=make_ctx()) for c in area_data['cards']],
'row_groups': area_data['row_groups'],
},
]
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}"
# -------------------------------------------------------
# 私有辅助方法
# -------------------------------------------------------
def _get_company_display(self, company_id):
"""根据公司ID列表获取公司显示名(多选支持)
company_id 是 yuthon.property.company 的 ID 列表(多选)或单值
"""
if not company_id:
return '全部'
try:
# 多选:company_id 是列表,获取所有名称用逗号拼接
cid_list = _id_list(company_id)
if not cid_list:
return '全部'
companies = self.env['yuthon.property.company'].browse(cid_list)
names = [c.name for c in companies if c.name]
return ', '.join(names) if names else '未知'
except Exception:
return '未知'
def _get_admin_display(self, partner_id):
"""根据片区ID列表获取管理员显示名(多选支持)
partner_id 是 yuthon.precinct.message 的 ID 列表(多选)或单值
display_name 已自动拼接为 "{片区名称}{管理员姓名}",如 "二B梁嘉辉"
"""
if not partner_id:
return '全部'
try:
# 多选:partner_id 是列表,获取所有名称用逗号拼接
pid_list = _id_list(partner_id)
if not pid_list:
return '全部'
names = []
for pid in pid_list:
precinct = self.env['yuthon.precinct.message'].browse(pid)
if precinct:
names.append(precinct.display_name)
return '、'.join(names) if names else '未知'
except Exception:
return '未知'
def _get_fee_expense_data(self, base_params, query_date):
"""费用支出:管理费、维修费、评估费及其它"""
try:
month_first = query_date.replace(day=1)
last_day = calendar.monthrange(query_date.year, query_date.month)[1]
month_last = query_date.replace(day=last_day)
company_id = _id_list(base_params.get('company_id'))
# 多选标准化:partner_id 统一为列表
partner_id = _id_list(base_params.get('partner_id'))
keyword = base_params.get('keyword')
# 按费用类型分别统计(数据源:property.bills.list)
fee_types = ['管理费', '维修费', '评估费']
cards = []
row_labels = []
row_labels_explanations = []
for label in fee_types:
# 费用支出数据来源:property.bills.list
domain = [
('management_type_id.name', '=', label),
('actual_date', '>=', month_first),
('actual_date', '<=', month_last),
]
if company_id:
domain.append(('lessor_id', 'in', company_id))
if partner_id:
# 多选:片区管理员用 'in' 操作符
domain += ['|',
('contract_id.precinct_id', 'in', partner_id),
('receivables_id.precinct_id', 'in', partner_id),
]
if keyword:
domain += ['|', '|', '|',
('name', 'ilike', keyword),
('contract_id.contract_code', 'ilike', keyword),
('lessee_id.name', 'ilike', keyword),
('property_name', 'ilike', keyword)]
records = self.env['property.bills.list'].sudo().search(domain)
month_total = sum((r.amount_actual or 0.0) for r in records)
# 本年累计
year_start = datetime.date(query_date.year, 1, 1)
year_domain = [
('management_type_id.name', '=', label),
('actual_date', '>=', year_start),
('actual_date', '<=', month_last),
]
if company_id:
year_domain.append(('lessor_id', 'in', company_id))
if partner_id:
# 多选:片区管理员用 'in' 操作符
year_domain += ['|',
('contract_id.precinct_id', 'in', partner_id),
('receivables_id.precinct_id', 'in', partner_id),
]
year_records = self.env['property.bills.list'].sudo().search(year_domain)
year_total = sum((r.amount_actual or 0.0) for r in year_records)
# 本月支出卡片:管理费、维修费绑定报表,其他费用暂不绑定
if label == '管理费':
cards.append({
'label': f'{label}·本月支出', 'value': round(month_total, 2),
'is_negative': False,
'action': 'yuthon_report.action_management_fee_monthly_report',
'action_context': {},
})
elif label == '维修费':
cards.append({
'label': f'{label}·本月支出', 'value': round(month_total, 2),
'is_negative': False,
'action': 'yuthon_report.action_maintenance_fee_monthly_report',
'action_context': {},
})
elif label == '评估费':
cards.append({
'label': f'{label}·本月支出', 'value': round(month_total, 2),
'is_negative': False,
'action': 'yuthon_report.action_assessment_fee_monthly_report',
'action_context': {},
})
else:
cards.append({
'label': f'{label}·本月支出', 'value': round(month_total, 2),
'is_negative': False, 'action': '', 'action_context': {},
})
# 本年累计卡片:管理费、维修费绑定报表,其他费用暂不绑定
if label == '管理费':
cards.append({
'label': f'{label}·本年累计', 'value': round(year_total, 2),
'is_negative': False,
'action': 'yuthon_report.action_management_fee_yearly_report',
'action_context': {},
})
elif label == '维修费':
cards.append({
'label': f'{label}·本年累计', 'value': round(year_total, 2),
'is_negative': False,
'action': 'yuthon_report.action_maintenance_fee_yearly_report',
'action_context': {},
})
elif label == '评估费':
cards.append({
'label': f'{label}·本年累计', 'value': round(year_total, 2),
'is_negative': False,
'action': 'yuthon_report.action_assessment_fee_yearly_report',
'action_context': {},
})
else:
cards.append({
'label': f'{label}·本年累计', 'value': round(year_total, 2),
'is_negative': False, 'action': '', 'action_context': {},
})
row_labels.append(label)
# 说明信息
desc = f"{label}本月{month_total:.2f}元"
row_labels_explanations.append({'label': label, 'description': desc, 'amount': round(month_total, 2)})
row_groups = [
[f'{l}·本月支出', f'{l}·本年累计'] for l in row_labels
]
return {'cards': cards, 'row_groups': row_groups, 'explanations': row_labels_explanations}
except Exception:
return {'cards': [], 'row_groups': [], 'explanations': []}
def _get_area_data(self, base_params, query_date):
"""面积/比率板块
使用子报表方法获取汇总数据,确保看板卡片值与报表一致
已租面积拆为两个卡片:物业已租面积、合同已租面积
"""
try:
# 管理面积:调用管理面积报表(summary_only=True 跳过 rows 构建)
area_params = dict(base_params)
area_params['summary_only'] = True
management_result = self.env['management.area.report'].get_management_area_report(area_params)
management_stats = management_result.get('stats', [])
total_management = management_stats[1].get('amount', 0.0) if len(management_stats) > 1 else 0.0
# 可租面积:调用可租面积报表
rent_result = self.env['rent.area.report'].get_rent_area_report(area_params)
rent_stats = rent_result.get('stats', [])
total_rent = rent_stats[1].get('amount', 0.0) if len(rent_stats) > 1 else 0.0
# 已租面积:调用已租面积报表(返回物业已租面积和合同已租面积两个汇总)
leased_result = self.env['leased.area.report'].get_leased_area_report(area_params)
leased_stats = leased_result.get('stats', [])
# stats[1] = 物业已租面积合计, stats[2] = 合同已租面积合计
total_property_leased = leased_stats[1].get('amount', 0.0) if len(leased_stats) > 1 else 0.0
total_contract_leased = leased_stats[2].get('amount', 0.0) if len(leased_stats) > 2 else 0.0
# 出租率:物业已租面积 / 可租面积
rental_rate = round((total_property_leased / total_rent * 100) if total_rent > 0 else 0, 2)
cards = [
{'label': '管理面积(㎡)', 'value': round(total_management, 4), 'is_negative': False,
'action': 'yuthon_report.action_management_area_report', 'action_context': {}, 'is_area': True},
{'label': '可租面积(㎡)', 'value': round(total_rent, 4), 'is_negative': False,
'action': 'yuthon_report.action_rent_area_report', 'action_context': {}, 'is_area': True},
{'label': '物业已租面积(㎡)', 'value': round(total_property_leased, 4), 'is_negative': False,
'action': 'yuthon_report.action_leased_area_report', 'action_context': {}, 'is_area': True},
{'label': '合同已租面积(㎡)', 'value': round(total_contract_leased, 4), 'is_negative': False,
'action': 'yuthon_report.action_leased_area_report', 'action_context': {}, 'is_area': True},
{'label': '出租率', 'value': rental_rate, 'is_negative': False,
'action': '', 'action_context': {}, 'is_percent': True},
{'label': '收费率', 'value': 0.00, 'is_negative': False,
'action': '', 'action_context': {}, 'is_percent': True},
]
return {
'cards': cards,
'row_groups': [['管理面积(㎡)', '可租面积(㎡)', '物业已租面积(㎡)', '合同已租面积(㎡)', '出租率', '收费率']],
}
except Exception as e:
_logger.error("[DASHBOARD_AREA_ERROR] %s", e, exc_info=True)
return {'cards': [], 'row_groups': []}