391 lines
19 KiB
Python
391 lines
19 KiB
Python
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}
|