From d8625d23ea16e63bca22491ea5e9f05fc5b1a143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= <9664676+pengyuthon@user.noreply.gitee.com> Date: Fri, 20 Mar 2026 20:41:54 +0800 Subject: [PATCH 01/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/soong_workflow_record.py | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/soong_workflow/models/soong_workflow_record.py b/soong_workflow/models/soong_workflow_record.py index bce7f00a4..27d95184c 100644 --- a/soong_workflow/models/soong_workflow_record.py +++ b/soong_workflow/models/soong_workflow_record.py @@ -657,22 +657,44 @@ class SoongWorkflowRecord(models.Model): @api.model def get_view(self, view_id=None, view_type='form', **options): - current_user_id = self.env.user.id - current_employee = self.env['hr.employee'].search([('user_id', '=', current_user_id)], limit=1) - view = super().get_view(view_id, view_type, **options) - if not current_employee: - return view - if view_type == 'list': - all_records = self.search([ - '|', - ('employee_ids', 'in', [current_employee.id]), - ('employee2_ids', 'in', [current_employee.id]), - ('reclaim_state', '=', False) # 补回丢失的过滤条件 - ]) # 限制最大记录数,避免性能问题 - if all_records: - all_records._compute_is_done() - all_records._compute_is_to_do() - return view + all_records = self.search([ + ('users_ids', 'in', self.env.user.id), + ]) + for record in all_records: + # 校验关联记录是否存在 + if not record.res_model or not record.res_id: + continue + record_id = self.env[record.res_model].browse(record.res_id) + if not record_id or not hasattr(record_id, 'log_ids'): + continue + + current_user_logs = record_id.log_ids.filtered( + lambda l: l.done_by.id == self.env.user.id + ) + if not current_user_logs: + continue + approval_states = current_user_logs.mapped('approval_state') + if any(state in ['0', '1'] for state in approval_states): + record.write({'soong_state': 'to_do'}) + elif all(state == '2' for state in approval_states): + record.write({'soong_state': 'done'}) + for log in current_user_logs: + if log.name == '归档': + if log.approval_state == '0' or log.approval_state == '1': + record.write({'soong_state': 'to_active'}) + elif log.approval_state == '2': + record.write({'soong_state': 'active'}) + break + + for log2 in record_id.log_ids2: + if log2.done_by.name == self.env.user.name: + if log2.approval_state == '2': + record.write({'soong_state': 'done'}) + elif log2.approval_state in ['0', '1']: + record.write({'soong_state': 'to_do'}) + record._compute_is_done() + record._compute_is_to_do() + return super(SoongWorkflowRecord, self).get_view(view_id, view_type, **options) def to_do_process_reminders(self): workflow_records = self.env['soong.workflow.record'].search([ From 4132e5c8098087c9bf8f62983b71cf25c0671db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= <9664676+pengyuthon@user.noreply.gitee.com> Date: Fri, 20 Mar 2026 20:43:05 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- soong_workflow/models/soong_workflow_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/soong_workflow/models/soong_workflow_record.py b/soong_workflow/models/soong_workflow_record.py index 27d95184c..174438abb 100644 --- a/soong_workflow/models/soong_workflow_record.py +++ b/soong_workflow/models/soong_workflow_record.py @@ -659,6 +659,7 @@ class SoongWorkflowRecord(models.Model): def get_view(self, view_id=None, view_type='form', **options): all_records = self.search([ ('users_ids', 'in', self.env.user.id), + ('reclaim_state', '=', False) ]) for record in all_records: # 校验关联记录是否存在 From 71b04dc16103b6669d5bcd922b1bc25f1b0f3116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= <9664676+pengyuthon@user.noreply.gitee.com> Date: Fri, 20 Mar 2026 20:57:25 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/soong_workflow_record.py | 89 +++++++++++++------ 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/soong_workflow/models/soong_workflow_record.py b/soong_workflow/models/soong_workflow_record.py index 174438abb..26c6b8f38 100644 --- a/soong_workflow/models/soong_workflow_record.py +++ b/soong_workflow/models/soong_workflow_record.py @@ -657,44 +657,77 @@ class SoongWorkflowRecord(models.Model): @api.model def get_view(self, view_id=None, view_type='form', **options): + if view_type != 'list': + return super(SoongWorkflowRecord, self).get_view(view_id, view_type, **options) + current_user = self.env.user all_records = self.search([ - ('users_ids', 'in', self.env.user.id), + ('users_ids', 'in', current_user.id), ('reclaim_state', '=', False) ]) + if not all_records: + return super(SoongWorkflowRecord, self).get_view(view_id, view_type, **options) + model_res_ids = {} for record in all_records: - # 校验关联记录是否存在 if not record.res_model or not record.res_id: continue - record_id = self.env[record.res_model].browse(record.res_id) + if record.res_model not in model_res_ids: + model_res_ids[record.res_model] = [] + model_res_ids[record.res_model].append(record.res_id) + + model_records = {} + for model, res_ids in model_res_ids.items(): + model_records[model] = self.env[model].browse(res_ids).sudo().prefetch(['log_ids', 'log_ids2']) + + record_state_map = {} + for record in all_records: + if not record.res_model or not record.res_id: + continue + record_id = model_records.get(record.res_model, self.env[model]).browse(record.res_id) if not record_id or not hasattr(record_id, 'log_ids'): continue - current_user_logs = record_id.log_ids.filtered( - lambda l: l.done_by.id == self.env.user.id - ) - if not current_user_logs: - continue - approval_states = current_user_logs.mapped('approval_state') - if any(state in ['0', '1'] for state in approval_states): - record.write({'soong_state': 'to_do'}) - elif all(state == '2' for state in approval_states): - record.write({'soong_state': 'done'}) - for log in current_user_logs: - if log.name == '归档': - if log.approval_state == '0' or log.approval_state == '1': - record.write({'soong_state': 'to_active'}) - elif log.approval_state == '2': - record.write({'soong_state': 'active'}) - break + new_state = None + # 处理log_ids(批量过滤,无数据库查询) + current_user_logs = record_id.log_ids.filtered(lambda l: l.done_by.id == current_user.id) + if current_user_logs: + approval_states = current_user_logs.mapped('approval_state') + # 优先级:归档日志 > 普通日志 + archive_log = current_user_logs.filtered(lambda l: l.name == '归档') + if archive_log: + if archive_log[0].approval_state in ['0', '1']: + new_state = 'to_active' + elif archive_log[0].approval_state == '2': + new_state = 'active' + else: + if any(state in ['0', '1'] for state in approval_states): + new_state = 'to_do' + elif all(state == '2' for state in approval_states): + new_state = 'done' - for log2 in record_id.log_ids2: - if log2.done_by.name == self.env.user.name: - if log2.approval_state == '2': - record.write({'soong_state': 'done'}) - elif log2.approval_state in ['0', '1']: - record.write({'soong_state': 'to_do'}) - record._compute_is_done() - record._compute_is_to_do() + # 处理log_ids2(批量过滤,用ID匹配而非name) + if not new_state and hasattr(record_id, 'log_ids2'): + user_logs2 = record_id.log_ids2.filtered(lambda l: l.done_by.id == current_user.id) + if user_logs2: + log2_states = user_logs2.mapped('approval_state') + if '2' in log2_states: + new_state = 'done' + elif any(s in ['0', '1'] for s in log2_states): + new_state = 'to_do' + if new_state: + record_state_map[record.id] = new_state + + if record_state_map: + state_groups = {} + for rec_id, state in record_state_map.items(): + if state not in state_groups: + state_groups[state] = [] + state_groups[state].append(rec_id) + for state, rec_ids in state_groups.items(): + self.browse(rec_ids).write({'soong_state': state}) + + if all_records: + all_records._compute_is_done() + all_records._compute_is_to_do() return super(SoongWorkflowRecord, self).get_view(view_id, view_type, **options) def to_do_process_reminders(self): From 4af1d8d09788d5acbd32c6477a5b18b91d3af8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= <9664676+pengyuthon@user.noreply.gitee.com> Date: Sat, 21 Mar 2026 15:49:54 +0800 Subject: [PATCH 04/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_hr_attendance/models/hr_attendance.py | 6 +- .../models/yuthon_vacation_reports.py | 224 +++++++----------- .../views/yuthon_vacation_reports_views.xml | 2 + 3 files changed, 96 insertions(+), 136 deletions(-) diff --git a/yuthon_hr_attendance/models/hr_attendance.py b/yuthon_hr_attendance/models/hr_attendance.py index 28455fa6c..3940de0b7 100644 --- a/yuthon_hr_attendance/models/hr_attendance.py +++ b/yuthon_hr_attendance/models/hr_attendance.py @@ -278,7 +278,6 @@ class HrAttendance(models.Model): def attendance_abnormal_remind(self): current_utc_time = fields.Datetime.now() target_time = current_utc_time + timedelta(hours=8) - target_hour = target_time.hour target_minute = target_time.minute @@ -293,6 +292,9 @@ class HrAttendance(models.Model): return today = date.today() + is_workday = self._process_shift_days(today) + if not is_workday: + return current_year = today.year current_month = today.month if current_month == 1: @@ -364,7 +366,7 @@ class HrAttendance(models.Model): url = f'https://phone.thtzjt.com/phone/AttendanceSummary/{last_month.year}/{last_month.month}/{emp.user_id.id}' self.sudo().env["wecom.apps"].sync_send_message_textcard(category='att', user_id=emp.wecom_userid, - title=f'【{title}】', + title=f'{title}', description='考勤异常请尽快处理', exception_alerts = True, url=url, btntxt="查看详情") diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py index 5012908fc..acfdf4d8d 100644 --- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py +++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py @@ -98,6 +98,7 @@ class YuthonVacationReports(models.Model): should_attendance = fields.Float(string="应到", store=True, group_operator=None, compute='compute_attendance_fields') actual_attendance = fields.Float(string="实到", compute='compute_attendance_fields', store=True,group_operator=None) expatriation = fields.Float(string='外派', compute='compute_attendance_fields', store=True, group_operator=None) + go_out = fields.Float(string='外出', compute='compute_attendance_fields', store=True, group_operator=None) is_late_card = fields.Integer(string="迟到", compute='compute_attendance_fields', store=True, group_operator=None) is_early_card = fields.Integer(string="早退", compute='compute_attendance_fields', store=True, group_operator=None) @@ -822,191 +823,154 @@ class YuthonVacationReports(models.Model): @api.depends('employee_id', 'vacation_date') def compute_attendance_fields(self): for rec in self: - is_late = 0 - is_early = 0 - is_no = 0 - is_work = 0 - is_abnormal = 0 - probation_days = 0 # 试用期天数 - regular_days = 0 # 转正天数 - expatriation_days = 0 # 初始化外派天数为0 - rec.test_days = 0 - rec.right_days = 0 - actual_attendance = 0 # 实到天数 = 应到天数 - 请假总天数 - 异常天数 - leave_total_days = 0 # 累计请假天数 + # 初始化基础变量 + is_late = is_early = is_no = is_work = is_abnormal = 0 + probation_days = regular_days = expatriation_days = 0 + actual_attendance = leave_total_days = 0 is_retired = rec.employee_id.user_state in ('leave', 'withdraw') - # 初始化外派相关变量 - is_outside = False # 是否有外派单 - exp_has_return = False # 是否有返回单 - exp_start_date = None # 外派开始日期 - exp_end_return = None # 返回单结束日期 - exp_end_date = None # 外派实际结束日期 + # 核心:外出天数统计(按workflow_id去重) + go_out_days = 0.0 # 外出总天数 + processed_workflow_ids = set() # 已处理的workflow_id(去重) + + # 外派相关变量 + is_outside = exp_has_return = False + exp_start_date = exp_end_return = exp_end_date = None if rec.employee_id and rec.vacation_date and not is_retired: - year = rec.vacation_date.year - month = rec.vacation_date.month + # 考勤月份时间范围 + year, month = rec.vacation_date.year, rec.vacation_date.month month_start = date(year, month, 1) month_end = date(year, month, calendar.monthrange(year, month)[1]) entry_date = rec.employee_id.entry_date days_in_month = calendar.monthrange(year, month)[1] - # ===================== 1. 查询外派/返回单 ===================== + # 1. 查询外派/返回单 employee = rec.employee_id - - # 1.1 查询该员工当月有效的外派驻点审批单 exp_approval = self.env['expatriation.approval'].sudo().search([ - ('expatriates_ids', 'in', [employee.id]), # 外派人员包含当前员工 - ('start_date', '<=', month_end), # 外派开始日期 <= 当月末 - ('work_end', '=', True) + ('expatriates_ids', 'in', [employee.id]), + ('start_date', '<=', month_end), ('work_end', '=', True) ], limit=1) - if exp_approval: is_outside = True - exp_start_date = exp_approval.start_date # 取外派单的开始日期 - - # 1.2 查询该员工当月有效的外派返回确认单 + exp_start_date = exp_approval.start_date + # 查询返回单 back_approval = self.env['back.approval'].sudo().search([ - ('expatriates_id', '=', employee.id), # 匹配当前员工 - ('end_date', '>=', month_start), # 返回结束日期 >= 当月初 - ('end_date', '<=', month_end), # 返回结束日期 <= 当月末 - ('work_end', '=', True) + ('expatriates_id', '=', employee.id), + ('end_date', '>=', month_start), ('end_date', '<=', month_end), ('work_end', '=', True) ], limit=1) - if back_approval: exp_has_return = True - exp_end_return = back_approval.end_date # 取返回单的结束日期 - exp_end_date = min(date.today(), exp_end_return) # 有返回单:外派结束=返回结束日 + exp_end_return = back_approval.end_date + exp_end_date = min(date.today(), exp_end_return) else: - # 无返回单:外派结束=当前日期(且不超过当月末) exp_end_date = min(date.today(), month_end) - # ===================== 2. 计算外派天数 ===================== + # 2. 计算外派天数 if is_outside and exp_start_date and exp_end_date: - # 外派天数 = 外派开始日 到 外派结束日 的工作日数 - # 确保外派开始日不早于当月初,避免跨月统计 actual_exp_start = max(exp_start_date, month_start) expatriation_days = rec._create_leave_record(employee, actual_exp_start, exp_end_date) - # 确定考勤统计的起始日 - if not entry_date or month_start > entry_date: - start_day = 1 - else: - start_day = min(entry_date.day, days_in_month) - - # 根据外派返回单调整考勤统计起始日 + # 3. 确定考勤统计起始日(适配外派/入职) + start_day = 1 if (not entry_date or month_start > entry_date) else min(entry_date.day, days_in_month) if is_outside and exp_has_return and exp_end_return: - # 有返回单:从返回结束次日开始统计 - if exp_end_return.month == month: # 确保返回日期在当月 - actual_start_day = exp_end_return.day + 1 - else: - actual_start_day = start_day - actual_start_day = max(actual_start_day, start_day) # 不早于入职/月初 + actual_start_day = exp_end_return.day + 1 if exp_end_return.month == month else start_day + actual_start_day = max(actual_start_day, start_day) elif is_outside and not exp_has_return: - # 无返回单:不统计考勤(实到应到均为0) - actual_start_day = days_in_month + 1 # 让range为空 + actual_start_day = days_in_month + 1 # 无返回单不统计 else: - # 非外派:使用原有起始日 actual_start_day = start_day - actual_days_range = range(actual_start_day, days_in_month + 1) - - for day in actual_days_range: + # 4. 遍历当月考勤日统计 + for day in range(actual_start_day, days_in_month + 1): current_date = date(year, month, day) + # 跳过外派日期 + if is_outside and exp_start_date and exp_end_date and ( + exp_start_date <= current_date <= exp_end_date): + continue - # 跳过外派日期(外派开始到返回结束/当前日期) - if is_outside and exp_start_date and exp_end_date: - if exp_start_date <= current_date <= exp_end_date: - continue - + # 查询当日考勤记录 attendances = self.env["hr.attendance"].sudo().search([ - ('employee_id', '=', rec.employee_id.id), - ("current_data", '=', current_date) + ('employee_id', '=', rec.employee_id.id), ("current_data", '=', current_date) ]) - if attendances: - # 1. 过滤有效考勤:仅排除休息/异常,保留请假记录 - valid_attendances = [ - att for att in attendances - if (att.check_in or att.check_out) - and att.shift_name != '休息' - and not att.is_abnormal - ] - leave_hours = 0 + # 过滤有效考勤记录 + valid_attendances = [att for att in attendances if ( + att.check_in or att.check_out) and att.shift_name != '休息' and not att.is_abnormal] leave_days = 0 - processed_leave_codes = set() # 记录已处理的请假单code,避免重复扣减 + processed_leave_codes = set() + # 遍历考勤记录统计 for att in attendances: + # ========== 核心:外出天数统计(按workflow_id去重) ========== + if (att.workflow_id and att.workflow_id.workflow_id + and att.workflow_id.workflow_id.name == '外出' + and att.workflow_id.id not in processed_workflow_ids): + # 通过model+res_id获取外出记录 + leave_model = self.env[att.workflow_id.res_model].sudo() + leave_record = leave_model.browse(att.workflow_id.res_id) + if leave_record: + processed_workflow_ids.add(att.workflow_id.id) + # 区分按天/按小时计算 + if leave_record.is_day: + go_out_days += float(leave_record.days or 0) + elif leave_record.is_hour: + go_out_days += float(leave_record.hours or 0) / 8 + + # 原有请假天数统计 if att.workflow_id and att.workflow_id.workflow_id.name == '请假': leave_record = self.env[att.workflow_id.res_model].sudo().browse(att.workflow_id.res_id) if leave_record.code and leave_record.code in processed_leave_codes: continue - if leave_record.days: leave_days += leave_record.days - if leave_record.code: - processed_leave_codes.add(leave_record.code) + processed_leave_codes.add(leave_record.code) + # 计算当日实到天数 total_leave_days = leave_days leave_total_days += leave_days if valid_attendances: - daily_actual = max(1 - total_leave_days, 0) - actual_attendance += daily_actual - - rec.actual_attendance = max(actual_attendance, 0) + actual_attendance += max(1 - total_leave_days, 0) + # 统计考勤异常次数 for att in attendances: - if att.is_late_card: - is_late += 1 - if att.is_early_card: - is_early += 1 - if att.is_no_card: - is_no += 1 - if att.absenteeism_card: - is_work += 1 - if att.is_abnormal: - is_abnormal += 1 + if att.is_late_card: is_late += 1 + if att.is_early_card: is_early += 1 + if att.is_no_card: is_no += 1 + if att.absenteeism_card: is_work += 1 + if att.is_abnormal: is_abnormal += 1 - # 重新计算应到天数:考虑外派返回单 + # 5. 计算应到天数(适配外派) contract = employee.contract_id if is_outside: if exp_has_return and exp_end_return: - # 有返回单:应到天数 = 返回结束次日 到 月末的工作日 - duty_start = exp_end_return + timedelta(days=1) - duty_end = month_end - work_days = rec._create_leave_record(rec.employee_id, duty_start, duty_end) + work_days = rec._create_leave_record(employee, exp_end_return + timedelta(days=1), month_end) else: - # 无返回单:应到天数为0 work_days = 0 else: - # 非外派:使用原有逻辑计算应到天数 - if contract and contract.date_start: - start_date = month_start if (is_outside and not contract) else max(contract.date_start, - month_start) - else: - start_date = month_start - end_date = month_end if (is_outside and not contract) else ( - min(contract.date_end, month_end) if contract and contract.date_end else month_end) - work_days = rec._create_leave_record(rec.employee_id, start_date, end_date) + start_date = max(contract.date_start, month_start) if ( + contract and contract.date_start) else month_start + end_date = min(contract.date_end, month_end) if (contract and contract.date_end) else month_end + work_days = rec._create_leave_record(employee, start_date, end_date) - # 试用期天数计算(原有逻辑) + # 6. 计算试用期/转正天数 probation_contract = self.env['hr.contract'].search([ - ('employee_id', '=', rec.employee_id.id), - ('is_probation', '=', True), - ('end_date', '>=', month_start), + ('employee_id', '=', rec.employee_id.id), ('is_probation', '=', True), + ('end_date', '>=', month_start) ], limit=1) if probation_contract: prob_end = min(probation_contract.end_date, month_end) prob_start = max(probation_contract.start_date, month_start) - if prob_start <= prob_end: # 避免开始日期大于结束日期 - probation_days = rec._create_leave_record(rec.employee_id, prob_start, prob_end) - - # 转正阶段的时间范围:当月剩余时段(实习结束后 → 月末) - reg_start = prob_end + timedelta(days=1) - reg_end = month_end - if reg_start <= reg_end: - regular_days = rec._create_leave_record(rec.employee_id, reg_start, reg_end) + if prob_start <= prob_end: + probation_days = rec._create_leave_record(employee, prob_start, prob_end) + # 转正天数 + reg_start = prob_end + timedelta(days=1) + reg_end = month_end + if reg_start <= reg_end: + regular_days = rec._create_leave_record(employee, reg_start, reg_end) + # 赋值考勤字段 + rec.go_out = go_out_days # 外出天数最终赋值 rec.should_attendance = work_days rec.test_days = probation_days rec.right_days = regular_days @@ -1015,22 +979,14 @@ class YuthonVacationReports(models.Model): rec.is_no_card = is_no rec.is_work = is_work rec.is_abnormal = is_abnormal - rec.actual_attendance = actual_attendance + rec.actual_attendance = max(actual_attendance, 0) rec.att_mount = actual_attendance rec.expatriation = expatriation_days else: - # 重置字段值(员工为空/离职/无vacation_date时) - rec.test_days = 0 - rec.right_days = 0 - rec.actual_attendance = 0 - rec.should_attendance = 0 - rec.is_late_card = 0 - rec.is_early_card = 0 - rec.is_no_card = 0 - rec.is_work = 0 - rec.is_abnormal = 0 - rec.att_mount = 0 - rec.expatriation = 0 + # 重置字段(员工为空/离职/无考勤月份) + rec.test_days = rec.right_days = rec.actual_attendance = rec.should_attendance = 0 + rec.is_late_card = rec.is_early_card = rec.is_no_card = rec.is_work = rec.is_abnormal = 0 + rec.att_mount = rec.expatriation = rec.go_out = 0.0 def update_is_confirm1(self): records = self.env["yuthon.vacation.reports"].browse( diff --git a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml index 854713d4b..2460d5e86 100644 --- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml +++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml @@ -63,6 +63,7 @@ + @@ -131,6 +132,7 @@ + From d8fe15c7d0f68299661616e262c75f43bcfd659a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sat, 21 Mar 2026 16:09:31 +0800 Subject: [PATCH 05/16] =?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/property_bank_statements.py | 2 +- yuthon_report/__manifest__.py | 1 + yuthon_report/models/__init__.py | 1 + .../models/deposit_month_received_report.py | 69 ++++-- .../models/deposit_pre_received_report.py | 69 ++++-- .../models/receivable_flow_report.py | 211 ++++++++++++++++++ .../views/receivable_flow_report.xml | 40 ++++ 7 files changed, 362 insertions(+), 31 deletions(-) create mode 100644 yuthon_report/models/receivable_flow_report.py create mode 100644 yuthon_report/views/receivable_flow_report.xml diff --git a/yuthon_property/models/property_bank_statements.py b/yuthon_property/models/property_bank_statements.py index 29fea8d2c..aa3106ad6 100644 --- a/yuthon_property/models/property_bank_statements.py +++ b/yuthon_property/models/property_bank_statements.py @@ -197,7 +197,7 @@ class ReceivableModifyLine(models.Model): original_amount = fields.Float(related='bank_id.original_amount', string='交易金额', store=False) no_amount = fields.Float(related='bank_id.no_amount', string='未对', store=False) bank_state = fields.Selection(related='bank_id.bank_state', string='状态', store=False) - receivable_id = fields.Many2one('property.accounts.receivable', string='应收账单') + receivable_id = fields.Many2one('property.accounts.receivable', string='合同编号') management_type_id = fields.Many2one('yuthon.management.type', related='receivable_id.management_type_id', string='应收类型', store=True) due_date = fields.Date(related='receivable_id.due_date', string='应收开始', store=False) due_date_end = fields.Date(related='receivable_id.due_date_end', string='应收结束', store=False) diff --git a/yuthon_report/__manifest__.py b/yuthon_report/__manifest__.py index a211ecdce..f7ea90084 100644 --- a/yuthon_report/__manifest__.py +++ b/yuthon_report/__manifest__.py @@ -35,6 +35,7 @@ 'views/att_report_view.xml', 'views/staff_position_report.xml', 'views/hydropower_report_views.xml', + 'views/receivable_flow_report.xml', ], 'installable': True, 'application': True, diff --git a/yuthon_report/models/__init__.py b/yuthon_report/models/__init__.py index eaec86839..da42b8dd4 100644 --- a/yuthon_report/models/__init__.py +++ b/yuthon_report/models/__init__.py @@ -28,4 +28,5 @@ from . import deposit_pre_received_report from . import deposit_month_received_report from . import deposit_cum_arrears_report from . import deposit_month_refund_report +from . import receivable_flow_report from . import hydropower_report \ No newline at end of file diff --git a/yuthon_report/models/deposit_month_received_report.py b/yuthon_report/models/deposit_month_received_report.py index e05222300..7af2692bf 100644 --- a/yuthon_report/models/deposit_month_received_report.py +++ b/yuthon_report/models/deposit_month_received_report.py @@ -77,38 +77,77 @@ class DepositMonthReceivedReport(models.Model): wizards = self.env['penalty.line.wizard'].search( [('receivable_id', '=', receivable_id)] ) - # 只处理有wizard关联的记录,确保使用penalty.pay.line的字段 - for wizard in wizards: - for pay in wizard.pay_line_ids: - if not pay.date: - continue - if period_start <= pay.date <= period_end: - amount = round(pay.amount, 2) + if wizards: + # 处理有wizard关联的记录,确保使用penalty.pay.line的字段 + for wizard in wizards: + for pay in wizard.pay_line_ids: + if not pay.date: + continue + # 确保支付类型是押金 + if pay.management_type_id and pay.management_type_id.name != '押金': + continue + if pay.date >= period_start and pay.date <= period_end: + amount = round(pay.amount, 2) + total_amount += amount + rows.append({ + "序号": idx, + "合同编号": { + 'model': 'property.lease.contract', + 'id': rec.contract_id.id if rec.contract_id else None, + 'display_name': rec.contract_id.contract_code if rec.contract_id else "", + } if rec.contract_id else "", + "物业名称": { + 'model': 'yuthon.property', + 'id': rec.contract_id.lease_ids[0].address_id.id if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else None, + 'display_name': rec.property_name, + } if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "", + "承租方名称": { + 'model': 'property.tenant.information', + 'id': rec.lessee_id.id if rec.lessee_id else None, + 'display_name': rec.lessee_id.name if rec.lessee_id else "", + } if rec.lessee_id else "", + "收款日期": pay.date.strftime("%Y-%m-%d"), + "金额": amount, + "所属公司": rec.company_id.name if rec.company_id else "", + "管理员": { + 'model': 'res.users', + 'id': rec.admin_id.id if rec.admin_id else None, + 'display_name': rec.admin_id.name if rec.admin_id else "", + } if rec.admin_id else "", + }) + idx += 1 + else: + # 如果没有关联的wizard,尝试直接从line获取信息 + if line.delivered1 and (line.receivable_id.payout_date or line.create_date): + # 使用payout_date作为收款日期,如果没有则使用create_date + payment_date = line.receivable_id.payout_date or line.create_date.date() + if period_start <= payment_date <= period_end: + amount = round(line.delivered1, 2) total_amount += amount rows.append({ "序号": idx, "合同编号": { 'model': 'property.lease.contract', - 'id': rec.contract_id.id, - 'display_name': rec.contract_id.contract_code, + 'id': rec.contract_id.id if rec.contract_id else None, + 'display_name': rec.contract_id.contract_code if rec.contract_id else "", } if rec.contract_id else "", "物业名称": { 'model': 'yuthon.property', - 'id': rec.contract_id.lease_ids[0].address_id.id, + 'id': rec.contract_id.lease_ids[0].address_id.id if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else None, 'display_name': rec.property_name, } if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "", "承租方名称": { 'model': 'property.tenant.information', - 'id': rec.lessee_id.id, - 'display_name': rec.lessee_id.name, + 'id': rec.lessee_id.id if rec.lessee_id else None, + 'display_name': rec.lessee_id.name if rec.lessee_id else "", } if rec.lessee_id else "", - "收款日期": pay.date.strftime("%Y-%m-%d"), + "收款日期": payment_date.strftime("%Y-%m-%d"), "金额": amount, "所属公司": rec.company_id.name if rec.company_id else "", "管理员": { 'model': 'res.users', - 'id': rec.admin_id.id, - 'display_name': rec.admin_id.name, + 'id': rec.admin_id.id if rec.admin_id else None, + 'display_name': rec.admin_id.name if rec.admin_id else "", } if rec.admin_id else "", }) idx += 1 diff --git a/yuthon_report/models/deposit_pre_received_report.py b/yuthon_report/models/deposit_pre_received_report.py index 062386e51..d0d48697b 100644 --- a/yuthon_report/models/deposit_pre_received_report.py +++ b/yuthon_report/models/deposit_pre_received_report.py @@ -65,38 +65,77 @@ class DepositPreReceivedReport(models.Model): wizards = self.env['penalty.line.wizard'].search( [('receivable_id', '=', receivable_id)] ) - # 只处理有wizard关联的记录,确保使用penalty.pay.line的字段 - for wizard in wizards: - for pay in wizard.pay_line_ids: - if not pay.date: - continue - if pay.date <= period_end: - amount = round(pay.amount, 2) + if wizards: + # 处理有wizard关联的记录,确保使用penalty.pay.line的字段 + for wizard in wizards: + for pay in wizard.pay_line_ids: + if not pay.date: + continue + # 确保支付类型是押金 + if pay.management_type_id and pay.management_type_id.name != '押金': + continue + if pay.date <= period_end: + amount = round(pay.amount, 2) + total_amount += amount + rows.append({ + "序号": idx, + "合同编号": { + 'model': 'property.lease.contract', + 'id': rec.contract_id.id if rec.contract_id else None, + 'display_name': rec.contract_id.contract_code if rec.contract_id else "", + } if rec.contract_id else "", + "物业名称": { + 'model': 'yuthon.property', + 'id': rec.contract_id.lease_ids[0].address_id.id if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else None, + 'display_name': rec.property_name, + } if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "", + "承租方名称": { + 'model': 'property.tenant.information', + 'id': rec.lessee_id.id if rec.lessee_id else None, + 'display_name': rec.lessee_id.name if rec.lessee_id else "", + } if rec.lessee_id else "", + "收款日期": pay.date.strftime("%Y-%m-%d"), + "金额": amount, + "所属公司": rec.company_id.name if rec.company_id else "", + "管理员": { + 'model': 'res.users', + 'id': rec.admin_id.id if rec.admin_id else None, + 'display_name': rec.admin_id.name if rec.admin_id else "", + } if rec.admin_id else "", + }) + idx += 1 + else: + # 如果没有关联的wizard,尝试直接从line获取信息 + if line.delivered1 and (line.receivable_id.payout_date or line.create_date): + # 使用payout_date作为收款日期,如果没有则使用create_date + payment_date = line.receivable_id.payout_date or line.create_date.date() + if payment_date <= period_end: + amount = round(line.delivered1, 2) total_amount += amount rows.append({ "序号": idx, "合同编号": { 'model': 'property.lease.contract', - 'id': rec.contract_id.id, - 'display_name': rec.contract_id.contract_code, + 'id': rec.contract_id.id if rec.contract_id else None, + 'display_name': rec.contract_id.contract_code if rec.contract_id else "", } if rec.contract_id else "", "物业名称": { 'model': 'yuthon.property', - 'id': rec.contract_id.lease_ids[0].address_id.id, + 'id': rec.contract_id.lease_ids[0].address_id.id if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else None, 'display_name': rec.property_name, } if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "", "承租方名称": { 'model': 'property.tenant.information', - 'id': rec.lessee_id.id, - 'display_name': rec.lessee_id.name, + 'id': rec.lessee_id.id if rec.lessee_id else None, + 'display_name': rec.lessee_id.name if rec.lessee_id else "", } if rec.lessee_id else "", - "收款日期": pay.date.strftime("%Y-%m-%d"), + "收款日期": payment_date.strftime("%Y-%m-%d"), "金额": amount, "所属公司": rec.company_id.name if rec.company_id else "", "管理员": { 'model': 'res.users', - 'id': rec.admin_id.id, - 'display_name': rec.admin_id.name, + 'id': rec.admin_id.id if rec.admin_id else None, + 'display_name': rec.admin_id.name if rec.admin_id else "", } if rec.admin_id else "", }) idx += 1 diff --git a/yuthon_report/models/receivable_flow_report.py b/yuthon_report/models/receivable_flow_report.py new file mode 100644 index 000000000..026984211 --- /dev/null +++ b/yuthon_report/models/receivable_flow_report.py @@ -0,0 +1,211 @@ +import datetime + +from odoo import api, models + + +class ReceivableFlowReport(models.Model): + _inherit = 'property.accounts.receivable' + + @api.model + def get_receivable_flow_report(self, params): + """ + 应收流水中间报表: + - 显示租金的完整流水信息 + - 包括基本信息、收款情况和违约金相关信息 + """ + domain = [ + ('is_invalid', '=', False), + ] + + date_start = params.get("date_start") + date_end = params.get("date_end") + if date_start: + domain.append(('due_date', '>=', datetime.datetime.fromisoformat(date_start).date())) + if date_end: + domain.append(('due_date', '<=', datetime.datetime.fromisoformat(date_end).date())) + + keyword = params.get("keyword") + if keyword: + domain += [ + '|', '|', + ('contract_id.contract_code', 'ilike', keyword), + ('property_name', 'ilike', keyword), + ('lessee_id.name', 'ilike', keyword), + ] + + condition = params.get('condition') + partner_id = params.get('partner_id') + company_id = params.get('company_id') + + if condition: + domain.append(('tenure_type', '=', condition)) + if partner_id: + domain.append(('precinct_id', '=', partner_id)) + if company_id: + domain.append(('company_id', '=', company_id)) + + receivables = self.search(domain, order="due_date asc") + + rows = [] + + for idx, rec in enumerate(receivables, start=1): + # 获取收款信息 + modify_lines = self.env['receivable.modify.line'].search([ + ('receivable_id', '=', rec.id), + ('reconciliation_date', '!=', False) + ]) + + # 计算收款总金额 + total_collected = sum(line.amount for line in modify_lines) + + # 获取最新的收款时间和备注 + latest_payment = modify_lines.sorted(key=lambda l: l.reconciliation_date or datetime.datetime.min, reverse=True) + payment_date = latest_payment[0].reconciliation_date if latest_payment else None + payment_remark = latest_payment[0].bank_id.remake if latest_payment and latest_payment[0].bank_id else "" + + # 获取收款金额(最近一次) + payment_amount = latest_payment[0].amount if latest_payment else 0.0 + + # 获取状态 + charge_status = "未收" + if rec.submitted == 0: + charge_status = "已收" + + # 计算违约金相关信息 + # 计算欠租金额(直接使用合同的rent字段,即月租金) + rent_owed = 0.0 + if rec.contract_id: + rent_owed = rec.contract_id.rent or 0.0 + rent_owed = rent_owed or rec.rent_owed or 0.0 + + # 计算每天违约金(欠租金额 × 违约金比例) + per_day = 0.0 + if rent_owed > 0 and rec.contract_id and rec.contract_id.percentage_liquidated_damages: + penalty_ratio = rec.contract_id.percentage_liquidated_damages.name or "0" + try: + penalty_ratio = float(penalty_ratio) + per_day = rent_owed * penalty_ratio + except ValueError: + per_day = 0.0 + per_day = per_day or rec.difference_amount or rec.per_day or 0.0 + + # 计算违约金金额(每天违约金 × 违约天数) + penalty_amount = 0.0 + billing_period = rec.charging_range or "" + days_diff = 0 + + if billing_period: + try: + # 兼容中文"到"和英文"~"两种分隔符 + if ' 到 ' in billing_period: + date_parts = billing_period.split(' 到 ') + elif ' ~ ' in billing_period: + date_parts = billing_period.split(' ~ ') + else: + date_parts = [] + if len(date_parts) == 2: + start_date_str = date_parts[0].strip() + end_date_str = date_parts[1].strip() + start_date = datetime.datetime.strptime(start_date_str, "%Y-%m-%d").date() + end_date = datetime.datetime.strptime(end_date_str, "%Y-%m-%d").date() + days_diff = (end_date - start_date).days + 1 + billing_period = f"{days_diff} {start_date_str} ~ {end_date_str}" + except Exception: + if rec.due_date: + start_date = rec.due_date.replace(day=1) + if rec.payout_date: + end_date = rec.payout_date + else: + if date_end: + end_date_obj = datetime.datetime.fromisoformat(date_end).date() + if end_date_obj.month == 12: + last_day = datetime.date(end_date_obj.year, 12, 31) + else: + last_day = datetime.date(end_date_obj.year, end_date_obj.month + 1, 1) - datetime.timedelta(days=1) + end_date = last_day + else: + today = datetime.date.today() + if today.month == 12: + last_day = datetime.date(today.year, 12, 31) + else: + last_day = datetime.date(today.year, today.month + 1, 1) - datetime.timedelta(days=1) + end_date = last_day + + days_diff = (end_date - start_date).days + 1 + billing_period = f"{days_diff} {start_date.strftime('%Y-%m-%d')} ~ {end_date.strftime('%Y-%m-%d')}" + else: + if rec.due_date: + start_date = rec.due_date.replace(day=1) + if rec.payout_date: + end_date = rec.payout_date + else: + if date_end: + end_date_obj = datetime.datetime.fromisoformat(date_end).date() + if end_date_obj.month == 12: + last_day = datetime.date(end_date_obj.year, 12, 31) + else: + last_day = datetime.date(end_date_obj.year, end_date_obj.month + 1, 1) - datetime.timedelta(days=1) + end_date = last_day + else: + today = datetime.date.today() + if today.month == 12: + last_day = datetime.date(today.year, 12, 31) + else: + last_day = datetime.date(today.year, today.month + 1, 1) - datetime.timedelta(days=1) + end_date = last_day + + days_diff = (end_date - start_date).days + 1 + billing_period = f"{days_diff} {start_date.strftime('%Y-%m-%d')} ~ {end_date.strftime('%Y-%m-%d')}" + + # 计算违约金金额 + penalty_amount = days_diff * per_day + penalty_amount = penalty_amount or rec.penalty or rec.original_amount or 0.0 + + # 获取违约金收款信息 + wizards = self.env['penalty.line.wizard'].search([('receivable_id', '=', rec.id)]) + penalty_pay_lines = wizards.mapped('pay_line_ids') + + # 计算违约金收款金额 + total_penalty_collected = sum(line.amount for line in penalty_pay_lines) + + # 获取最新的违约金收款时间 + latest_penalty_payment = penalty_pay_lines.sorted(key=lambda l: l.date or datetime.datetime.min, reverse=True) + penalty_payment_date = latest_penalty_payment[0].date if latest_penalty_payment else None + + rows.append({ + "序号": idx, + "租金id": rec.id, + "应收日期": rec.due_date.strftime("%Y-%m-%d") if rec.due_date else "", + "应收金额": round(rec.original_amount, 2), + "收款总金额": round(total_collected, 2), + "未收": round(rec.submitted, 2), + "收款金额": round(payment_amount, 2), + "收款时间": payment_date.strftime("%Y-%m-%d") if payment_date else "", + "收款备注": payment_remark, + "状态": charge_status, + "违约金计算时间": billing_period, + "违约天数": days_diff, + "违约金金额": round(penalty_amount, 2), + "违约金收款时间": penalty_payment_date.strftime("%Y-%m-%d") if penalty_payment_date else "", + "违约金收款金额": round(total_penalty_collected, 2), + "合同编号": { + 'model': 'property.lease.contract', + 'id': rec.contract_id.id if rec.contract_id else None, + 'display_name': rec.contract_id.contract_code if rec.contract_id else "", + } if rec.contract_id else "", + "物业名称": rec.property_name or "", + "承租方名称": { + 'model': 'property.tenant.information', + 'id': rec.lessee_id.id if rec.lessee_id else None, + 'display_name': rec.lessee_id.name if rec.lessee_id else "", + } if rec.lessee_id else "", + }) + + stats = [ + {'name': '记录总数', 'amount': len(rows)}, + {'name': '应收总额', 'amount': round(sum(row["应收金额"] for row in rows), 2)}, + {'name': '已收总额', 'amount': round(sum(row["收款总金额"] for row in rows), 2)}, + {'name': '未收总额', 'amount': round(sum(row["未收"] for row in rows), 2)}, + ] + + return {"rows": rows, "stats": stats} diff --git a/yuthon_report/views/receivable_flow_report.xml b/yuthon_report/views/receivable_flow_report.xml new file mode 100644 index 000000000..4f70c2c9f --- /dev/null +++ b/yuthon_report/views/receivable_flow_report.xml @@ -0,0 +1,40 @@ + + + + 应收流水中间报表 + receivable_report_client + + { + "sideBar": true, + "report_config": { + "model": "property.accounts.receivable", + "method": "get_receivable_flow_report", + "dateMode": "range", + "columns": [ + {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"}, + {"headerName": "租金id", "field": "租金id", "flex": 1}, + {"headerName": "应收日期", "field": "应收日期", "flex": 1}, + {"headerName": "应收金额", "field": "应收金额", "flex": 1, "type": "number"}, + {"headerName": "收款总金额", "field": "收款总金额", "flex": 1, "type": "number"}, + {"headerName": "未收", "field": "未收", "flex": 1, "type": "number"}, + {"headerName": "收款金额", "field": "收款金额", "flex": 1, "type": "number"}, + {"headerName": "收款时间", "field": "收款时间", "flex": 1}, + {"headerName": "收款备注", "field": "收款备注", "flex": 2, "wrapText": true}, + {"headerName": "状态", "field": "状态", "flex": 1}, + {"headerName": "违约金计算时间", "field": "违约金计算时间", "flex": 2.5, "wrapText": true}, + {"headerName": "违约天数", "field": "违约天数", "flex": 1, "type": "number"}, + {"headerName": "违约金金额", "field": "违约金金额", "flex": 1, "type": "number"}, + {"headerName": "违约金收款时间", "field": "违约金收款时间", "flex": 1}, + {"headerName": "违约金收款金额", "field": "违约金收款金额", "flex": 1, "type": "number"}, + {"headerName": "合同编号", "field": "合同编号", "flex": 1.2}, + {"headerName": "物业名称", "field": "物业名称", "flex": 2}, + {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4} + ] + } + } + + + + + + From 6b0efc5802c6b6592fc69ae2c7195db01eb2c2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sat, 21 Mar 2026 16:25:04 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_report/views/receivable_flow_report.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yuthon_report/views/receivable_flow_report.xml b/yuthon_report/views/receivable_flow_report.xml index 4f70c2c9f..5550fb826 100644 --- a/yuthon_report/views/receivable_flow_report.xml +++ b/yuthon_report/views/receivable_flow_report.xml @@ -36,5 +36,5 @@ - + From d748f55013a578a69947cdd34c6505517743b0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sat, 21 Mar 2026 16:39:00 +0800 Subject: [PATCH 07/16] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_report/views/receivable_flow_report.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yuthon_report/views/receivable_flow_report.xml b/yuthon_report/views/receivable_flow_report.xml index 5550fb826..4cff46fa9 100644 --- a/yuthon_report/views/receivable_flow_report.xml +++ b/yuthon_report/views/receivable_flow_report.xml @@ -13,6 +13,9 @@ "columns": [ {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"}, {"headerName": "租金id", "field": "租金id", "flex": 1}, + {"headerName": "合同编号", "field": "合同编号", "flex": 1.2}, + {"headerName": "物业名称", "field": "物业名称", "flex": 2}, + {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4}, {"headerName": "应收日期", "field": "应收日期", "flex": 1}, {"headerName": "应收金额", "field": "应收金额", "flex": 1, "type": "number"}, {"headerName": "收款总金额", "field": "收款总金额", "flex": 1, "type": "number"}, @@ -25,10 +28,7 @@ {"headerName": "违约天数", "field": "违约天数", "flex": 1, "type": "number"}, {"headerName": "违约金金额", "field": "违约金金额", "flex": 1, "type": "number"}, {"headerName": "违约金收款时间", "field": "违约金收款时间", "flex": 1}, - {"headerName": "违约金收款金额", "field": "违约金收款金额", "flex": 1, "type": "number"}, - {"headerName": "合同编号", "field": "合同编号", "flex": 1.2}, - {"headerName": "物业名称", "field": "物业名称", "flex": 2}, - {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4} + {"headerName": "违约金收款金额", "field": "违约金收款金额", "flex": 1, "type": "number"} ] } } From c222c684c46aa5ce621333f994648e1990268231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sat, 21 Mar 2026 17:04:13 +0800 Subject: [PATCH 08/16] =?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/receivable_flow_report.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/yuthon_report/models/receivable_flow_report.py b/yuthon_report/models/receivable_flow_report.py index 026984211..37e7515fc 100644 --- a/yuthon_report/models/receivable_flow_report.py +++ b/yuthon_report/models/receivable_flow_report.py @@ -45,21 +45,49 @@ class ReceivableFlowReport(models.Model): domain.append(('company_id', '=', company_id)) receivables = self.search(domain, order="due_date asc") + rec_ids = receivables.ids + + # ===== 批量预加载,避免N+1查询 ===== + # 1. 批量查收款明细行 + all_modify_lines = self.env['receivable.modify.line'].search([ + ('receivable_id', 'in', rec_ids), + ('reconciliation_date', '!=', False), + ]) + # 按 receivable_id 分组 + modify_map = {} # {receivable_id: [lines]} + for ml in all_modify_lines: + modify_map.setdefault(ml.receivable_id.id, []).append(ml) + + # 2. 批量查违约金wizard + all_wizards = self.env['penalty.line.wizard'].search([ + ('receivable_id', 'in', rec_ids), + ]) + # 按 receivable_id 分组,并预取 pay_line_ids + wizard_map = {} # {receivable_id: [wizards]} + for wz in all_wizards: + wizard_map.setdefault(wz.receivable_id.id, []).append(wz) + # 预取所有 pay_line_ids + all_pay_lines = all_wizards.mapped('pay_line_ids') + # 按 wizard_id 分组 + pay_line_map = {} # {wizard_id: [pay_lines]} + for pl in all_pay_lines: + pay_line_map.setdefault(pl.penalty_id.id, []).append(pl) + # ===== 预加载结束 ===== rows = [] for idx, rec in enumerate(receivables, start=1): - # 获取收款信息 - modify_lines = self.env['receivable.modify.line'].search([ - ('receivable_id', '=', rec.id), - ('reconciliation_date', '!=', False) - ]) + # 获取收款信息(从预加载字典取) + modify_lines = modify_map.get(rec.id, []) # 计算收款总金额 total_collected = sum(line.amount for line in modify_lines) # 获取最新的收款时间和备注 - latest_payment = modify_lines.sorted(key=lambda l: l.reconciliation_date or datetime.datetime.min, reverse=True) + latest_payment = sorted( + [l for l in modify_lines if l.reconciliation_date], + key=lambda l: l.reconciliation_date, reverse=True + ) payment_date = latest_payment[0].reconciliation_date if latest_payment else None payment_remark = latest_payment[0].bank_id.remake if latest_payment and latest_payment[0].bank_id else "" @@ -161,15 +189,20 @@ class ReceivableFlowReport(models.Model): penalty_amount = days_diff * per_day penalty_amount = penalty_amount or rec.penalty or rec.original_amount or 0.0 - # 获取违约金收款信息 - wizards = self.env['penalty.line.wizard'].search([('receivable_id', '=', rec.id)]) - penalty_pay_lines = wizards.mapped('pay_line_ids') + # 获取违约金收款信息(从预加载字典取) + wz_list = wizard_map.get(rec.id, []) + penalty_pay_lines = [] + for wz in wz_list: + penalty_pay_lines.extend(pay_line_map.get(wz.id, [])) # 计算违约金收款金额 total_penalty_collected = sum(line.amount for line in penalty_pay_lines) # 获取最新的违约金收款时间 - latest_penalty_payment = penalty_pay_lines.sorted(key=lambda l: l.date or datetime.datetime.min, reverse=True) + latest_penalty_payment = sorted( + [l for l in penalty_pay_lines if l.date], + key=lambda l: l.date, reverse=True + ) penalty_payment_date = latest_penalty_payment[0].date if latest_penalty_payment else None rows.append({ From de263182ad43bdfdd73a56190d18fda4f96fbb19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sat, 21 Mar 2026 17:10:47 +0800 Subject: [PATCH 09/16] =?UTF-8?q?=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yuthon_report/models/receivable_flow_report.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yuthon_report/models/receivable_flow_report.py b/yuthon_report/models/receivable_flow_report.py index 37e7515fc..87e16a40a 100644 --- a/yuthon_report/models/receivable_flow_report.py +++ b/yuthon_report/models/receivable_flow_report.py @@ -44,12 +44,12 @@ class ReceivableFlowReport(models.Model): if company_id: domain.append(('company_id', '=', company_id)) - receivables = self.search(domain, order="due_date asc") + receivables = self.sudo().search(domain, order="due_date asc") rec_ids = receivables.ids # ===== 批量预加载,避免N+1查询 ===== - # 1. 批量查收款明细行 - all_modify_lines = self.env['receivable.modify.line'].search([ + # 1. 批量查收款明细行(sudo跳过多公司权限) + all_modify_lines = self.env['receivable.modify.line'].sudo().search([ ('receivable_id', 'in', rec_ids), ('reconciliation_date', '!=', False), ]) @@ -58,8 +58,8 @@ class ReceivableFlowReport(models.Model): for ml in all_modify_lines: modify_map.setdefault(ml.receivable_id.id, []).append(ml) - # 2. 批量查违约金wizard - all_wizards = self.env['penalty.line.wizard'].search([ + # 2. 批量查违约金wizard(sudo跳过多公司权限) + all_wizards = self.env['penalty.line.wizard'].sudo().search([ ('receivable_id', 'in', rec_ids), ]) # 按 receivable_id 分组,并预取 pay_line_ids From cc06119c9ee231344f5ae8118672bf189bb799f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= <9664676+pengyuthon@user.noreply.gitee.com> Date: Sat, 21 Mar 2026 17:16:16 +0800 Subject: [PATCH 10/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/property_bills_payable.py | 10 +++++++++- .../views/inherit_yuthon_property_views.xml | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/yuthon_property_approval/models/property_bills_payable.py b/yuthon_property_approval/models/property_bills_payable.py index 9d94579aa..97dbea493 100644 --- a/yuthon_property_approval/models/property_bills_payable.py +++ b/yuthon_property_approval/models/property_bills_payable.py @@ -43,7 +43,15 @@ class PropertyBillsPayable(models.Model): def end_approval(self): self.write({'work_end': True}) management_type_id = self.env['yuthon.management.type'].search([('name', '=', '押金')], limit=1) - self.env['property.bills.list'].create({ + domain = [ + ('management_type_id', '=', management_type_id.id), + ('contract_id', '=', self.lease_contract_id.id), + ('amount', '=', self.amount_payable), + ('amount_actual', '=', self.actual_amount), + ] + existing_bill = self.env['property.bills.list'].search(domain, limit=1) + if not existing_bill: + self.env['property.bills.list'].create({ 'due_date': fields.Date.today(), 'management_type_id': management_type_id.id, 'amount': self.amount_payable, diff --git a/yuthon_property_approval/views/inherit_yuthon_property_views.xml b/yuthon_property_approval/views/inherit_yuthon_property_views.xml index 74a7c4b72..950c4a051 100644 --- a/yuthon_property_approval/views/inherit_yuthon_property_views.xml +++ b/yuthon_property_approval/views/inherit_yuthon_property_views.xml @@ -422,7 +422,7 @@ property.accounts.receivable - +