Files
gzth/yuthon_report/models/leased_area_report.py
T
2026-06-17 18:01:58 +08:00

391 lines
19 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.
import calendar
import datetime
import logging
from odoo import api, models
from .report_utils import _id_list # 导入 ID 列表解析工具函数,支持多选参数
_logger = logging.getLogger(__name__)
class LeasedAreaReport(models.Model):
_name = 'leased.area.report'
_description = '已租面积报表'
_auto = False
@api.model
def get_leased_area_report(self, params):
"""已租面积报表数据源。
核心逻辑:
1. 数据源为 property.lease.contract(按合同维度分组)
2. 日期过滤:基于 actual_start_time 和 actual_end_time
- actual_start_time 为空 → 查不到(数据不完整)
- actual_start_time > 查询月末 → 查不到(合同还没开始)
- 1号起租:actual_end_time >= 查询月初 → 能查到
- 非1号起租:actual_end_time 往前推一个月 >= 查询月初 → 能查到
- actual_end_time 为空 → 查不到(数据不完整)
3. 物业已租面积:合同下所有物业的 property_area 之和(即 contract.total_area)
4. 合同已租面积:contract.total_area(合同总面积)
5. 实际开始时间、实际结束时间取自 contract.actual_start_time / actual_end_time
6. 片区管理员从第一个物业的 manage.information.line 取(按查询日期匹配)
7. 支持按物业名称(property_id)、承租方(lessee_id)、合同编号(contract_id)搜索
8. 按物业筛选时,只保留包含该物业的合同,但合同仍显示全部物业信息(物业名称、面积均累加全部物业)
9. 同一物业出现在多个合同时,面积只计算一次(归属到 start_time 最晚的合同)
"""
# ========== 一、参数解析 ==========
date_start = params.get("date_start")
date_end = params.get("date_end")
# 单日期模式,优先取 date_start
query_date_str = date_start or date_end
keyword = params.get("keyword")
condition = params.get('condition')
partner_id = _id_list(params.get('partner_id')) # 解析片区 ID 为整数列表(多选支持)
company_id = _id_list(params.get('company_id'))
# 前端侧边栏下拉选择器传入的 ID
property_id = params.get('property_id')
lessee_id = params.get('lessee_id')
contract_id = params.get('contract_id')
summary_only = params.get("summary_only", False)
# ========== 二、查询日期解析 ==========
query_date = None
if query_date_str:
try:
# 截断时间后缀
query_date = datetime.datetime.fromisoformat(query_date_str.split(' ')[0]).date()
except Exception:
_logger.warning("[LEASED_AREA] 日期解析失败: %s", query_date_str)
# ========== 三、查询日期范围计算 ==========
# 查询月初和月末,用于时间过滤
query_month_first = None
query_month_last = None
if query_date:
query_month_first = datetime.date(query_date.year, query_date.month, 1)
_, last_day = calendar.monthrange(query_date.year, query_date.month)
query_month_last = datetime.date(query_date.year, query_date.month, last_day)
# ========== 四、查询合同(按 actual_start_time / actual_end_time 过滤) ==========
# 不再使用 contact_state 字段,改用实际起止时间做时间过滤
contract_domain = []
# 承租方过滤
if lessee_id:
contract_domain.append(('lessee_id', '=', int(lessee_id)))
# 合同编号过滤
if contract_id:
contract_domain.append(('id', '=', int(contract_id)))
# 物业过滤:只保留包含该物业的合同,但不过滤合同行(合同仍显示全部物业)
if property_id:
prop_contract_ids = self.env['property.lease.contract.line'].sudo().search([
('address_id', '=', int(property_id))
]).mapped('lease_id.id')
contract_domain.append(('id', 'in', prop_contract_ids))
# 先查出基础符合条件的合同(不过滤时间,后面在循环中精确判断)
contracts = self.env['property.lease.contract'].sudo().search(contract_domain)
# ========== 四、查询合同下的所有物业明细行 ==========
# 用于:物业名称、计算物业已租面积
# 注意:不对 contract.line 做 property_id 过滤,合同仍显示全部物业信息
line_domain = [('lease_id', 'in', contracts.ids)] if contracts else [('id', '=', -1)]
contract_lines = self.env['property.lease.contract.line'].sudo().search(line_domain)
# 按合同分组:contract_id → [contract_lines]
lines_by_contract = {}
for cl in contract_lines:
cid = cl.lease_id.id
if cid not in lines_by_contract:
lines_by_contract[cid] = []
lines_by_contract[cid].append(cl)
# ========== 五、批量预取管理信息(片区管理员) ==========
property_ids = list({cl.address_id.id for cl in contract_lines if cl.address_id})
manage_map = {} # property_id → manage.information.line
if property_ids:
manage_domain = [('yuthon_property_id', 'in', property_ids)]
if query_date:
manage_domain += [
('start_time', '<=', query_date),
'|', ('end_date', '>=', query_date), ('end_date', '=', False),
]
manage_lines = self.env['manage.information.line'].sudo().search(manage_domain)
for ml in manage_lines:
if ml.yuthon_property_id:
manage_map[ml.yuthon_property_id.id] = ml
# ========== 六、第一轮遍历:过滤 + 收集有效合同 ==========
# 先把所有过滤条件跑一遍,收集最终出现在报表中的合同列表
# 再基于过滤后的合同做面积归属预扫描,避免被过滤掉的合同抢走归属权
filtered_contracts = [] # [(contract, lines, first_cl, first_property, manage_line)]
for contract in contracts:
lines = lines_by_contract.get(contract.id, [])
if not lines:
continue
# --- 时间过滤:基于 actual_start_time 和 actual_end_time ---
if query_date:
# 实际开始时间为空,查不到
if not contract.actual_start_time:
continue
# 条件1:实际开始时间在查询月之后就查不到
if contract.actual_start_time > query_month_last:
continue
# 实际结束时间为空,查不到
if not contract.actual_end_time:
continue
# 条件2:结束时间判断
if contract.actual_start_time.day == 1:
# 1号起租:actual_end_time >= 查询月初就能查到
if contract.actual_end_time < query_month_first:
continue
else:
# 非1号起租:actual_end_time 往前推一个月 >= 查询月初
end_year = contract.actual_end_time.year
end_month = contract.actual_end_time.month
end_day = contract.actual_end_time.day
# 前一个月
if end_month == 1:
pushed_year = end_year - 1
pushed_month = 12
else:
pushed_year = end_year
pushed_month = end_month - 1
# 处理日期(如前一个月没有这么多天,取最后一天)
_, pushed_last_day = calendar.monthrange(pushed_year, pushed_month)
pushed_day = min(end_day, pushed_last_day)
pushed_end = datetime.date(pushed_year, pushed_month, pushed_day)
if pushed_end < query_month_first:
continue
# --- 合同信息(提前取,后面关键字过滤需要) ---
contract_code = contract.contract_code or ""
lessee_name = contract.lessee_id.name if contract.lessee_id else ""
# --- 关键字过滤(物业名称、承租方、合同编号) ---
if keyword:
match = False
kw = keyword.lower()
# 合同编号
if contract_code and kw in contract_code.lower():
match = True
# 承租方
if lessee_name and kw in lessee_name.lower():
match = True
# 物业名称(遍历该合同下所有物业)
if not match:
for cl in lines:
prop_name = cl.address_id.name or ""
if prop_name and kw in prop_name.lower():
match = True
break
if not match:
continue
# --- 权属类型过滤 ---
if condition:
matched = False
for cl in lines:
ml = manage_map.get(cl.address_id.id) if cl.address_id else None
if ml and ml.tenure_type == condition:
matched = True
break
if not matched:
continue
# --- 所属片区/公司过滤(通过管理信息) ---
if partner_id:
matched = False
for cl in lines:
ml = manage_map.get(cl.address_id.id) if cl.address_id else None
if ml and ml.precinct_id and ml.precinct_id.id in partner_id: # 片区匹配(多选支持)
matched = True
break
if not matched:
continue
if company_id:
matched = False
for cl in lines:
ml = manage_map.get(cl.address_id.id) if cl.address_id else None
if ml and ml.company_id and ml.company_id.id in company_id:
matched = True
break
if not matched:
continue
# 通过所有过滤,加入有效合同列表
first_cl = lines[0]
first_property = first_cl.address_id
manage_line = manage_map.get(first_property.id) if first_property else None
filtered_contracts.append((contract, lines, first_cl, first_property, manage_line))
# ========== 七、预扫描:确定每个物业id的面积计算归属 ==========
# 仅基于过滤后的有效合同做归属判断,避免被过滤掉的合同抢走归属权
# 规则:同一物业出现在多个有效合同时,归属到 actual_start_time 更晚的合同
pid_owner_contract = {} # pid → (contract_id, actual_start_time)
for contract, lines, _, _, _ in filtered_contracts:
if not contract.actual_start_time:
continue
for cl in lines:
if not cl.address_id:
continue
pid = cl.address_id.id
existing = pid_owner_contract.get(pid)
if existing is None:
# 首次见到该物业,记录当前合同为候选
pid_owner_contract[pid] = (contract.id, contract.actual_start_time)
elif contract.actual_start_time > existing[1]:
# 当前合同的 start_time 更晚,更新归属到当前合同
pid_owner_contract[pid] = (contract.id, contract.actual_start_time)
# ========== 八、第二轮遍历:构建报表行 + 计算合计 ==========
rows = []
row_count = 0
total_property_area = 0.0 # 物业已租面积合计
total_contract_area = 0.0 # 合同已租面积合计
counted_pids = set() # 已参与合计计算的物业id
for idx, (contract, lines, first_cl, first_property, manage_line) in enumerate(filtered_contracts, start=1):
# --- 8.1 合同信息 ---
contract_code = contract.contract_code or ""
contract_record_id = contract.id
lessee_name = contract.lessee_id.name if contract.lessee_id else ""
lessee_record_id = contract.lessee_id.id if contract.lessee_id else False
# --- 8.2 片区管理员(取第一个物业的片区管理员) ---
admin_display = ""
if manage_line and manage_line.precinct_id:
admin_display = manage_line.precinct_id.display_name or ""
# --- 8.3 物业已租面积:该合同下所有物业 property_area 之和 ---
property_area_sum = sum((cl.property_area or 0.0) for cl in lines)
# 合同已租面积
contract_area = contract.total_area or 0.0
# 如果 property_area_sum 和 contract.total_area 差异较大,用 property_area_sum
# 否则以 contract.total_area 为准
property_area = property_area_sum if property_area_sum > 0 else contract_area
# --- 8.4 合计数去重:同一物业id只计算一次,归属到 start_time 最晚的合同 ---
# 规则:物业id只出现一次→正常计算;出现多次→只在start_time最晚的合同中计算
# 解析该合同下所有物业id及对应面积
property_ids_in_row = [cl.address_id.id for cl in lines if cl.address_id]
pid_area_map = {cl.address_id.id: cl.property_area or 0.0 for cl in lines if cl.address_id}
# 找出归属于当前合同的物业(pid_owner_contract 中记录的 owner == 当前合同)
owner_pids = [
pid for pid in property_ids_in_row
if pid_owner_contract.get(pid, (None, None))[0] == contract.id
]
# 其中尚未参与过计算的
new_owner_pids = [pid for pid in owner_pids if pid not in counted_pids]
# 不归属当前合同的物业(属于start_time更晚的其他合同,不参与计算但仍展示)
skip_pids = [pid for pid in property_ids_in_row if pid not in owner_pids]
# 调试日志
_logger.info(
"[LEASED_AREA_DEBUG] 合同=%s(id=%s), start=%s, 所有物业id=%s, "
"归属本合同id=%s, 新计算id=%s, 跳过id(归属更晚合同)=%s, "
"property_area=%.4f, contract_area=%.4f",
contract_code, contract.id, contract.actual_start_time,
property_ids_in_row, owner_pids, new_owner_pids, skip_pids,
property_area, contract_area,
)
if new_owner_pids and property_area > 0:
# 直接按归属物业的 property_area 逐个累加
add_prop_area = sum(pid_area_map.get(pid, 0.0) for pid in new_owner_pids)
total_property_area += add_prop_area
# 合同已租面积按归属物业面积占该行总物业面积的比例分配
if property_area_sum > 0:
ratio = add_prop_area / property_area_sum
else:
ratio = 1.0
total_contract_area += contract_area * ratio
_logger.info(
"[LEASED_AREA_DEBUG] → 计入合计: add_prop_area=%.4f, ratio=%.4f, "
"累计 property=%.4f, contract=%.4f",
add_prop_area, ratio,
total_property_area, total_contract_area,
)
else:
_logger.info("[LEASED_AREA_DEBUG] → 未计入合计(无新归属物业)")
# 只标记实际参与计算的物业为已处理
for pid in new_owner_pids:
counted_pids.add(pid)
# --- 8.5 物业名称和物业ID:列出该合同下所有物业 ---
property_names = []
property_ids_strs = []
for cl in lines:
if cl.address_id:
property_names.append(cl.address_id.name or "")
property_ids_strs.append(str(cl.address_id.id))
display_property_name = "、".join(property_names) if property_names else ""
display_property_ids = "、".join(property_ids_strs) if property_ids_strs else ""
# 物业名称:只有一条物业时支持穿透,多条时纯文本
if len(lines) == 1 and first_property:
property_name_value = {
'model': 'yuthon.property',
'id': first_property.id,
'display_name': display_property_name,
}
else:
property_name_value = display_property_name
# --- 8.6 构建行数据(物业名称、合同编号、承租方支持穿透) ---
# 实际起止时间格式化
actual_start = contract.actual_start_time
if actual_start:
actual_start_str = actual_start.strftime("%Y-%m-%d") if hasattr(actual_start, 'strftime') else str(actual_start)
else:
actual_start_str = ""
actual_end = contract.actual_end_time
if actual_end:
actual_end_str = actual_end.strftime("%Y-%m-%d") if hasattr(actual_end, 'strftime') else str(actual_end)
else:
actual_end_str = ""
if not summary_only:
rows.append({
"序号": idx,
"物业id": display_property_ids,
"物业名称": property_name_value,
"合同编号": {
'model': 'property.lease.contract',
'id': contract_record_id,
'display_name': contract_code,
} if contract_record_id else contract_code or "",
"承租方": {
'model': 'property.tenant.information',
'id': lessee_record_id,
'display_name': lessee_name,
} if lessee_record_id else lessee_name or "",
"实际开始时间": actual_start_str,
"实际结束日期": actual_end_str,
"物业已租面积": round(property_area, 4),
"合同已租面积": round(contract_area, 4),
"片区管理员": admin_display,
})
row_count += 1
# ========== 九、构建统计汇总数据 ==========
stats = [
{'name': '记录总数', 'amount': row_count},
{'name': '物业已租面积合计', 'amount': round(total_property_area, 4), 'display_amount': f"{round(total_property_area, 4):.4f}"},
{'name': '合同已租面积合计', 'amount': round(total_contract_area, 4), 'display_amount': f"{round(total_contract_area, 4):.4f}"},
]
_logger.info("[LEASED_AREA] query_date=%s, rows=%d, property_area=%.4f, contract_area=%.4f",
query_date, len(rows), total_property_area, total_contract_area)
return {"rows": rows, "stats": stats}