diff --git a/yuthon_report/models/leased_area_report.py b/yuthon_report/models/leased_area_report.py index 9f56bbb97..2159a66ab 100644 --- a/yuthon_report/models/leased_area_report.py +++ b/yuthon_report/models/leased_area_report.py @@ -30,6 +30,7 @@ class LeasedAreaReport(models.Model): 6. 片区管理员从第一个物业的 manage.information.line 取(按查询日期匹配) 7. 支持按物业名称(property_id)、承租方(lessee_id)、合同编号(contract_id)搜索 8. 按物业筛选时,只保留包含该物业的合同,但合同仍显示全部物业信息(物业名称、面积均累加全部物业) + 9. 同一物业出现在多个合同时,面积只计算一次(归属到 start_time 最晚的合同) """ # ========== 一、参数解析 ========== @@ -115,37 +116,27 @@ class LeasedAreaReport(models.Model): if ml.yuthon_property_id: manage_map[ml.yuthon_property_id.id] = ml - # ========== 六、构建报表行(按合同分组,一个合同一行) ========== - rows = [] - total_property_area = 0.0 # 物业已租面积合计(按物业id去重) - total_contract_area = 0.0 # 合同已租面积合计(按物业id去重) - seen_property_ids = set() # 已计入合计的物业id集合 + # ========== 六、第一轮遍历:过滤 + 收集有效合同 ========== + # 先把所有过滤条件跑一遍,收集最终出现在报表中的合同列表 + # 再基于过滤后的合同做面积归属预扫描,避免被过滤掉的合同抢走归属权 + filtered_contracts = [] # [(contract, lines, first_cl, first_property, manage_line)] - for idx, contract in enumerate(contracts, start=1): + for contract in contracts: lines = lines_by_contract.get(contract.id, []) if not lines: continue # --- 时间过滤:基于 actual_start_time 和 actual_end_time --- - # 规则: - # 1. actual_start_time 为空 → 查不到(数据不完整) - # 2. actual_start_time > 查询月末 → 合同在查询月之后才启动,查不到 - # 3. 1号起租:actual_end_time >= 查询月初 → 能查到 - # 4. 非1号起租:actual_end_time 往前推一个月 >= 查询月初 → 能查到 - # 5. 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 >= 查询月初就能查到 @@ -156,7 +147,6 @@ class LeasedAreaReport(models.Model): 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 @@ -164,22 +154,18 @@ class LeasedAreaReport(models.Model): 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 - # --- 6.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 - # --- 6.2 关键字过滤(物业名称、承租方、合同编号) --- + # --- 关键字过滤(物业名称、承租方、合同编号) --- if keyword: match = False kw = keyword.lower() @@ -199,13 +185,8 @@ class LeasedAreaReport(models.Model): if not match: continue - # --- 6.3 权属类型过滤 --- - # 取第一个物业的管理信息做判断 - first_cl = lines[0] - first_property = first_cl.address_id - manage_line = manage_map.get(first_property.id) if first_property else None + # --- 权属类型过滤 --- if condition: - # 遍历该合同下所有物业,只要有一个匹配就保留 matched = False for cl in lines: ml = manage_map.get(cl.address_id.id) if cl.address_id else None @@ -215,7 +196,7 @@ class LeasedAreaReport(models.Model): if not matched: continue - # --- 6.4 所属片区/公司过滤(通过管理信息) --- + # --- 所属片区/公司过滤(通过管理信息) --- if partner_id: matched = False for cl in lines: @@ -235,32 +216,109 @@ class LeasedAreaReport(models.Model): if not matched: continue - # --- 6.5 片区管理员(取第一个物业的片区管理员) --- + # 通过所有过滤,加入有效合同列表 + 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 = [] + 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 "" - # --- 6.6 物业已租面积:该合同下所有物业 property_area 之和 --- - # 即 contract.total_area,但逐个计算更可靠 + # --- 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 - # --- 合计数去重:按物业id去重,同一物业id只参与一次合计 --- - # 解析该合同下所有物业id + # --- 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] - has_new = any(pid not in seen_property_ids for pid in property_ids_in_row) - if has_new: - total_property_area += property_area - total_contract_area += contract_area - for pid in property_ids_in_row: - seen_property_ids.add(pid) + 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] - # --- 6.7 物业名称和物业ID:列出该合同下所有物业 --- + # 调试日志 + _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: @@ -280,7 +338,7 @@ class LeasedAreaReport(models.Model): else: property_name_value = display_property_name - # --- 6.8 构建行数据(物业名称、合同编号、承租方支持穿透) --- + # --- 8.6 构建行数据(物业名称、合同编号、承租方支持穿透) --- # 实际起止时间格式化 actual_start = contract.actual_start_time if actual_start: @@ -314,7 +372,7 @@ class LeasedAreaReport(models.Model): "片区管理员": admin_display, }) - # ========== 七、构建统计汇总数据 ========== + # ========== 九、构建统计汇总数据 ========== stats = [ {'name': '记录总数', 'amount': len(rows)}, {'name': '物业已租面积合计', 'amount': round(total_property_area, 4), 'display_amount': f"{round(total_property_area, 4):.4f}"},