Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -81,6 +81,7 @@ class SoongWorkflowMixin(models.AbstractModel):
|
||||
domain=lambda self: [('res_model', '=', self._name)], auto_join=True,
|
||||
string="审批明细")
|
||||
approval_sign_ids = fields.Many2many('res.users', store=False, compute="_compute_approval_sign_ids", string="加签人员")
|
||||
is_import = fields.Boolean(string='是否导入')
|
||||
is_back = fields.Boolean(string='是否允许收回', compute='_compute_is_back', store=False,)
|
||||
is_active = fields.Boolean(string='是否允许归档', compute='_compute_is_active', store=False,)
|
||||
is_confirm = fields.Boolean(string="未确认", compute='_compute_is_confirm', store=False,)
|
||||
@@ -1163,6 +1164,11 @@ class SoongWorkflowMixin(models.AbstractModel):
|
||||
end_el.set("invisible", "1")
|
||||
header_el.append(end_el)
|
||||
|
||||
import_el = etree.Element("field")
|
||||
import_el.set("name", "is_import")
|
||||
import_el.set("invisible", "1")
|
||||
header_el.append(import_el)
|
||||
|
||||
back_el = etree.Element("field")
|
||||
back_el.set("name", "is_back")
|
||||
back_el.set("invisible", "1")
|
||||
|
||||
@@ -657,22 +657,78 @@ 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
|
||||
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', 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
|
||||
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
|
||||
|
||||
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'
|
||||
|
||||
# 处理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):
|
||||
workflow_records = self.env['soong.workflow.record'].search([
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<template id="soong_workflow_log_manager">
|
||||
<notebook>
|
||||
<page string="审批详情">
|
||||
<page string="审批详情" invisible="is_import">
|
||||
<field name="log_ids" style="width:100%; margin-top: 10px;" mode="kanban,tree" context="{'kanban_view_ref':'soong_workflow.soong_workflow_log_manager_kanban'}">
|
||||
<tree editable="bottom" create="1" edit="1" delete="1">
|
||||
<field name="sequence" string="" widget="handle" groups="soong_workflow.group_soong_workflow_manager"/>
|
||||
@@ -30,7 +30,7 @@
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="关联流程">
|
||||
<page string="关联流程" invisible="is_import">
|
||||
<field name="soong_record_ids" style="width:100%; margin-top: 10px;">
|
||||
<tree create="1" edit="1" delete="1" editable="bottom" type="object">
|
||||
<button name="open_record" type="object" string="详情" class="btn-primary"/>
|
||||
@@ -61,7 +61,7 @@
|
||||
</template>
|
||||
<template id="soong_workflow_log">
|
||||
<notebook>
|
||||
<page string="审批详情">
|
||||
<page string="审批详情" invisible="is_import">
|
||||
<field name="log_ids" style="width:100%%; margin-top: 10px;" domain="[('node_btn_name', '!=', '1')]" mode="kanban,tree" context="{'kanban_view_ref':'soong_workflow.soong_workflow_log_kanban'}">
|
||||
<tree create="false" edit="false" delete="false" editable="bottom" decoration-danger="node_btn_name == '1'">
|
||||
<field name="sequence" string="" column_invisible="1"/>
|
||||
@@ -88,7 +88,7 @@
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="关联流程">
|
||||
<page string="关联流程" invisible="is_import">
|
||||
<field name="soong_record_ids" style="width:100%%; margin-top: 10px;">
|
||||
<tree editable="bottom" delete="False">
|
||||
<button name="open_record" type="object" string="详情" class="btn-primary"/>
|
||||
|
||||
@@ -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="查看详情")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
<field name="should_attendance" options='{ "fg_color": "white: should_attendance == False"}'/>
|
||||
<field name="actual_attendance" options='{ "fg_color": "white: actual_attendance == False"}'/>
|
||||
<field name="expatriation" options='{ "fg_color": "white: expatriation == False"}'/>
|
||||
<field name="go_out" options='{ "fg_color": "white: go_out == False"}'/>
|
||||
<field name="right_days" options='{ "fg_color": "white: right_days == False"}'/>
|
||||
<field name="test_days" options='{ "fg_color": "white: test_days == False"}'/>
|
||||
<field name="duty" options='{ "fg_color": "white: duty == False"}'/>
|
||||
@@ -131,6 +132,7 @@
|
||||
<field name="should_attendance" options='{ "fg_color": "white: should_attendance == False"}'/>
|
||||
<field name="actual_attendance" options='{ "fg_color": "white: actual_attendance == False"}'/>
|
||||
<field name="expatriation" options='{ "fg_color": "white: expatriation == False"}'/>
|
||||
<field name="go_out" options='{ "fg_color": "white: go_out == False"}'/>
|
||||
<field name="right_days" options='{ "fg_color": "white: right_days == False"}'/>
|
||||
<field name="test_days" options='{ "fg_color": "white: test_days == False"}'/>
|
||||
<field name="duty" options='{ "fg_color": "white: duty == False"}'/>
|
||||
|
||||
@@ -39,6 +39,7 @@ class PropertyBankStatements(models.Model):
|
||||
remake = fields.Text('备注')
|
||||
registered = fields.Float('挂账')
|
||||
is_reconciliation = fields.Boolean('是否对账')
|
||||
workflow_record_id = fields.Many2one('soong.workflow.record', string="流程")
|
||||
company_select = fields.Selection([('jhtz', '天河投资'), ('thwg', '天河物管'), ('thcj', '天河城建')])
|
||||
property_ids = fields.Many2many('yuthon.property', string='物业地址')
|
||||
bank_state = fields.Selection([('not_reconciled', '未对账'), ('reconciled', '已对账'), ('registered', '挂账'), ('other', '其他')], string='状态', default='not_reconciled')
|
||||
@@ -46,6 +47,16 @@ class PropertyBankStatements(models.Model):
|
||||
change_amount = fields.Float(string='修改金额')
|
||||
receivable_modify_ids = fields.One2many('receivable.modify.line', 'bank_id', string='修改应收单')
|
||||
|
||||
def open_record(self):
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": self.workflow_record_id.res_model,
|
||||
"res_id": self.workflow_record_id.res_id,
|
||||
"name": self.workflow_record_id.name,
|
||||
"view_mode": "form",
|
||||
}
|
||||
|
||||
|
||||
def update_company_id(self):
|
||||
records = self.env['property.bank.statements'].search([])
|
||||
for record in records:
|
||||
@@ -197,7 +208,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)
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<field name="your_own_unit"/>
|
||||
<field name="property_ids" widget="many2many_tags" column_invisible="1"/>
|
||||
<field name="bank_state" />
|
||||
<field name="workflow_record_id" column_invisible="1"/>
|
||||
<button name="open_record" string="流程" type="object" class="btn-primary" invisible="not workflow_record_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -45,6 +47,8 @@
|
||||
<field name="extract"/>
|
||||
<field name="remake"/>
|
||||
<field name="bank_state"/>
|
||||
<field name="workflow_record_id" column_invisible="1"/>
|
||||
<button name="open_record" string="流程" type="object" class="btn-primary" invisible="not workflow_record_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -146,10 +150,20 @@
|
||||
<field name="receivable_id"/>
|
||||
<field name="property_name"/>
|
||||
<field name="bank_id"/>
|
||||
<field name="management_type_id"/>
|
||||
<field name="reconciliation_date"/>
|
||||
<field name="due_date"/>
|
||||
<field name="due_date_end"/>
|
||||
<field name="payout_date"/>
|
||||
<field name="original_amount"/>
|
||||
<field name="amount"/>
|
||||
<field name="delivered1"/>
|
||||
<field name="is_penalty"/>
|
||||
<field name="bank_state"/>
|
||||
<group expand="0" string="分组">
|
||||
<filter string="应收类型" name="group_by_management_type" context="{'group_by':'management_type_id'}"/>
|
||||
<filter string="对账日期" name="group_by_reconciliation_date" context="{'group_by':'reconciliation_date'}"/>
|
||||
<filter string="银行流水" name="group_by_bank_id" context="{'group_by':'bank_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
||||
@@ -13,8 +13,18 @@ class PropertyBillsList(models.Model):
|
||||
contract_id = fields.Many2one('property.lease.contract', string="合同编号")
|
||||
lessee_id = fields.Many2one(related="contract_id.lessee_id", string="承租方", store=True)
|
||||
property_name = fields.Char(related='contract_id.property_name', string="物业信息")
|
||||
workflow_record_id = fields.Many2one('soong.workflow.record', string="流程")
|
||||
remake = fields.Text(string="备注")
|
||||
|
||||
def open_record(self):
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": self.workflow_record_id.res_model,
|
||||
"res_id": self.workflow_record_id.res_id,
|
||||
"name": self.workflow_record_id.name,
|
||||
"view_mode": "form",
|
||||
}
|
||||
|
||||
def action_pay(self):
|
||||
return {
|
||||
'name': '支付账单弹窗',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -45,6 +45,13 @@ class PropertyFeeSpending(models.Model):
|
||||
self_users_ids = fields.Many2many('res.users', string="当前经办人", default=lambda self: [(6, 0, [self.env.user.id])])
|
||||
|
||||
|
||||
def all_is_import(self):
|
||||
spending_ids = self.env["property.fee.spending"].browse(
|
||||
self._context.get('active_ids', self._context.get('active_id'))
|
||||
)
|
||||
for spending in spending_ids:
|
||||
spending.is_import = True
|
||||
|
||||
def build_pop(self):
|
||||
return {
|
||||
'name': '添加物业',
|
||||
|
||||
@@ -165,6 +165,19 @@ class PropertyLeaseRenewal(models.Model):
|
||||
'due_date': rec.effective_date,
|
||||
})
|
||||
elif rec.renewal_type == 'early_checkout':
|
||||
lessor_id = rec.contract_id.lessor_id
|
||||
company_id = self.env['res.company'].search([('name', '=', '集团本部')], limit=1)
|
||||
if lessor_id.referred in ['天河投资', '天投城建']:
|
||||
company_id = self.env['res.company'].search([('name', '=', '集团本部')], limit=1)
|
||||
elif lessor_id.referred in ['天河物管']:
|
||||
company_id = self.env['res.company'].search([('name', '=', '天河物管')], limit=1)
|
||||
elif lessor_id.referred in ['天河粮储']:
|
||||
company_id = self.env['res.company'].search([('name', '=', '天河粮储')], limit=1)
|
||||
|
||||
bank_accounts = self.env['property.bank.line'].search([
|
||||
('tenant_id', '=', rec.lessee_id.id)
|
||||
]).mapped('bank')
|
||||
bank_accounts = [acc for acc in bank_accounts if acc][0]
|
||||
rec.contract_id.write({
|
||||
'actual_end_time': rec.early_quitting_date
|
||||
})
|
||||
@@ -197,6 +210,8 @@ class PropertyLeaseRenewal(models.Model):
|
||||
[('name', '=', '押金')],
|
||||
limit=1
|
||||
)
|
||||
workflow_record_id = self.env["soong.workflow.record"].search(
|
||||
[("res_model", "=", self._name), ("res_id", "=", self.id)])
|
||||
deposit_amount = rec.deposit if rec.is_full_refund else rec.refund_amount
|
||||
if deposit_amount > 0:
|
||||
bills_line_list.append({
|
||||
@@ -204,17 +219,33 @@ class PropertyLeaseRenewal(models.Model):
|
||||
'management_type_id': deposit_management_type.id,
|
||||
'amount': deposit_amount,
|
||||
'contract_id': rec.contract_id.id,
|
||||
'remake': rec.refund_remake
|
||||
'remake': '押转租/没收押金',
|
||||
'workflow_record_id': workflow_record_id.id,
|
||||
})
|
||||
|
||||
if not rec.is_full_refund:
|
||||
start_time = datetime.combine(rec.early_quitting_date, datetime.min.time().replace(hour=8, minute=00))
|
||||
self.env['property.bank.statements'].sudo().create({
|
||||
'start_time': start_time,
|
||||
'original_amount': rec.balance,
|
||||
'name_account': rec.lessee_id.name,
|
||||
'remake': rec.refund_remake,
|
||||
})
|
||||
existing_bank_statement = self.env['property.bank.statements'].sudo().search([
|
||||
('start_time', '=', start_time),
|
||||
('original_amount', '=', rec.balance),
|
||||
('name_account', '=', rec.lessee_id.name),
|
||||
('remake', '=', '押转租/没收押金'),
|
||||
('workflow_record_id', '=', workflow_record_id.id),
|
||||
], limit=1)
|
||||
if not existing_bank_statement:
|
||||
bank_id = self.env['property.bank.statements'].sudo().create({
|
||||
'start_time': start_time,
|
||||
'name': bank_accounts,
|
||||
'original_amount': rec.balance,
|
||||
'name_account': rec.lessee_id.name,
|
||||
'remake': '押转租/没收押金',
|
||||
'lending_direction': 'loan',
|
||||
'workflow_record_id': workflow_record_id.id,
|
||||
'company_id': company_id.id,
|
||||
'your_own_unit': lessor_id.referred,
|
||||
'your_own_account': lessor_id.rent_account,
|
||||
})
|
||||
bank_id._compute_property_employee_id()
|
||||
|
||||
# 创建应付账单
|
||||
if bills_line_list:
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
<field name="model">property.accounts.receivable</field>
|
||||
<field name="inherit_id" ref="yuthon_property.view_property_accounts_receivable_tree1"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='has_date']" position="before">
|
||||
<xpath expr="//field[@name='has_date']" position="after">
|
||||
<button name="verification_wizard" string="核销" type="object" class="btn btn-primary"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='remark']" position="after">
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
<field name="property_name"/>
|
||||
<field name="lessee_id"/>
|
||||
<field name="remake"/>
|
||||
<field name="workflow_record_id" column_invisible="1"/>
|
||||
<button name="action_pay" string="核销" type="object" class="btn-primary" invisible="actual_date"/>
|
||||
<button name="open_record" string="流程" type="object" class="btn-primary" invisible="not workflow_record_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -109,6 +109,13 @@
|
||||
<field name="res_model">property.fee.spending</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<record id="select_is_import" model="ir.actions.server">
|
||||
<field name="name">一次性</field>
|
||||
<field name="model_id" ref="yuthon_property_approval.model_property_fee_spending"/>
|
||||
<field name="binding_model_id" ref="yuthon_property_approval.model_property_fee_spending"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = model.all_is_import()</field>
|
||||
</record>
|
||||
<record id="view_yuthon_expenses_type_tree" model="ir.ui.view">
|
||||
<field name="name">yuthon.expenses.type.tree</field>
|
||||
<field name="model">yuthon.expenses.type</field>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
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')
|
||||
property_id = params.get('property_id')
|
||||
lessee_id = params.get('lessee_id')
|
||||
contract_id = params.get('contract_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))
|
||||
if property_id:
|
||||
domain.append(('property_ids', 'in', [property_id]))
|
||||
if lessee_id:
|
||||
domain.append(('lessee_id', '=', lessee_id))
|
||||
if contract_id:
|
||||
domain.append(('contract_id', '=', contract_id))
|
||||
|
||||
receivables = self.sudo().search(domain, order="id asc")
|
||||
rec_ids = receivables.ids
|
||||
|
||||
# ===== 批量预加载,避免N+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]}
|
||||
for ml in all_modify_lines:
|
||||
modify_map.setdefault(ml.receivable_id.id, []).append(ml)
|
||||
|
||||
# 2. 批量查违约金wizard(sudo跳过多公司权限)
|
||||
all_wizards = self.env['penalty.line.wizard'].sudo().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 = modify_map.get(rec.id, [])
|
||||
|
||||
# 计算收款总金额
|
||||
total_collected = sum(line.amount for line in modify_lines)
|
||||
|
||||
# 获取最新的收款时间和备注
|
||||
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 ""
|
||||
|
||||
# 获取收款金额(最近一次)
|
||||
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
|
||||
|
||||
# 获取违约金收款信息(从预加载字典取)
|
||||
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 = 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({
|
||||
"序号": idx,
|
||||
"租金id": rec.id,
|
||||
"应收日期": rec.due_date.strftime("%Y-%m-%d") if rec.due_date else "",
|
||||
"应收金额": {
|
||||
'model': 'property.accounts.receivable',
|
||||
'display_name': str(round(rec.original_amount, 2)),
|
||||
'view_mode': 'tree',
|
||||
'domain': [('id', '=', rec.id)],
|
||||
},
|
||||
"收款总金额": {
|
||||
'model': 'receivable.modify.line',
|
||||
'display_name': str(round(total_collected, 2)),
|
||||
'view_mode': 'tree',
|
||||
'domain': [('receivable_id', '=', rec.id), ('reconciliation_date', '!=', False)],
|
||||
},
|
||||
"未收": 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,
|
||||
"违约金金额": {
|
||||
'model': 'property.accounts.receivable',
|
||||
'display_name': str(round(penalty_amount, 2)),
|
||||
'view_mode': 'tree',
|
||||
'domain': [('id', '=', rec.id)],
|
||||
},
|
||||
"违约金收款时间": 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 "",
|
||||
"物业名称": {
|
||||
'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 or "",
|
||||
} 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 "",
|
||||
})
|
||||
|
||||
stats = [
|
||||
{'name': '记录总数', 'amount': len(rows)},
|
||||
{'name': '应收总额', 'amount': round(sum(float(row["应收金额"]['display_name']) for row in rows), 2)},
|
||||
{'name': '已收总额', 'amount': round(sum(float(row["收款总金额"]['display_name']) for row in rows), 2)},
|
||||
{'name': '未收总额', 'amount': round(sum(row["未收"] for row in rows), 2)},
|
||||
]
|
||||
|
||||
return {"rows": rows, "stats": stats}
|
||||
@@ -33,6 +33,9 @@ class ReceivableReport extends Component {
|
||||
selectPartner: {},
|
||||
selectCompany: {},
|
||||
selectManagementType: {},
|
||||
selectProperty: {},
|
||||
selectLessee: {},
|
||||
selectContract: {},
|
||||
condition: false,
|
||||
conditions: [
|
||||
{'name': '', 'value': ''},
|
||||
@@ -142,13 +145,15 @@ class ReceivableReport extends Component {
|
||||
* 穿透渲染器:将对象格式的字段渲染为可点击的链接
|
||||
* 如果字段值是对象(包含 model、id、display_name),则渲染为可点击链接
|
||||
* 否则直接显示原值
|
||||
* 支持 view_mode: 'tree' + domain 打开 tree 弹窗
|
||||
* 支持 view_mode: 'form' 或默认打开 form 弹窗
|
||||
*/
|
||||
linkRender(params) {
|
||||
if (!params.value) return '';
|
||||
|
||||
// 如果是对象格式(包含 model 和 id),渲染为可点击链接
|
||||
if (typeof params.value === 'object' && params.value.model && params.value.id) {
|
||||
const { model, id, display_name } = params.value;
|
||||
// 如果是对象格式(包含 model),渲染为可点击链接
|
||||
if (typeof params.value === 'object' && params.value.model) {
|
||||
const { model, id, display_name, view_mode, domain, view_id } = params.value;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'o_record_cell';
|
||||
@@ -156,13 +161,25 @@ class ReceivableReport extends Component {
|
||||
el.classList.add('btn');
|
||||
el.classList.add('btn-link');
|
||||
el.onclick = () => {
|
||||
params.context.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: model,
|
||||
res_id: id,
|
||||
views: [[false, 'form'], [false, 'search']],
|
||||
target: 'new'
|
||||
});
|
||||
if (view_mode === 'tree') {
|
||||
// tree 弹窗:用 domain 过滤多条记录
|
||||
params.context.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: model,
|
||||
views: [[view_id || false, 'tree']],
|
||||
domain: domain || [],
|
||||
target: 'new',
|
||||
});
|
||||
} else {
|
||||
// form 弹窗:打开单条记录
|
||||
params.context.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: model,
|
||||
res_id: id,
|
||||
views: [[false, 'form'], [false, 'search']],
|
||||
target: 'new'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return el;
|
||||
@@ -292,6 +309,9 @@ class ReceivableReport extends Component {
|
||||
partner_id: this.state.selectPartner?.id,
|
||||
company_id: this.state.selectCompany?.id,
|
||||
management_type_id: this.state.selectManagementType?.id || false,
|
||||
property_id: this.state.selectProperty?.id || false,
|
||||
lessee_id: this.state.selectLessee?.id || false,
|
||||
contract_id: this.state.selectContract?.id || false,
|
||||
};
|
||||
} else {
|
||||
let startDate = formatDate(this.state.startDate, {format: "yyyy-MM-dd"})
|
||||
@@ -304,6 +324,9 @@ class ReceivableReport extends Component {
|
||||
partner_id: this.state.selectPartner?.id,
|
||||
company_id: this.state.selectCompany?.id,
|
||||
management_type_id: this.state.selectManagementType?.id || false,
|
||||
property_id: this.state.selectProperty?.id || false,
|
||||
lessee_id: this.state.selectLessee?.id || false,
|
||||
contract_id: this.state.selectContract?.id || false,
|
||||
};
|
||||
}
|
||||
const result = await this.serviceOrm.call(
|
||||
@@ -376,6 +399,21 @@ class ReceivableReport extends Component {
|
||||
this._applyManagementTypeFilter();
|
||||
}
|
||||
|
||||
async onUpdateProperty(v) {
|
||||
this.state.selectProperty = v[0] || {};
|
||||
await this.onSearch();
|
||||
}
|
||||
|
||||
async onUpdateLessee(v) {
|
||||
this.state.selectLessee = v[0] || {};
|
||||
await this.onSearch();
|
||||
}
|
||||
|
||||
async onUpdateContract(v) {
|
||||
this.state.selectContract = v[0] || {};
|
||||
await this.onSearch();
|
||||
}
|
||||
|
||||
_applyManagementTypeFilter() {
|
||||
if (!this.grid) return;
|
||||
const typeName = this.state.selectManagementType?.display_name || '';
|
||||
|
||||
@@ -109,6 +109,45 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 物业名称 -->
|
||||
<div class="col">
|
||||
<Many2XAutocomplete
|
||||
value="state.selectProperty?.display_name"
|
||||
getDomain="getDomainCompany"
|
||||
resModel="'yuthon.property'"
|
||||
fieldString="'物业名称'"
|
||||
placeholder="'物业名称'"
|
||||
activeActions="{}"
|
||||
update.bind="onUpdateProperty"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 承租方 -->
|
||||
<div class="col">
|
||||
<Many2XAutocomplete
|
||||
value="state.selectLessee?.display_name"
|
||||
getDomain="getDomainCompany"
|
||||
resModel="'property.tenant.information'"
|
||||
fieldString="'承租方'"
|
||||
placeholder="'承租方'"
|
||||
activeActions="{}"
|
||||
update.bind="onUpdateLessee"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 合同编号 -->
|
||||
<div class="col">
|
||||
<Many2XAutocomplete
|
||||
value="state.selectContract?.display_name"
|
||||
getDomain="getDomainCompany"
|
||||
resModel="'property.lease.contract'"
|
||||
fieldString="'合同编号'"
|
||||
placeholder="'合同编号'"
|
||||
activeActions="{}"
|
||||
update.bind="onUpdateContract"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-success" t-on-click="onExportExcel">导出</button>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_receivable_flow_report" model="ir.actions.client">
|
||||
<field name="name">应收流水中间报表</field>
|
||||
<field name="tag">receivable_report_client</field>
|
||||
<field name="context">
|
||||
{
|
||||
"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.2},
|
||||
{"headerName": "物业名称", "field": "物业名称", "flex": 2, "cellRenderer": "linkRender"},
|
||||
{"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4},
|
||||
{"headerName": "应收日期", "field": "应收日期", "flex": 1},
|
||||
{"headerName": "应收金额", "field": "应收金额", "flex": 1, "type": "number", "cellRenderer": "linkRender"},
|
||||
{"headerName": "收款总金额", "field": "收款总金额", "flex": 1, "type": "number", "cellRenderer": "linkRender"},
|
||||
{"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", "cellRenderer": "linkRender"},
|
||||
{"headerName": "违约金收款时间", "field": "违约金收款时间", "flex": 1},
|
||||
{"headerName": "违约金收款金额", "field": "违约金收款金额", "flex": 1, "type": "number"}
|
||||
]
|
||||
}
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- 将报表添加到菜单 -->
|
||||
<menuitem id="menu_receivable_flow_report" name="应收流水中间报表" parent="yuthon_property_approval.property_approval_report_menu" action="action_receivable_flow_report" sequence="10"/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user