diff --git a/yuthon_report/models/penalty_month_received_report.py b/yuthon_report/models/penalty_month_received_report.py index f034df2dd..3d94fa291 100644 --- a/yuthon_report/models/penalty_month_received_report.py +++ b/yuthon_report/models/penalty_month_received_report.py @@ -1,5 +1,6 @@ -import datetime # 日期时间处理模块,用于日期解析和计算 +import datetime # 日期时间处理模块,用于日期解析、计算、格式化 import logging # 日志模块,用于调试输出 +import re # 正则表达式模块,用于解析收费区间字符串 from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类 @@ -7,7 +8,7 @@ _logger = logging.getLogger(__name__) # 获取当前模块的日志记录器 class PenaltyMonthReceivedReport(models.Model): - _inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型 + _inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法 @api.model def get_penalty_month_received_report(self, params): @@ -15,12 +16,14 @@ class PenaltyMonthReceivedReport(models.Model): 违约金本月实收报表 两路数据源: - 来源A:property.accounts.receivable 中 is_batch=True 的违约金记录 + 来源A:property.accounts.receivable 中收款类型为违约金 且 is_batch=True 的记录 通过 source_receivable_id 关联租金 - 本月实收:取该违约金记录的 receivable.modify.line,reconciliation_date 在查询月内的行 来源B:receivable.modify.line 中 is_penalty=True 的记录 - 通过 receivable_id 关联租金(receivable_id 指向的就是那笔租金应收记录) - 本月实收:reconciliation_date 在查询月内 + 通过其 receivable_id(违约金应收单)的 charging_range 反推租金的收款日期, + 在 receivable.modify.line 中找到匹配的租金记录(同承租方+同物业+同合同+同收款日期) + + 本月实收:来源A取 reconciliation_date 在查询月内的 modify.line; + 来源B取 is_penalty 且 reconciliation_date 在查询月内的 modify.line """ # ========== 一、参数解析 ========== @@ -72,6 +75,30 @@ class PenaltyMonthReceivedReport(models.Model): return False return True + # ========== 辅助函数:modify.line 内存过滤(来源B用)========== + def _match_filters_ml(ml, prop_name_str): + """对 receivable.modify.line 记录做内存过滤""" + if keyword: + kw = keyword.lower() + match = False + if ml.contract_id and ml.contract_id.contract_code and kw in ml.contract_id.contract_code.lower(): + match = True + if prop_name_str and kw in prop_name_str.lower(): + match = True + if ml.lessee_id and ml.lessee_id.name and kw in ml.lessee_id.name.lower(): + match = True + if not match: + return False + if partner_id and ml.precinct_id and ml.precinct_id.id != int(partner_id): + return False + if company_id and ml.company_id and ml.company_id.id != int(company_id): + return False + if lessee_id and (not ml.lessee_id or ml.lessee_id.id != int(lessee_id)): + return False + if contract_id and (not ml.contract_id or ml.contract_id.id != int(contract_id)): + return False + return True + # ========== 辅助函数:构建物业名称显示(穿透对象) ========== def _build_property_display(contract_rec, property_name_str): """构建物业名称穿透对象""" @@ -83,172 +110,276 @@ class PenaltyMonthReceivedReport(models.Model): } return property_name_str or "" - # ========== 辅助函数:构建行数据(共用) ========== - def _build_row(pay_amount, pay_date, pay_remark, rent_rec, penalty_rec, contract_rec, - prop_name, lessee_rec, company_name, source_tag): - """构建报表行字典""" - # 租金信息 - rent_amount = 0.0 - rent_collected = 0.0 - rent_uncollected = 0.0 - rent_status = "" - rent_due_date = "" - rent_payout_date = "" - if rent_rec: - rent_amount = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2) - rent_collected = round(rent_rec.delivered1 or 0.0, 2) - rent_uncollected = round(rent_rec.submitted or 0.0, 2) - rent_due_date = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else "" - rent_payout_date = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "" - if rent_rec.deposit_state == 'paid': - rent_status = "已收" - elif rent_rec.deposit_state == 'no_paid': - rent_status = "收部分" - else: - rent_status = "未收" + # ========== 辅助函数:解析收费区间字符串 ========== + def _parse_charging_range(range_str): + """ + 解析收费区间字符串,提取起止日期 - # 违约金信息 - penalty_amount = 0.0 - penalty_collected = 0.0 - penalty_uncollected = 0.0 - penalty_status = "" - penalty_start_date = "" - charging_range = "" - remark = "" - if penalty_rec: - penalty_amount = round(penalty_rec.original_amount or 0.0, 2) - penalty_collected = round(penalty_rec.delivered1 or 0.0, 2) - penalty_uncollected = round(penalty_rec.submitted or 0.0, 2) - penalty_start_date = penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "" - penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get( - penalty_rec.deposit_state, penalty_rec.deposit_state) - charging_range = penalty_rec.charging_range or "" - remark = penalty_rec.remark or "" - - return { - "来源": source_tag, - "租金ID": rent_rec.id if rent_rec else "", - "租金应收日": rent_due_date, - "租金收款日期": rent_payout_date, - "租金金额": rent_amount, - "租金已收": rent_collected, - "租金未收": rent_uncollected, - "租金状态": rent_status, - "违约金金额": penalty_amount, - "违约金已收": penalty_collected, - "违约金未收": penalty_uncollected, - "违约金状态": penalty_status, - "违约金应收开始": penalty_start_date, - "实收金额": round(pay_amount, 2), - "收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "", - "收款备注": pay_remark or "", - "收费区间": charging_range, - "备注": remark, - "合同编号": { - 'model': 'property.lease.contract', - 'id': contract_rec.id, - 'display_name': contract_rec.contract_code, - } if contract_rec else "", - "物业名称": _build_property_display(contract_rec, prop_name), - "承租方名称": { - 'model': 'property.tenant.information', - 'id': lessee_rec.id, - 'display_name': lessee_rec.name, - } if lessee_rec else "", - "所属公司": company_name or "", - } + 格式示例:"2025-10-01 到 2025-11-25共56" + 返回:(start_date, end_date) 或 (None, None) + """ + if not range_str: + return None, None + # 匹配 YYYY-MM-DD 到 YYYY-MM-DD 共NN 格式 + pattern = r'(\d{4}-\d{2}-\d{2})\s*到\s*(\d{4}-\d{2}-\d{2})' + match = re.search(pattern, range_str) + if match: + try: + start_d = datetime.datetime.strptime(match.group(1), '%Y-%m-%d').date() + end_d = datetime.datetime.strptime(match.group(2), '%Y-%m-%d').date() + return start_d, end_d + except ValueError: + pass + return None, None # ========== 初始化 ========== - rows = [] - total_collected = 0.0 + rows = [] # 报表行数据列表 + total_collected = 0.0 # 实收违约金合计 + processed_keys = set() # 去重集合 - # ========== 三、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ========== - # 查出 is_batch=True 的违约金记录 - penalty_a_recs = self.sudo().search([ + # ========== 二、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ========== + # 查出 is_batch=True 且收款类型为违约金的记录 + penalty_recs = self.sudo().search([ ('management_type_id.name', '=', '违约金'), # 收款类型为违约金 ('is_batch', '=', True), # 标记为批量修复的违约金数据 ('is_invalid', '!=', True), # 排除已作废 ]) # 查出这些违约金记录在查询月内的 modify.line - if penalty_a_recs: - modify_a_lines = self.env['receivable.modify.line'].sudo().search([ - ('receivable_id', 'in', penalty_a_recs.ids), # 关联到 is_batch 违约金记录 + if penalty_recs: + modify_lines = self.env['receivable.modify.line'].sudo().search([ + ('receivable_id', 'in', penalty_recs.ids), # 关联到 is_batch 违约金记录 ('reconciliation_date', '!=', False), # 有对账日期 ('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始 ('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束 ]) - for ml in modify_a_lines: - pay_amount = ml.amount or 0.0 + for ml in modify_lines: + pay_amount = ml.amount or 0.0 # 实收金额 if pay_amount <= 0: continue penalty_rec = ml.receivable_id # 这就是 is_batch 违约金记录 rent_rec = penalty_rec.source_receivable_id # 通过 source_receivable_id 关联租金 + # 物业名称 prop_name = penalty_rec.property_name or (rent_rec.property_name if rent_rec else "") # 内存过滤 if not _match_filters(penalty_rec, prop_name): continue - total_collected += pay_amount - rows.append(_build_row( - pay_amount=pay_amount, - pay_date=ml.reconciliation_date, - pay_remark=ml.remake1, - rent_rec=rent_rec, - penalty_rec=penalty_rec, - contract_rec=penalty_rec.contract_id, - prop_name=prop_name, - lessee_rec=penalty_rec.lessee_id, - company_name=penalty_rec.company_id.name if penalty_rec.company_id else "", - source_tag="A", - )) + # 去重 key + key = ('A', ml.id) + if key in processed_keys: + continue + processed_keys.add(key) - # ========== 四、来源B:receivable.modify.line 中 is_penalty=True 的记录 ========== - # 查询月内 is_penalty=True 的付款行 - modify_b_lines = self.env['receivable.modify.line'].sudo().search([ + # --- 构建租金信息 --- + rent_amount_val = 0.0 + rent_collected_amt = 0.0 + rent_uncollected_amt = 0.0 + rent_status = "" + rent_due_date_str = "" + rent_payout_date_str = "" + if rent_rec: + rent_amount_val = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2) + rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2) + rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2) + rent_due_date_str = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else "" + rent_payout_date_str = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "" + if rent_rec.deposit_state == 'paid': + rent_status = "已收" + elif rent_rec.deposit_state == 'no_paid': + rent_status = "收部分" + else: + rent_status = "未收" + + # --- 构建违约金信息 --- + penalty_amount = round(penalty_rec.original_amount or 0.0, 2) + penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2) + penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2) + penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get( + penalty_rec.deposit_state, penalty_rec.deposit_state) + + # 累计统计 + total_collected += pay_amount + + contract = penalty_rec.contract_id + rows.append({ + "来源": "A", + "租金ID": rent_rec.id if rent_rec else "", + "租金应收日": rent_due_date_str, + "租金收款日期": rent_payout_date_str, + "租金金额": rent_amount_val, + "租金已收": rent_collected_amt, + "租金未收": rent_uncollected_amt, + "租金状态": rent_status, + "违约金金额": penalty_amount, + "违约金已收": penalty_collected_amt, + "违约金未收": penalty_uncollected_amt, + "违约金状态": penalty_status, + "违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "", + "实收金额": round(pay_amount, 2), + "收款日期": ml.reconciliation_date.strftime("%Y-%m-%d") if ml.reconciliation_date else "", + "收款备注": ml.remake1 or "", + "收费区间": penalty_rec.charging_range or "", + "备注": penalty_rec.remark or "", + "合同编号": { + 'model': 'property.lease.contract', + 'id': contract.id, + 'display_name': contract.contract_code, + } if contract else "", + "物业名称": _build_property_display(contract, prop_name), + "承租方名称": { + 'model': 'property.tenant.information', + 'id': penalty_rec.lessee_id.id, + 'display_name': penalty_rec.lessee_id.name, + } if penalty_rec.lessee_id else "", + "所属公司": penalty_rec.company_id.name if penalty_rec.company_id else "", + }) + + # ========== 三、来源B:receivable.modify.line 中 is_penalty=True 的记录 ========== + # 通过 charging_range 反查关联租金 + # 步骤:取 is_penalty=True 的 modify.line → 读 receivable_id 的 charging_range → + # 解析出最后付款日 → 在 modify.line 中找匹配的租金记录(同承租方+同物业+同合同+同收款日期) + + source_b_domain = [ ('is_penalty', '=', True), # 标记为违约金付款 - ('receivable_id', '!=', False), # 必须关联应收记录 ('reconciliation_date', '!=', False), # 有对账日期 ('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始 ('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束 - ]) + ] + penalty_modify_lines = self.env['receivable.modify.line'].sudo().search(source_b_domain) - for ml in modify_b_lines: - pay_amount = ml.amount or 0.0 + for pml in penalty_modify_lines: + # 取违约金金额(delivered1 字段) + pay_amount = round(pml.delivered1 or 0.0, 2) if pay_amount <= 0: continue - rent_rec = ml.receivable_id # receivable_id 就是关联的那笔租金 - prop_name = rent_rec.property_name or "" - - # 内存过滤 - if not _match_filters(rent_rec, prop_name): + # 获取违约金对应的应收单(property.accounts.receivable) + penalty_rec = pml.receivable_id + if not penalty_rec: continue - total_collected += pay_amount - rows.append(_build_row( - pay_amount=pay_amount, - pay_date=ml.reconciliation_date, - pay_remark=ml.remake1, - rent_rec=rent_rec, - penalty_rec=None, # 来源B没有单独的违约金记录 - contract_rec=rent_rec.contract_id, - prop_name=prop_name, - lessee_rec=rent_rec.lessee_id, - company_name=rent_rec.company_id.name if rent_rec.company_id else "", - source_tag="B", - )) + # --- 读取 charging_range 并解析日期 --- + charging_range = penalty_rec.charging_range or "" + range_start, range_end = _parse_charging_range(charging_range) + if not range_end: + # 无法从收费区间解析出日期,跳过这条 + continue - # ========== 五、构建统计汇总数据 ========== + # --- 通过 charging_range 的结束日期反查租金 modify.line --- + # 匹配条件:同一承租方 + 同一合同 + 收款日期=range_end + 非违约金记录 + rent_ml_domain = [ + ('id', '!=', pml.id), # 排除自身 + ('is_penalty', '=', False), # 非违约金(即租金) + ('reconciliation_date', '=', range_end), # 收款日期 = 收费区间结束日期 + ] + if pml.lessee_id: + rent_ml_domain.append(('lessee_id', '=', pml.lessee_id.id)) # 同一承租方 + if pml.contract_id: + rent_ml_domain.append(('contract_id', '=', pml.contract_id.id)) # 同一合同 + + rent_mls = self.env['receivable.modify.line'].sudo().search(rent_ml_domain, limit=1) + if not rent_mls: + continue + rent_ml = rent_mls[0] + + # 获取租金对应的应收单 + rent_receivable = rent_ml.receivable_id + + # 物业名称(优先从违约金应收单取) + prop_name = penalty_rec.property_name or (rent_receivable.property_name if rent_receivable else "") + + # 内存过滤(用违约金应收单的信息过滤) + if not _match_filters_ml(pml, prop_name): + continue + + # 去重 key + key = ('B', pml.id) + if key in processed_keys: + continue + processed_keys.add(key) + + # --- 构建租金信息(从租金 modify.line 和对应应收单获取)--- + rent_amount_val = 0.0 + rent_collected_amt = 0.0 + rent_uncollected_amt = 0.0 + rent_status = "" + rent_due_date_str = "" + rent_payout_date_str = "" + + if rent_receivable: + rent_amount_val = round( + (rent_receivable.original_amount or 0.0) - (rent_receivable.discount_amount or 0.0), 2 + ) + rent_collected_amt = round(rent_receivable.delivered1 or 0.0, 2) + rent_uncollected_amt = round(rent_receivable.submitted or 0.0, 2) + rent_due_date_str = rent_receivable.due_date.strftime("%Y-%m-%d") if rent_receivable.due_date else "" + rent_payout_date_str = rent_receivable.payout_date.strftime("%Y-%m-%d") if rent_receivable.payout_date else "" + if rent_receivable.deposit_state == 'paid': + rent_status = "已收" + elif rent_receivable.deposit_state == 'no_paid': + rent_status = "收部分" + else: + rent_status = "未收" + + # --- 构建违约金信息 --- + penalty_amount = round(penalty_rec.original_amount or 0.0, 2) + penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2) + penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2) + penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get( + penalty_rec.deposit_state, penalty_rec.deposit_state) + + # 累计统计 + total_collected += pay_amount + + contract = penalty_rec.contract_id or pml.contract_id + lessee_obj = penalty_rec.lessee_id or pml.lessee_id + company_obj = penalty_rec.company_id or pml.company_id + + rows.append({ + "来源": "B", + "租金ID": rent_receivable.id if rent_receivable else "", + "租金应收日": rent_due_date_str, + "租金收款日期": rent_payout_date_str, + "租金金额": rent_amount_val, + "租金已收": rent_collected_amt, + "租金未收": rent_uncollected_amt, + "租金状态": rent_status, + "违约金金额": penalty_amount, + "违约金已收": penalty_collected_amt, + "违约金未收": penalty_uncollected_amt, + "违约金状态": penalty_status, + "违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "", + "实收金额": pay_amount, + "收款日期": pml.reconciliation_date.strftime("%Y-%m-%d") if pml.reconciliation_date else "", + "收款备注": pml.remake1 or "", + "收费区间": charging_range, + "备注": penalty_rec.remark or "", + "合同编号": { + 'model': 'property.lease.contract', + 'id': contract.id, + 'display_name': contract.contract_code, + } if contract else "", + "物业名称": _build_property_display(contract, prop_name), + "承租方名称": { + 'model': 'property.tenant.information', + 'id': lessee_obj.id, + 'display_name': lessee_obj.name, + } if lessee_obj else "", + "所属公司": company_obj.name if company_obj else "", + }) + + # ========== 四、构建统计汇总数据 ========== stats = [ {'name': '记录总数', 'amount': len(rows)}, {'name': '实收违约金', 'amount': round(total_collected, 2)}, ] - _logger.info("[PENALTY_MONTH_RECEIVED] ds=%s, de=%s, rows=%d, total=%.2f", - ds, de, len(rows), total_collected) + _logger.info("[PENALTY_MONTH_RECEIVED] ds=%s, de=%s, source_a_rows=%d, source_b_rows=%d, total=%.2f", + ds, de, sum(1 for r in rows if r.get('来源') == 'A'), + sum(1 for r in rows if r.get('来源') == 'B'), total_collected) return {"rows": rows, "stats": stats} diff --git a/yuthon_report/models/penalty_year_cum_report.py b/yuthon_report/models/penalty_year_cum_report.py index 1daed0095..e172985fb 100644 --- a/yuthon_report/models/penalty_year_cum_report.py +++ b/yuthon_report/models/penalty_year_cum_report.py @@ -1,5 +1,6 @@ -import datetime # 日期时间处理模块,用于日期解析和计算 +import datetime # 日期时间处理模块,用于日期解析、计算、格式化 import logging # 日志模块,用于调试输出 +import re # 正则表达式模块,用于解析收费区间字符串 from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类 @@ -7,7 +8,7 @@ _logger = logging.getLogger(__name__) # 获取当前模块的日志记录器 class PenaltyYearCumReport(models.Model): - _inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型 + _inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法 @api.model def get_penalty_year_cum_report(self, params): @@ -15,12 +16,14 @@ class PenaltyYearCumReport(models.Model): 违约金本年累计报表 两路数据源: - 来源A:property.accounts.receivable 中 is_batch=True 的违约金记录 + 来源A:property.accounts.receivable 中收款类型为违约金 且 is_batch=True 的记录 通过 source_receivable_id 关联租金 - 本年累计:取该违约金记录的 receivable.modify.line,reconciliation_date 在查询年内的行 来源B:receivable.modify.line 中 is_penalty=True 的记录 - 通过 receivable_id 关联租金(receivable_id 指向的就是那笔租金应收记录) - 本年累计:reconciliation_date 在查询年内 + 通过其 receivable_id(违约金应收单)的 charging_range 反推租金的收款日期, + 在 receivable.modify.line 中找到匹配的租金记录(同承租方+同物业+同合同+同收款日期) + + 本年累计:来源A取 reconciliation_date 在查询年内的 modify.line; + 来源B取 is_penalty 且 reconciliation_date 在查询年内的 modify.line """ # ========== 一、参数解析 ========== @@ -74,6 +77,30 @@ class PenaltyYearCumReport(models.Model): return False return True + # ========== 辅助函数:modify.line 内存过滤(来源B用)========== + def _match_filters_ml(ml, prop_name_str): + """对 receivable.modify.line 记录做内存过滤""" + if keyword: + kw = keyword.lower() + match = False + if ml.contract_id and ml.contract_id.contract_code and kw in ml.contract_id.contract_code.lower(): + match = True + if prop_name_str and kw in prop_name_str.lower(): + match = True + if ml.lessee_id and ml.lessee_id.name and kw in ml.lessee_id.name.lower(): + match = True + if not match: + return False + if partner_id and ml.precinct_id and ml.precinct_id.id != int(partner_id): + return False + if company_id and ml.company_id and ml.company_id.id != int(company_id): + return False + if lessee_id and (not ml.lessee_id or ml.lessee_id.id != int(lessee_id)): + return False + if contract_id and (not ml.contract_id or ml.contract_id.id != int(contract_id)): + return False + return True + # ========== 辅助函数:构建物业名称显示(穿透对象) ========== def _build_property_display(contract_rec, property_name_str): """构建物业名称穿透对象""" @@ -85,172 +112,273 @@ class PenaltyYearCumReport(models.Model): } return property_name_str or "" - # ========== 辅助函数:构建行数据(共用) ========== - def _build_row(pay_amount, pay_date, pay_remark, rent_rec, penalty_rec, contract_rec, - prop_name, lessee_rec, company_name, source_tag): - """构建报表行字典""" - # 租金信息 - rent_amount = 0.0 - rent_collected = 0.0 - rent_uncollected = 0.0 - rent_status = "" - rent_due_date = "" - rent_payout_date = "" - if rent_rec: - rent_amount = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2) - rent_collected = round(rent_rec.delivered1 or 0.0, 2) - rent_uncollected = round(rent_rec.submitted or 0.0, 2) - rent_due_date = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else "" - rent_payout_date = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "" - if rent_rec.deposit_state == 'paid': - rent_status = "已收" - elif rent_rec.deposit_state == 'no_paid': - rent_status = "收部分" - else: - rent_status = "未收" + # ========== 辅助函数:解析收费区间字符串 ========== + def _parse_charging_range(range_str): + """ + 解析收费区间字符串,提取起止日期 - # 违约金信息 - penalty_amount = 0.0 - penalty_collected = 0.0 - penalty_uncollected = 0.0 - penalty_status = "" - penalty_start_date = "" - charging_range = "" - remark = "" - if penalty_rec: - penalty_amount = round(penalty_rec.original_amount or 0.0, 2) - penalty_collected = round(penalty_rec.delivered1 or 0.0, 2) - penalty_uncollected = round(penalty_rec.submitted or 0.0, 2) - penalty_start_date = penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "" - penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get( - penalty_rec.deposit_state, penalty_rec.deposit_state) - charging_range = penalty_rec.charging_range or "" - remark = penalty_rec.remark or "" - - return { - "来源": source_tag, - "租金ID": rent_rec.id if rent_rec else "", - "租金应收日": rent_due_date, - "租金收款日期": rent_payout_date, - "租金金额": rent_amount, - "租金已收": rent_collected, - "租金未收": rent_uncollected, - "租金状态": rent_status, - "违约金金额": penalty_amount, - "违约金已收": penalty_collected, - "违约金未收": penalty_uncollected, - "违约金状态": penalty_status, - "违约金应收开始": penalty_start_date, - "实收金额": round(pay_amount, 2), - "收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "", - "收款备注": pay_remark or "", - "收费区间": charging_range, - "备注": remark, - "合同编号": { - 'model': 'property.lease.contract', - 'id': contract_rec.id, - 'display_name': contract_rec.contract_code, - } if contract_rec else "", - "物业名称": _build_property_display(contract_rec, prop_name), - "承租方名称": { - 'model': 'property.tenant.information', - 'id': lessee_rec.id, - 'display_name': lessee_rec.name, - } if lessee_rec else "", - "所属公司": company_name or "", - } + 格式示例:"2025-10-01 到 2025-11-25共56" + 返回:(start_date, end_date) 或 (None, None) + """ + if not range_str: + return None, None + # 匹配 YYYY-MM-DD 到 YYYY-MM-DD 共NN 格式 + pattern = r'(\d{4}-\d{2}-\d{2})\s*到\s*(\d{4}-\d{2}-\d{2})' + match = re.search(pattern, range_str) + if match: + try: + start_d = datetime.datetime.strptime(match.group(1), '%Y-%m-%d').date() + end_d = datetime.datetime.strptime(match.group(2), '%Y-%m-%d').date() + return start_d, end_d + except ValueError: + pass + return None, None # ========== 初始化 ========== - rows = [] - total_collected = 0.0 + rows = [] # 报表行数据列表 + total_collected = 0.0 # 实收违约金合计 + processed_keys = set() # 去重集合 - # ========== 三、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ========== - # 查出 is_batch=True 的违约金记录 - penalty_a_recs = self.sudo().search([ + # ========== 二、来源A:property.accounts.receivable 中 is_batch=True 的违约金 ========== + # 查出 is_batch=True 且收款类型为违约金的记录 + penalty_recs = self.sudo().search([ ('management_type_id.name', '=', '违约金'), # 收款类型为违约金 ('is_batch', '=', True), # 标记为批量修复的违约金数据 ('is_invalid', '!=', True), # 排除已作废 ]) # 查出这些违约金记录在查询年内的 modify.line - if penalty_a_recs: - modify_a_lines = self.env['receivable.modify.line'].sudo().search([ - ('receivable_id', 'in', penalty_a_recs.ids), # 关联到 is_batch 违约金记录 + if penalty_recs: + modify_lines = self.env['receivable.modify.line'].sudo().search([ + ('receivable_id', 'in', penalty_recs.ids), # 关联到 is_batch 违约金记录 ('reconciliation_date', '!=', False), # 有对账日期 ('reconciliation_date', '>=', ds), # 对账日期 >= 当年1月1日 ('reconciliation_date', '<=', de), # 对账日期 <= 当年12月31日 ]) - for ml in modify_a_lines: - pay_amount = ml.amount or 0.0 + for ml in modify_lines: + pay_amount = ml.amount or 0.0 # 实收金额 if pay_amount <= 0: continue penalty_rec = ml.receivable_id # 这就是 is_batch 违约金记录 rent_rec = penalty_rec.source_receivable_id # 通过 source_receivable_id 关联租金 + # 物业名称 prop_name = penalty_rec.property_name or (rent_rec.property_name if rent_rec else "") # 内存过滤 if not _match_filters(penalty_rec, prop_name): continue - total_collected += pay_amount - rows.append(_build_row( - pay_amount=pay_amount, - pay_date=ml.reconciliation_date, - pay_remark=ml.remake1, - rent_rec=rent_rec, - penalty_rec=penalty_rec, - contract_rec=penalty_rec.contract_id, - prop_name=prop_name, - lessee_rec=penalty_rec.lessee_id, - company_name=penalty_rec.company_id.name if penalty_rec.company_id else "", - source_tag="A", - )) + # 去重 key + key = ('A', ml.id) + if key in processed_keys: + continue + processed_keys.add(key) - # ========== 四、来源B:receivable.modify.line 中 is_penalty=True 的记录 ========== - # 查询年内 is_penalty=True 的付款行 - modify_b_lines = self.env['receivable.modify.line'].sudo().search([ + # --- 构建租金信息 --- + rent_amount_val = 0.0 + rent_collected_amt = 0.0 + rent_uncollected_amt = 0.0 + rent_status = "" + rent_due_date_str = "" + rent_payout_date_str = "" + if rent_rec: + rent_amount_val = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2) + rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2) + rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2) + rent_due_date_str = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else "" + rent_payout_date_str = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "" + if rent_rec.deposit_state == 'paid': + rent_status = "已收" + elif rent_rec.deposit_state == 'no_paid': + rent_status = "收部分" + else: + rent_status = "未收" + + # --- 构建违约金信息 --- + penalty_amount = round(penalty_rec.original_amount or 0.0, 2) + penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2) + penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2) + penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get( + penalty_rec.deposit_state, penalty_rec.deposit_state) + + # 累计统计 + total_collected += pay_amount + + contract = penalty_rec.contract_id + rows.append({ + "来源": "A", + "租金ID": rent_rec.id if rent_rec else "", + "租金应收日": rent_due_date_str, + "租金收款日期": rent_payout_date_str, + "租金金额": rent_amount_val, + "租金已收": rent_collected_amt, + "租金未收": rent_uncollected_amt, + "租金状态": rent_status, + "违约金金额": penalty_amount, + "违约金已收": penalty_collected_amt, + "违约金未收": penalty_uncollected_amt, + "违约金状态": penalty_status, + "违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "", + "实收金额": round(pay_amount, 2), + "收款日期": ml.reconciliation_date.strftime("%Y-%m-%d") if ml.reconciliation_date else "", + "收款备注": ml.remake1 or "", + "收费区间": penalty_rec.charging_range or "", + "备注": penalty_rec.remark or "", + "合同编号": { + 'model': 'property.lease.contract', + 'id': contract.id, + 'display_name': contract.contract_code, + } if contract else "", + "物业名称": _build_property_display(contract, prop_name), + "承租方名称": { + 'model': 'property.tenant.information', + 'id': penalty_rec.lessee_id.id, + 'display_name': penalty_rec.lessee_id.name, + } if penalty_rec.lessee_id else "", + "所属公司": penalty_rec.company_id.name if penalty_rec.company_id else "", + }) + + # ========== 三、来源B:receivable.modify.line 中 is_penalty=True 的记录 ========== + # 通过 charging_range 反查关联租金 + source_b_domain = [ ('is_penalty', '=', True), # 标记为违约金付款 - ('receivable_id', '!=', False), # 必须关联应收记录 ('reconciliation_date', '!=', False), # 有对账日期 ('reconciliation_date', '>=', ds), # 对账日期 >= 当年1月1日 ('reconciliation_date', '<=', de), # 对账日期 <= 当年12月31日 - ]) + ] + penalty_modify_lines = self.env['receivable.modify.line'].sudo().search(source_b_domain) - for ml in modify_b_lines: - pay_amount = ml.amount or 0.0 + for pml in penalty_modify_lines: + # 取违约金金额(delivered1 字段) + pay_amount = round(pml.delivered1 or 0.0, 2) if pay_amount <= 0: continue - rent_rec = ml.receivable_id # receivable_id 就是关联的那笔租金 - prop_name = rent_rec.property_name or "" - - # 内存过滤 - if not _match_filters(rent_rec, prop_name): + # 获取违约金对应的应收单(property.accounts.receivable) + penalty_rec = pml.receivable_id + if not penalty_rec: continue - total_collected += pay_amount - rows.append(_build_row( - pay_amount=pay_amount, - pay_date=ml.reconciliation_date, - pay_remark=ml.remake1, - rent_rec=rent_rec, - penalty_rec=None, # 来源B没有单独的违约金记录 - contract_rec=rent_rec.contract_id, - prop_name=prop_name, - lessee_rec=rent_rec.lessee_id, - company_name=rent_rec.company_id.name if rent_rec.company_id else "", - source_tag="B", - )) + # --- 读取 charging_range 并解析日期 --- + charging_range = penalty_rec.charging_range or "" + range_start, range_end = _parse_charging_range(charging_range) + if not range_end: + # 无法从收费区间解析出日期,跳过这条 + continue - # ========== 五、构建统计汇总数据 ========== + # --- 通过 charging_range 的结束日期反查租金 modify.line --- + # 匹配条件:同一承租方 + 同一合同 + 收款日期=range_end + 非违约金记录 + rent_ml_domain = [ + ('id', '!=', pml.id), # 排除自身 + ('is_penalty', '=', False), # 非违约金(即租金) + ('reconciliation_date', '=', range_end), # 收款日期 = 收费区间结束日期 + ] + if pml.lessee_id: + rent_ml_domain.append(('lessee_id', '=', pml.lessee_id.id)) # 同一承租方 + if pml.contract_id: + rent_ml_domain.append(('contract_id', '=', pml.contract_id.id)) # 同一合同 + + rent_mls = self.env['receivable.modify.line'].sudo().search(rent_ml_domain, limit=1) + if not rent_mls: + continue + rent_ml = rent_mls[0] + + # 获取租金对应的应收单 + rent_receivable = rent_ml.receivable_id + + # 物业名称(优先从违约金应收单取) + prop_name = penalty_rec.property_name or (rent_receivable.property_name if rent_receivable else "") + + # 内存过滤(用违约金 modify.line 的信息过滤) + if not _match_filters_ml(pml, prop_name): + continue + + # 去重 key + key = ('B', pml.id) + if key in processed_keys: + continue + processed_keys.add(key) + + # --- 构建租金信息(从租金 modify.line 和对应应收单获取)--- + rent_amount_val = 0.0 + rent_collected_amt = 0.0 + rent_uncollected_amt = 0.0 + rent_status = "" + rent_due_date_str = "" + rent_payout_date_str = "" + + if rent_receivable: + rent_amount_val = round( + (rent_receivable.original_amount or 0.0) - (rent_receivable.discount_amount or 0.0), 2 + ) + rent_collected_amt = round(rent_receivable.delivered1 or 0.0, 2) + rent_uncollected_amt = round(rent_receivable.submitted or 0.0, 2) + rent_due_date_str = rent_receivable.due_date.strftime("%Y-%m-%d") if rent_receivable.due_date else "" + rent_payout_date_str = rent_receivable.payout_date.strftime("%Y-%m-%d") if rent_receivable.payout_date else "" + if rent_receivable.deposit_state == 'paid': + rent_status = "已收" + elif rent_receivable.deposit_state == 'no_paid': + rent_status = "收部分" + else: + rent_status = "未收" + + # --- 构建违约金信息 --- + penalty_amount = round(penalty_rec.original_amount or 0.0, 2) + penalty_collected_amt = round(penalty_rec.delivered1 or 0.0, 2) + penalty_uncollected_amt = round(penalty_rec.submitted or 0.0, 2) + penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get( + penalty_rec.deposit_state, penalty_rec.deposit_state) + + # 累计统计 + total_collected += pay_amount + + contract = penalty_rec.contract_id or pml.contract_id + lessee_obj = penalty_rec.lessee_id or pml.lessee_id + company_obj = penalty_rec.company_id or pml.company_id + + rows.append({ + "来源": "B", + "租金ID": rent_receivable.id if rent_receivable else "", + "租金应收日": rent_due_date_str, + "租金收款日期": rent_payout_date_str, + "租金金额": rent_amount_val, + "租金已收": rent_collected_amt, + "租金未收": rent_uncollected_amt, + "租金状态": rent_status, + "违约金金额": penalty_amount, + "违约金已收": penalty_collected_amt, + "违约金未收": penalty_uncollected_amt, + "违约金状态": penalty_status, + "违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "", + "实收金额": pay_amount, + "收款日期": pml.reconciliation_date.strftime("%Y-%m-%d") if pml.reconciliation_date else "", + "收款备注": pml.remake1 or "", + "收费区间": charging_range, + "备注": penalty_rec.remark or "", + "合同编号": { + 'model': 'property.lease.contract', + 'id': contract.id, + 'display_name': contract.contract_code, + } if contract else "", + "物业名称": _build_property_display(contract, prop_name), + "承租方名称": { + 'model': 'property.tenant.information', + 'id': lessee_obj.id, + 'display_name': lessee_obj.name, + } if lessee_obj else "", + "所属公司": company_obj.name if company_obj else "", + }) + + # ========== 四、构建统计汇总数据 ========== stats = [ {'name': '记录总数', 'amount': len(rows)}, {'name': '实收违约金', 'amount': round(total_collected, 2)}, ] - _logger.info("[PENALTY_YEAR_CUM] year=%d, ds=%s, de=%s, rows=%d, total=%.2f", - year, ds, de, len(rows), total_collected) + _logger.info("[PENALTY_YEAR_CUM] year=%d, ds=%s, de=%s, source_a_rows=%d, source_b_rows=%d, total=%.2f", + year, ds, de, sum(1 for r in rows if r.get('来源') == 'A'), + sum(1 for r in rows if r.get('来源') == 'B'), total_collected) return {"rows": rows, "stats": stats}