From 0080d801fc899779e94b64cd970d6c607bbb99f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Wed, 25 Mar 2026 21:23:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/inherit_yuthon_property.py | 41 +++++++ .../wizard/property_reconciliation_wizard.py | 57 +++++---- .../models/receivable_flow_report.py | 112 ++++++------------ 3 files changed, 108 insertions(+), 102 deletions(-) diff --git a/yuthon_property_approval/models/inherit_yuthon_property.py b/yuthon_property_approval/models/inherit_yuthon_property.py index d5abc1739..4801c9a6e 100644 --- a/yuthon_property_approval/models/inherit_yuthon_property.py +++ b/yuthon_property_approval/models/inherit_yuthon_property.py @@ -5,6 +5,7 @@ from odoo import models, fields, api, exceptions, _ from odoo.exceptions import UserError, ValidationError from dateutil.relativedelta import relativedelta import calendar +import re class PropertyMeterWater(models.Model): @@ -621,6 +622,7 @@ class PropertyAccountsReceivable(models.Model): company_id2 = fields.Many2one("res.company", string="公司", compute='_compute_company_id2', store=True) contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批") rent_id = fields.Char(string='租金id') + source_receivable_id = fields.Many2one('property.accounts.receivable', string='原始应收', index=True, copy=False) contract_sort = fields.Selection([('property', '物业'), ('guanggao', '广告位'),('parking', '停车场'), ('guanzi', '管子间'), ('room', '机房'), ('base_station','基站'), ('other', '其他')], related='contract_id.contract_sort', string="资产类型", index=True) admin_id = fields.Many2one(related='contract_id.admin_id', string="片区管理员", store=True) @@ -882,6 +884,45 @@ class PropertyAccountsReceivable(models.Model): } } + def _resolve_source_receivable(self): + self.ensure_one() + empty_receivable = self.env['property.accounts.receivable'] + if self.source_receivable_id: + return self.source_receivable_id + if self.management_type_id.name != '违约金' or not self.contract_id: + return empty_receivable + + date_match = re.search(r'(\d{4}-\d{2}-\d{2})[至-](\d{4}-\d{2}-\d{2})', self.remark or '') + if not date_match: + return empty_receivable + + start_date = datetime.strptime(date_match.group(1), "%Y-%m-%d").date() + candidates = self.search([ + ('contract_id', '=', self.contract_id.id), + ('id', '!=', self.id), + ('is_invalid', '=', False), + ('management_type_id.name', 'not in', ['违约金', '押金']), + ('is_liquidated_damages', '=', True), + ], order='due_date asc, id asc') + + payout_matches = candidates.filtered( + lambda r: r.payout_date and r.payout_date + timedelta(days=1) == start_date + ) + if len(payout_matches) == 1: + return payout_matches[:1] + + cycle_matches = candidates.filtered( + lambda r: r.due_date and (r.due_date + relativedelta(months=1)).replace(day=1) == start_date + ) + if len(cycle_matches) == 1: + return cycle_matches[:1] + + if payout_matches: + return payout_matches[:1] + if cycle_matches: + return cycle_matches[:1] + return empty_receivable + @api.depends('contract_id') def _compute_property_name(self): for record in self: diff --git a/yuthon_property_approval/wizard/property_reconciliation_wizard.py b/yuthon_property_approval/wizard/property_reconciliation_wizard.py index 428943396..ce84fdde8 100644 --- a/yuthon_property_approval/wizard/property_reconciliation_wizard.py +++ b/yuthon_property_approval/wizard/property_reconciliation_wizard.py @@ -150,6 +150,7 @@ class PropertyReconciliationWizard(models.Model): ('management_type_id.name', '=', '违约金') ]) for penalty in receivable_penalties: + source_receivable = penalty.source_receivable_id or penalty._resolve_source_receivable() contract_penalties.append({ 'contract_id': penalty.contract_id.id, 'rent_month': 0, @@ -158,6 +159,7 @@ class PropertyReconciliationWizard(models.Model): 'amount': 0, 'collection_type': 'penalty', 'remake': penalty.remark, + 'source_receivable_id': source_receivable.id if source_receivable else False, }) contract_receivables_ids = self.env['property.accounts.receivable'].search([ ('contract_id', '=', contract_id.id.origin), @@ -194,6 +196,7 @@ class PropertyReconciliationWizard(models.Model): 'amount': 0, 'collection_type': 'penalty', 'remake': f'违约金({start_date_str}至{end_date_str},共{late_days}天)', + 'source_receivable_id': contract_receivables.id, }) # 查找应收是否有未交全的租金 @@ -225,6 +228,7 @@ class PropertyReconciliationWizard(models.Model): 'amount': 0, 'collection_type': 'penalty', 'remake': f'违约金({fields.Date.to_string(first_day)}至{fields.Date.to_string(self.actual_date)},共{penalty_days.days + 1}天)', + 'source_receivable_id': rent.id, })) # 3. 添加到总列表 @@ -404,8 +408,8 @@ class PropertyReconciliationWizard(models.Model): 'amount': line.amount, }) - management_type_id = self.env['yuthon.management.type'].search([('name', '=', '违约金')]) - if line.collection_type == 'penalty': + management_type_id = self.env['yuthon.management.type'].search([('name', '=', '违约金')], limit=1) + if line.collection_type == 'penalty' and management_type_id: penalty_date = line.due_date first_day_of_last_month = penalty_date.replace(day=1) - relativedelta(months=2) # 上个月最后一天:当前月第一天减1天 @@ -423,6 +427,11 @@ class PropertyReconciliationWizard(models.Model): date_match = re.search(date_pattern, line.remake or '') extract_start_date = date_match.group(1) if date_match else line.due_date extract_end_date = date_match.group(2) if date_match else None + if extract_start_date and isinstance(extract_start_date, str): + extract_start_date = datetime.strptime(extract_start_date, "%Y-%m-%d").date() + if extract_end_date and isinstance(extract_end_date, str): + extract_end_date = datetime.strptime(extract_end_date, "%Y-%m-%d").date() + check_domain = [ ('due_date', '=', line.due_date), ('contract_id', '=', line.contract_id.id), @@ -430,24 +439,19 @@ class PropertyReconciliationWizard(models.Model): ('is_invalid', '=', False), ('management_type_id', '=', management_type_id.id), ] - # 检查是否已有相同记录 existing_record = self.env['property.accounts.receivable'].search(check_domain) - is_reconciliation = round(line.amount,2) == round(line.rent, 2) + source_receivable = line.source_receivable_id + if not source_receivable and existing_record: + source_receivable = existing_record[:1].source_receivable_id or existing_record[:1]._resolve_source_receivable() + is_reconciliation = round(line.amount, 2) == round(line.rent, 2) receivable_id = self.env['property.accounts.receivable'] + if not existing_record and is_create: - domain2 = [ - ('due_date', '=', line.due_date), - ('contract_id', '=', line.contract_id.id), - ('collection_type', '=', 'rent'), - ] - extract_start_date = datetime.strptime(extract_start_date, "%Y-%m-%d").date() - extract_end_date = datetime.strptime(extract_end_date, "%Y-%m-%d").date() - reconciliation_id = self.env['property.reconciliation.line'].search(domain2, limit=1) receivable_id = self.env['property.accounts.receivable'].create({ 'is_reconciliation': is_reconciliation, - 'due_date': extract_end_date, + 'due_date': extract_end_date or line.due_date, 'due_date_end': None, - 'penalty': reconciliation_id.rent, + 'penalty': source_receivable.submitted if source_receivable else 0, 'management_type_id': management_type_id.id, 'contract_id': line.contract_id.id, 'delivered1': line.amount, @@ -455,13 +459,17 @@ class PropertyReconciliationWizard(models.Model): 'original_amount': line.rent, 'remark': line.remake, 'reconciliation_time': fields.Datetime.now(), - 'charging_range': f"{extract_start_date} 到 {extract_end_date}共{(extract_end_date - extract_start_date).days + 1}" + 'charging_range': f"{extract_start_date} 到 {extract_end_date}共{(extract_end_date - extract_start_date).days + 1}" if extract_start_date and extract_end_date else False, + 'source_receivable_id': source_receivable.id if source_receivable else False, }) else: - if line.amount: - existing_record.write({ - 'delivered1': existing_record.delivered1 + line.amount, - 'due_date': extract_end_date, + receivable_id = existing_record[:1] + if receivable_id and source_receivable and not receivable_id.source_receivable_id: + receivable_id.write({'source_receivable_id': source_receivable.id}) + if receivable_id and line.amount: + receivable_id.write({ + 'delivered1': receivable_id.delivered1 + line.amount, + 'due_date': extract_end_date or line.due_date, 'due_date_end': None, 'is_reconciliation': is_reconciliation, 'payout_date': rec.actual_date if (line.rent == line.amount and line.rent != 0) else None, @@ -470,9 +478,6 @@ class PropertyReconciliationWizard(models.Model): 'reconciliation_time': fields.Datetime.now(), }) - if existing_record: - receivable_id = existing_record - if receivable_id: self.env['receivable.modify.line'].create({ 'bank_id': rec.bank_id.id, @@ -480,13 +485,12 @@ class PropertyReconciliationWizard(models.Model): 'is_write': True if line.amount != 0 else False, 'is_penalty': True, 'reconciliation_date': rec.actual_date if line.amount != 0 else None, - 'receivable_id': receivable_id[0].id, + 'receivable_id': receivable_id.id, 'amount': line.amount, }) # 删除多余的违约金数据 - if len(receivable_id) > 1: - receivable_id = existing_record[1:] - receivable_id.unlink() + if len(existing_record) > 1: + existing_record[1:].unlink() rec.amount_been_reconciled += line.amount yes_amount += line.amount @@ -518,6 +522,7 @@ class PropertyReconciliationLine(models.Model): rent_month = fields.Float(string='当期金额') due_date = fields.Date(string='应收日期') rent = fields.Float(string='未收') + source_receivable_id = fields.Many2one('property.accounts.receivable', string='原始应收') amount = fields.Float(string='金额', store=True) collection_type = fields.Selection([('deposit', '押金'), ('rent', '租金'), ('water_fee', '水费'), ('lightbox', '灯箱'), ('guanzijian', '管子间'), ('electricity_fee', '电费'), ('penalty', '违约金'), ('management_fee', '管理费'), ('repair_fee', '维修费'), ('fee', '人工费/日光灯款/代垫用料费'), ('rename_fee', '更名费'), ('service', '有偿服务收入')], string='收款类型', store=True) diff --git a/yuthon_report/models/receivable_flow_report.py b/yuthon_report/models/receivable_flow_report.py index f6d97eead..59f37c2db 100644 --- a/yuthon_report/models/receivable_flow_report.py +++ b/yuthon_report/models/receivable_flow_report.py @@ -58,51 +58,43 @@ class ReceivableFlowReport(models.Model): rec_ids = receivables.ids # ===== 批量预加载,避免N+1查询 ===== - # 0. 先查询违约金类型记录 - penalty_type = self.env['yuthon.management.type'].search([('name', '=', '违约金')], limit=1) - - # 1. 批量查收款明细行(sudo跳过多公司权限) all_modify_lines = self.env['receivable.modify.line'].sudo().search([ ('receivable_id', 'in', rec_ids), ('reconciliation_date', '!=', False), ]) - # 按 receivable_id 分组 - modify_map = {} # {receivable_id: [lines]} + modify_map = {} for ml in all_modify_lines: modify_map.setdefault(ml.receivable_id.id, []).append(ml) - # 2. 直接查询所有 penalty.pay.line 记录 - # 先查询所有相关的 penalty.line.wizard - all_wizards = self.env['penalty.line.wizard'].sudo().search([ - ('receivable_id', 'in', rec_ids), + penalty_receivables = self.env['property.accounts.receivable'].sudo().search([ + ('contract_id', 'in', receivables.mapped('contract_id').ids), + ('is_invalid', '=', False), + ('management_type_id.name', '=', '违约金'), ]) - all_wizard_ids = all_wizards.ids - - # 然后查询所有相关的 penalty.pay.line - if all_wizard_ids: - all_penalty_pay_lines = self.env['penalty.pay.line'].sudo().search([ - ('penalty_id', 'in', all_wizard_ids), + penalty_source_map = {} + penalty_receivable_ids = [] + for penalty_rec in penalty_receivables: + source_receivable = penalty_rec.source_receivable_id or penalty_rec._resolve_source_receivable() + if source_receivable and source_receivable.id in rec_ids: + penalty_source_map[penalty_rec.id] = source_receivable.id + penalty_receivable_ids.append(penalty_rec.id) + + penalty_modify_map = {} + if penalty_receivable_ids: + penalty_modify_lines = self.env['receivable.modify.line'].sudo().search([ + ('receivable_id', 'in', penalty_receivable_ids), + ('reconciliation_date', '!=', False), ]) - else: - all_penalty_pay_lines = self.env['penalty.pay.line'] - - # 调试信息 - import logging - _logger = logging.getLogger(__name__) - _logger.info(f"查询的 rec_ids 数量: {len(rec_ids)}, 示例: {rec_ids[:5] if len(rec_ids) > 5 else rec_ids}") - _logger.info(f"查询到的 all_wizards 数量: {len(all_wizards)}") - _logger.info(f"查询到的 all_penalty_pay_lines 数量: {len(all_penalty_pay_lines)}") - - # 按 receivable_id 分组 {receivable_id: [penalty_pay_lines]} - penalty_pay_map = {} - for ppl in all_penalty_pay_lines: - rid = ppl.penalty_id.receivable_id.id if ppl.penalty_id and ppl.penalty_id.receivable_id else None - if rid: - penalty_pay_map.setdefault(rid, []).append(ppl) - - _logger.info(f"penalty_pay_map 中的 receivable_id 数量: {len(penalty_pay_map)}, 示例: {list(penalty_pay_map.keys())[:5] if len(penalty_pay_map) > 5 else list(penalty_pay_map.keys())}") + for ml in penalty_modify_lines: + source_id = penalty_source_map.get(ml.receivable_id.id) + if source_id and ( + ml.is_penalty + or (ml.receivable_id.management_type_id and ml.receivable_id.management_type_id.name == '违约金') + ): + penalty_modify_map.setdefault(source_id, []).append(ml) # ===== 预加载结束 ===== + rows = [] seq = 0 @@ -180,51 +172,19 @@ class ReceivableFlowReport(models.Model): if rec.submitted == 0: charge_status = "已收" - # 违约金收款:直接从 penalty.pay.line 计算 - pay_lines = penalty_pay_map.get(rec.id, []) - - # 调试信息 - import logging - _logger = logging.getLogger(__name__) - _logger.info(f"应收记录ID: {rec.id}, penalty_type: {penalty_type}, penalty_type.id: {penalty_type.id if penalty_type else None}") - _logger.info(f"pay_lines 数量: {len(pay_lines)}") - for ppl in pay_lines: - _logger.info(f" - penalty.pay.line ID: {ppl.id}, management_type_id: {ppl.management_type_id}, management_type_id.id: {ppl.management_type_id.id if ppl.management_type_id else None}, amount: {ppl.amount}, date: {ppl.date}") - - # 违约金收款:兼容不同公司/多条“违约金”管理类型,按名称兜底过滤 - penalty_pay_lines = [ - l for l in pay_lines - if ( - (penalty_type and l.management_type_id == penalty_type) - or (l.management_type_id and l.management_type_id.name == '违约金') + penalty_payment_lines = [] + if rec.management_type_id.name != '违约金': + penalty_payment_lines = sorted( + penalty_modify_map.get(rec.id, []), + key=lambda l: l.reconciliation_date or datetime.date.min ) - ] - # 如果 penalty.pay.line 没取到,兜底用 receivable.modify.line 中 is_penalty=True 的收款 - if not penalty_pay_lines and modify_lines: - penalty_pay_lines = [ - type('PenaltyPayProxy', (), { - 'amount': ml.amount, - 'date': ml.reconciliation_date, - }) - for ml in modify_lines - if getattr(ml, 'is_penalty', False) - ] + total_penalty_collected = round( + sum((line.amount or 0.0) for line in penalty_payment_lines), + 2, + ) + penalty_payment_date = penalty_payment_lines[-1].reconciliation_date if penalty_payment_lines else None - collected = sum((getattr(l, 'amount', 0.0) or 0.0) for l in penalty_pay_lines) - total_penalty_collected = round(collected, 2) - - # 违约金收款时间:取最新收款日期 - penalty_payment_date = None - if penalty_pay_lines: - latest_penalty_pay = sorted( - [l for l in penalty_pay_lines if getattr(l, 'date', False)], - key=lambda l: l.date, - reverse=True, - ) - penalty_payment_date = latest_penalty_pay[0].date if latest_penalty_pay else None - - _logger.info(f"total_penalty_collected: {total_penalty_collected}, penalty_payment_date: {penalty_payment_date}") # 复用 _onchange_calculation_date 逻辑计算违约金时间区间、逆期天数、违约金 penalty_range_str = ""