Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
pengyuthon
2026-04-26 13:45:38 +08:00
57 changed files with 964 additions and 381 deletions
@@ -195,7 +195,9 @@ def ks_time_addition(self, gb, query):
class KsDashboardNinjaItems(models.Model):
_name = 'ks_dashboard_ninja.item'
_description = 'Dashboard Ninja items'
_order = 'sequence asc'
sequence = fields.Char(string='序号', store=True)
name = fields.Char(string="Name", translate=True, help="The item will be represented by this unique name.")
ks_info = fields.Text(string="Item Description", translate=True)
ks_model_id = fields.Many2one('ir.model', string='Model',
@@ -5,7 +5,8 @@
<field name="name">Dashboard Items</field>
<field name="model">ks_dashboard_ninja.item</field>
<field name="arch" type="xml">
<tree create="false">
<tree create="false" editable="bottom">
<field name="sequence"/>
<field name="id" optional="show"/>
<field name="name" optional="show"/>
<field name="ks_dashboard_ninja_board_id" optional="show" string="Dashboard"/>
@@ -102,6 +102,12 @@ class SoongWorkflowNode(models.Model):
approval_concatenation = fields.Selection([('chuan', '串申'), ('bing', '并申')], string="审批形式", default="bing",
help='并:任何人均给意见,最后一个完成的人转交到下一节点.'
'串:都会收到消息,但任意一个人转交后,即到下一节点,其他人将不再显示,相当于收回.')
same_user_skip_type = fields.Selection([
('none', '不跳过'),
('next', '跳过下一个相同人员节点'),
('prev', '跳过上一个相同人员节点'),
], string='相同人员节点处理', default='none')
is_mandatory_node = fields.Boolean(string="必经节点", default=False)
@api.onchange("is_apostille")
def onchange_is_apostille(self):
@@ -34,8 +34,13 @@
<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"/>
<field name="workflow_record_id" domain="[('member_of_department', '=', True), ('approvals_group_id.name', 'not in', ['考勤', '人事'])]"
required="1" options="{'no_open': True}" context="{'tree_view_ref': 'soong_workflow.soong_workflow_record_no_action_tree'}"/>
<field name="workflow_record_id"
domain="['|',
('member_of_department', '=', True),
('users_ids', 'in', [uid])]"
required="1"
options="{'no_open': True}"
context="{'tree_view_ref': 'soong_workflow.soong_workflow_record_no_action_tree'}"/>
<field name="code" column_invisible="1"/>
<field name="workflow_id"/>
<field name="create_uid"/>
@@ -92,8 +97,13 @@
<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"/>
<field name="workflow_record_id" domain="[('member_of_department', '=', True), ('approvals_group_id.name', 'not in', ['考勤', '人事'])]"
required="1" options="{'no_open': True}" context="{'tree_view_ref': 'soong_workflow.soong_workflow_record_no_action_tree'}"/>
<field name="workflow_record_id"
domain="['|',
('member_of_department', '=', True),
('users_ids', 'in', [uid])]"
required="1"
options="{'no_open': True}"
context="{'tree_view_ref': 'soong_workflow.soong_workflow_record_no_action_tree'}"/>
<field name="code" column_invisible="1"/>
<field name="workflow_id"/>
<field name="create_uid"/>
@@ -17,6 +17,8 @@
<field name="return_comment_id"/>
<field name="confirm_comment_id"/>
<field name="end_comment_id"/>
<field name="is_mandatory_node"/>
<field name="same_user_skip_type"/>
<field name="illustrate"/>
</group>
<group>
@@ -39,9 +39,9 @@ class SoongWorkflowCommentWizard(models.TransientModel):
return_link_id = fields.Many2one('trans.link', string='退回节点', ondelete='set null')
invalid_link_ids = fields.Many2many(
'trans.link',
string='审批人节点,跳过节点',
string='跳过相同人员',
compute='_compute_invalid_link_ids',
help='没有找到符合条件审批人的节点'
help='跳过相同人员的节点'
)
employee_trans_domain_ids = fields.Many2many('hr.employee', 'employee_trans_domain_ids_rel', string="传阅人员", store=True)
@@ -173,32 +173,88 @@ class SoongWorkflowCommentWizard(models.TransientModel):
if current_link and all_links:
sorted_all_links = all_links.sorted('sequence')
for idx, link in enumerate(sorted_all_links):
if link.id == current_link.id and link.sequence == current_link.sequence:
approvers = self._get_approvers_for_link(link, record_id)
log_records = record_id.log_ids
approved_users = log_records.filtered(lambda log: log.node_btn_name != '1' and log.done_by).mapped('done_by.employee_ids')
approved_employee_ids = approved_users.ids
employee_ids = approvers.ids
if len(approvers) > 1:
is_all_included = all(emp_id in approved_employee_ids for emp_id in employee_ids)
if is_all_included:
if idx + 1 < len(sorted_all_links):
self.trans_link_id = sorted_all_links[idx + 1]
invalid_links |= current_link
break
approvers = self._get_approvers_for_link(link, record_id)
# 如果当前审批人是 1 个人,才判断跳过
if len(approvers) == 1:
current_approver = approvers[0]
# 跳过规则 next:跳过【下一个】节点
node_id = record_id.workflow_id.node_ids.search([
('name', '=', link.name),
('workflow_id', '=', record_id.workflow_id.id)
])
if node_id.same_user_skip_type == 'next':
# 有下一个节点
if idx + 1 < len(sorted_all_links):
next_link = sorted_all_links[idx + 1]
next_approvers = self._get_approvers_for_link(next_link, record_id)
# 下一个节点也是 1 个人,且和当前是同一个
if len(next_approvers) == 1 and next_approvers[0] == current_approver:
invalid_links |= next_link # 下一个节点无效(跳过)
# 跳过规则 prev:跳过【上一个】节点
if node_id.same_user_skip_type == 'prev':
# 有上一个节点
if idx - 1 >= 0:
prev_link = sorted_all_links[idx - 1]
prev_approvers = self._get_approvers_for_link(prev_link, record_id)
# 上一个节点也是 1 个人,且和当前是同一个
if len(prev_approvers) == 1 and prev_approvers[0] == current_approver:
invalid_links |= prev_link
record.invalid_link_ids = invalid_links
@api.onchange('trans_link_id')
def onchange_trans_employee_ids(self):
for record in self:
record_id = self.env[record.res_model].browse(record.res_id) if record.res_model and record.res_id else None
ctx_links = self.env.context.get('all_workflow_links', [])
all_links = self.env['trans.link'].browse(ctx_links) if isinstance(ctx_links, (list, tuple)) else ctx_links
valid_links = all_links - record.invalid_link_ids
record.is_selection = False
record.is_this_company = True
if self.node_btn_name == '退回':
record.is_this_company = False
else:
record.is_this_company = True
if record.node_btn_id.employee_ids and record.node_btn_id.is_carbon_copy:
record.circulated_employee_ids = record.node_btn_id.employee_ids
if not record.trans_link_id:
continue
record_id = self.env[record.res_model].browse(record.res_id) if record.res_model and record.res_id else None
# 排除规则:流程退回 不限制
if record_id and record_id.workflow_id:
has_exclude_log = record_id.log_ids.filtered(lambda l: l.node_btn_name == '1')
if not has_exclude_log:
# 1. 从 valid_links 里 匹配出 当前流程会经过的【必经节点】
current_mandatory_nodes = self.env['soong.workflow.node']
for link in valid_links:
link_node = record_id.workflow_id.node_ids.search([
('name', '=', link.name),
('workflow_id', '=', record_id.workflow_id.id)
], limit=1)
if link_node and link_node.is_mandatory_node:
current_mandatory_nodes |= link_node
if current_mandatory_nodes:
# 2. 取第一个必经节点
first_mandatory = current_mandatory_nodes.sorted('sequence')[0]
# 3. 只允许选择 <= 第一个必经节点 的链接
allow_links = self.env['trans.link']
for link in valid_links:
link_node = record_id.workflow_id.node_ids.search([
('name', '=', link.name),
('workflow_id', '=', record_id.workflow_id.id)
], limit=1)
if link_node and link_node.sequence <= first_mandatory.sequence:
allow_links |= link
# 4. 校验:选了必经节点之后的 → 报错
if record.trans_link_id not in allow_links:
record.trans_link_id = False
raise UserError(f"节点【{first_mandatory.name}】是必经节点,不可跳过!")
for link in record.trans_link_id:
employee_user_role_ids = self.env['hr.employee']
if link.name == '印章管理员':
@@ -499,7 +555,7 @@ class SoongWorkflowCommentWizard(models.TransientModel):
"node_btn_name": "0",
"node_btn_id": self.node_btn_id.id,
"approval_state": "2", # 已完成
"lase_click_date": fields.Datetime.now() + timedelta(hours=8),
"lase_click_date": fields.Datetime.now(),
"comment": self.comment,
})
else:
@@ -507,7 +563,7 @@ class SoongWorkflowCommentWizard(models.TransientModel):
"comment": self.comment,
"approval_state": "2", # 已完成
'document_ids': [(6, 0, self.document_ids.ids)],
"lase_click_date": fields.Datetime.now() + timedelta(hours=8)
"lase_click_date": fields.Datetime.now()
})
if current_node_log.node_btn_name == '3':
other_node_logs = self.sudo().env["soong.workflow.log"].search(
@@ -826,10 +882,10 @@ class SoongWorkflowCommentWizard(models.TransientModel):
"node_btn_id": self.node_btn_id.id,
"approval_state": "0",
})
# self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=employee.wecom_userid,
# title=record_id._description + "传阅/抄送",
# description=description, url=url,
# btntxt="查看详情")
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', user_id=employee.wecom_userid,
title=record_id._description + "传阅/抄送",
description=description, url=url,
btntxt="查看详情")
# if not record_id.log_ids2.search([('approval_state', '=', '0')]) or record_id.log_ids2:
last_log = record_id.log_ids[-1] if record_id.log_ids else None
if last_log:
@@ -1061,6 +1117,7 @@ class SoongWorkflowCommentWizard(models.TransientModel):
if self.res_model == 'purchase.order':
record.write({'purchase_state': 'done'})
record.log_ids[-1].write({'is_active_operate': True,
'lase_click_date': fields.Datetime.now(),
'approval_state': '1'
})
workflow_record_id = self.env['soong.workflow.record'].sudo().search([('res_id', '=', record_id.id),
+9 -1
View File
@@ -14,9 +14,17 @@ class YuthonCatering(models.Model):
entry_date = fields.Date(related='employee_id.entry_date', string="入职时间", store=True)
breakfast = fields.Boolean(string='早餐', default=False, group_operator='count_true')
lunch = fields.Boolean(string='午餐', default=False, group_operator='count_true')
breakfast_count = fields.Integer(string='早餐人数', compute='_compute_meal_count', store=True)
lunch_count = fields.Integer(string='午餐人数', compute='_compute_meal_count', store=True)
menu_day_id = fields.Many2one('yuthon.menu.setting', string='菜单')
amount = fields.Float(string='结算费用', compute='_compute_amount', store=True)
@api.depends('breakfast', 'lunch')
def _compute_meal_count(self):
for rec in self:
rec.breakfast_count = 1 if rec.breakfast else 0
rec.lunch_count = 1 if rec.lunch else 0
@api.depends('date')
def _compute_week_day(self):
week_map = {'0': '周一', '1': '周二', '2': '周三', '3': '周四', '4': '周五', '5': '周六', '6': '周日'}
@@ -395,7 +403,7 @@ class YuthonCatering(models.Model):
def fill_march4_confirmed_catering(self):
"""一次性:补3月4日缺失数据,已确认且早餐午餐全勾"""
from datetime import date as date_cls
target_date = date_cls(2026, 3, 4)
target_date = date_cls(2026, 4, 21)
employee_ids = self._get_active_conf_employees()
if not employee_ids:
return
+1 -1
View File
@@ -10,7 +10,7 @@
<header>
<button name="batch_remind_catering" string="批量提醒" type="object" class="btn btn-primary"/>
<button name="cron_fill_today_missing" string="补全并确认今日数据" type="object" class="btn btn-primary"/>
<button name="fill_march4_confirmed_catering" string="补3月4日确认数据" type="object" class="btn btn-primary"/>
<button name="fill_march4_confirmed_catering" string="补4月21日确认数据" type="object" class="btn btn-primary"/>
</header>
<field name="date"/>
<field name="week_day" readonly="1"/>
+1
View File
@@ -15,6 +15,7 @@
'security/ir.model.access.csv',
'data/contract_code_data.xml',
'wizard/yuthon_contract_wizard_views.xml',
'wizard/member_list_export_wizard_views.xml',
'views/inherit_hr_contract_views.xml',
],
'installable': True,
+1
View File
@@ -1 +1,2 @@
from . import inherit_hr_contract
from . import inherit_hr_employee
@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
from odoo.exceptions import UserError
import io
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, Border, Side, PatternFill
import base64
class HrEmployee(models.Model):
_inherit = "hr.employee"
# 从当前合同取甲方(用人单位),依赖 yuthon_contract 模块中 hr.contract.party_id 字段
party_id = fields.Many2one(
related='contract_id.party_id',
string="甲方(用人单位)",
store=True,
)
def action_member_list_export(self):
"""会员列表导出,直接下载Excel,不弹窗"""
active_ids = self.env.context.get('active_ids', self.ids)
if not active_ids:
raise UserError('请至少选择一条员工记录')
employees = self.env['hr.employee'].browse(active_ids)
workbook = Workbook()
sheet = workbook.active
sheet.title = 'sheet1'
# 设置列宽
col_widths = {1: 6, 2: 12, 3: 20, 4: 30, 5: 12, 6: 10, 7: 15}
for col, width in col_widths.items():
sheet.column_dimensions[chr(64 + col)].width = width
# 样式定义
center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
title_font = Font(name='宋体', size=14, bold=True)
header_font = Font(name='宋体', size=11, bold=True)
data_font = Font(name='宋体', size=11)
thin_border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin')
)
header_fill = PatternFill(start_color='B8CCE4', end_color='B8CCE4', fill_type='solid')
# 第1行:标题1
sheet.merge_cells('A1:B1')
sheet['A1'] = '住'
sheet['A1'].font = title_font
sheet['A1'].alignment = center_align
sheet.merge_cells('C1:E1')
sheet['C1'] = '广州市职工住院医疗互助保障计划'
sheet['C1'].font = title_font
sheet['C1'].alignment = center_align
sheet.merge_cells('F1:G1')
sheet['F1'] = '参续保人员名册'
sheet['F1'].font = title_font
sheet['F1'].alignment = center_align
# 第2行:标题2
sheet.merge_cells('C2:E2')
sheet['C2'] = '广州市职工非因工伤病残互助保障计划'
sheet['C2'].font = title_font
sheet['C2'].alignment = center_align
# 第3行:表头
headers = ['序号', '姓 名', '身 份 证 号 码', '工 作 单 位', '工人或干部', '是否外来工', '手机号码']
for col, header in enumerate(headers, start=1):
cell = sheet.cell(row=3, column=col, value=header)
cell.font = header_font
cell.alignment = center_align
cell.border = thin_border
cell.fill = header_fill
# 第4行起:数据
row = 4
for index, employee in enumerate(employees, start=1):
values = [
index,
employee.name or '',
employee.identification_id or '',
employee.party_id.party if employee.party_id else '',
employee.user_type or '',
employee.is_local_location or '',
employee.mobile_phone or '',
]
for col, value in enumerate(values, start=1):
cell = sheet.cell(row=row, column=col, value=value)
cell.font = data_font
cell.alignment = center_align
cell.border = thin_border
row += 1
output = io.BytesIO()
workbook.save(output)
output.seek(0)
file_data = base64.b64encode(output.read()).decode('utf-8')
attachment = self.env['ir.attachment'].create({
'name': '会员列表.xlsx',
'type': 'binary',
'datas': file_data,
'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
return {
'type': 'ir.actions.act_url',
'url': f'/web/content/{attachment.id}?download=true',
'target': 'self',
}
@@ -172,4 +172,16 @@
<field name="state">code</field>
<field name="code">action = model.action_hr_contract()</field>
</record>
<!-- 在会员列表视图中追加甲方(用人单位)列,party_id 字段由本模块定义 -->
<record id="inherit_employee_member_tree_party" model="ir.ui.view">
<field name="name">hr.employee.member.inherit.party</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="yuthon_hr_employee.view_employee_member_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='job_id']" position="after">
<field name="party_id" readonly="1" options='{"no_open": True}'/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 操作菜单中的 server action -->
<record id="action_member_list_export" model="ir.actions.server">
<field name="name">会员列表导出</field>
<field name="model_id" ref="hr.model_hr_employee"/>
<field name="binding_model_id" ref="hr.model_hr_employee"/>
<field name="state">code</field>
<field name="code">
action = model.action_member_list_export()
</field>
</record>
</odoo>
+3
View File
@@ -81,6 +81,7 @@
'wizard/yuthon_delete_planning_wizard_views.xml',
'wizard/attendance_controls_wizard.xml',
'wizard/attendance_correct_wizard.xml',
'wizard/customer_create_wizard_views.xml',
'wizard/vacation_put_wizard_views.xml',
'wizard/vacation_reports_wizard.xml',
'wizard/vacation_confirm_wizard_views.xml',
@@ -104,6 +105,8 @@
'yuthon_hr_attendance/static/src/xml/button_attendance_month.xml',
'yuthon_hr_attendance/static/src/js/button_detail_wechat.js',
'yuthon_hr_attendance/static/src/xml/button_detail_wechat.xml',
'yuthon_hr_attendance/static/src/js/button_customer_confirm.js',
'yuthon_hr_attendance/static/src/xml/button_customer_confirm.xml',
'yuthon_hr_attendance/static/src/components/*/*.xml',
'yuthon_hr_attendance/static/src/css/*.css',
'yuthon_hr_attendance/static/src/js/button_meal_confirm.js',
+19 -14
View File
@@ -75,23 +75,28 @@ class HrAttendance(models.Model):
@api.constrains("check_in")
def _check_in_out(self):
reminder_id = self.env['message.reminder.tasks'].search([('name', '=', self.employee_id.name), ('state', '=', 'todo'),
('type', '=', 'work'),
('current_data', '=', self.current_data)], limit=1)
if reminder_id:
reminder_id.state = 'done'
for record in self:
reminder_id = self.env['message.reminder.tasks'].search([('name', '=', record.employee_id.name), ('state', '=', 'todo'),
('type', '=', 'work'),
('current_data', '=', record.current_data)], limit=1)
if reminder_id:
reminder_id.state = 'done'
@api.constrains("check_out")
def _check_out_in(self):
if self.shift_name in ['夜班', '旧晚(旧区府晚班)']:
current_data = self.current_data + timedelta(days=1)
else:
current_data = self.current_data
reminder_id = self.env['message.reminder.tasks'].search([('name', '=', self.employee_id.name), ('state', '=', 'todo'),
('type', '=', 'offWork'),
('current_data', '=', current_data)], limit=1)
if reminder_id:
reminder_id.state = 'done'
for record in self:
if record.shift_name in ['夜班', '旧晚(旧区府晚班)']:
current_data = record.current_data + timedelta(days=1)
else:
current_data = record.current_data
reminder_id = self.env['message.reminder.tasks'].search([
('name', '=', record.employee_id.name),
('state', '=', 'todo'),
('type', '=', 'offWork'),
('current_data', '=', current_data)
], limit=1)
if reminder_id:
reminder_id.state = 'done'
def one_time(self):
for att in self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id'))):
@@ -21,14 +21,50 @@ class MessageReminderTasks(models.Model):
remake = fields.Char(string="备注")
holiday = fields.Boolean(string="假期")
def remove_duplicate_reminder_tasks(self):
tasks = self.env['message.reminder.tasks'].search([])
from collections import defaultdict
task_groups = defaultdict(list)
for task in tasks:
key = (task.name, task.current_data, task.type)
task_groups[key].append(task)
for key, task_list in task_groups.items():
if len(task_list) <= 1:
continue # 无重复,跳过
done_tasks = [t for t in task_list if t.state == 'done']
keep_task = None
if done_tasks:
keep_task = done_tasks[0]
else:
keep_task = task_list[0]
for task in task_list:
if task != keep_task:
task.unlink()
def get_reminder_tasks(self):
"""每天自动生成当日提醒任务"""
current_date = (fields.Date.today() + relativedelta(hours=8)).date()
self._generate_reminder_tasks_by_date(current_date)
def generate_weekly_reminder_tasks(self):
"""批量生成 2026-04-15 至 2026-04-21 提醒任务"""
start_date = fields.Date.from_string('2026-04-15')
end_date = fields.Date.from_string('2026-04-21')
current_date = start_date
while current_date <= end_date:
self._generate_reminder_tasks_by_date(current_date)
current_date += timedelta(days=1)
def _generate_reminder_tasks_by_date(self, current_date):
"""按指定日期生成提醒任务(每日/批量共用逻辑)"""
target_company_names = ['天河物管', '天河粮储', '集团本部']
target_companies = self.env['res.company'].search([('name', 'in', target_company_names)])
target_calendars = self.env['resource.calendar'].search([
('company_id', 'in', target_companies.ids)
])
# 获取日历关联的白名单员工
# 日历白名单员工
whitelist_employees = target_calendars.mapped('resource_user_ids').ids
condition1_employees = self.env['hr.employee'].search([
('id', 'not in', whitelist_employees),
@@ -36,42 +72,50 @@ class MessageReminderTasks(models.Model):
('user_state', '=', 'in'),
('active', '=', True)
])
# 角色:保安、工程、联安粮库
target_role_names = ['保安部', '工程部', '联安粮库']
target_roles = self.env['planning.role'].search([
('name', 'in', target_role_names)
])
default_reminder_types = ['work', 'offWork']
default_reminder_times = [
datetime.combine(current_date, datetime.strptime('00:55', '%H:%M').time()), # 时差
datetime.combine(current_date, datetime.strptime('10:00', '%H:%M').time())
]
target_roles = self.env['planning.role'].search([('name', 'in', target_role_names)])
condition2_employee_ids = []
if target_roles:
for role in target_roles:
resource_employees = role.resource_ids.mapped('employee_id')
condition2_employee_ids.extend(resource_employees)
condition2_employee_ids = list(set(condition2_employee_ids))
# 哺乳人员
breastfeeding_employees = target_calendars.mapped('breastfeeding_holiday_ids.employee_id')
breastfeeding_employee_ids = breastfeeding_employees.ids if breastfeeding_employees else []
# 获取不在白名单且是行政班次的人员
breastfeeding_employee_ids = breastfeeding_employees.ids or []
# 行政班人员(不在白名单、不在角色排班)
final_employees = condition1_employees.filtered(lambda emp: emp not in condition2_employee_ids)
task_vals_list = []
is_workday = self._process_shift_days(current_date)
template = self.env['planning.slot.template'].search(
[('role_id.name', '=', '集团行政')],
limit=1
)
# 集团行政模板
template = self.env['planning.slot.template'].search([('role_id.name', '=', '集团行政')], limit=1)
sch_check_in_time, sch_check_out_time = self.normal_clock_in_time(template, current_date)
# 默认提醒时间
default_reminder_times = [
datetime.combine(current_date, datetime.strptime('00:55', '%H:%M').time()),
datetime.combine(current_date, datetime.strptime('10:00', '%H:%M').time())
]
default_reminder_types = ['work', 'offWork']
# 1. 行政班次
if is_workday:
for employee in final_employees:
emp_name = employee.name
emp_department = employee.department_id.name
emp_company = employee.company_id.name
is_lactating = employee.id in breastfeeding_employee_ids
if is_lactating:
# 哺乳人员
reminder_times = self.get_reminder_lactating_personnel(employee, current_date, sch_check_in_time, sch_check_out_time)
reminder_times = self.get_reminder_lactating_personnel(employee, current_date, sch_check_in_time,
sch_check_out_time)
reminder_types = ['work', 'offWork']
shift_name = '行政班次(哺乳)'
else:
@@ -79,59 +123,88 @@ class MessageReminderTasks(models.Model):
reminder_types = default_reminder_types
shift_name = '行政班次'
for reminder_time, reminder_type in zip(reminder_times, reminder_types):
for rt, rtype in zip(reminder_times, reminder_types):
current_data_val = rt.date()
# ================== 加了这里:判断是否已存在 ==================
exists = self.env['message.reminder.tasks'].search([
('name', '=', emp_name),
('current_data', '=', current_data_val),
('type', '=', rtype),
], limit=1)
if exists:
continue # 已存在,跳过不创建
# ==============================================================
task_vals = {
'name': emp_name,
'current_data': reminder_time.date(),
'current_data': current_data_val,
'department': emp_department,
'company': emp_company,
'shift_name': shift_name,
'type': reminder_type,
'reminder_time': fields.Datetime.to_string(reminder_time),
'type': rtype,
'reminder_time': fields.Datetime.to_string(rt),
}
task_vals_list.append(task_vals)
# 计划行政
# 2. 计划排班班次
slot_start = datetime.combine(current_date, datetime.min.time()) - timedelta(hours=8)
slot_end = datetime.combine(current_date + timedelta(days=1), datetime.min.time()) - timedelta(hours=8)
# 查找员工对应的班次
for employee in condition2_employee_ids:
reminder_plan_times = []
reminder_plan_types = []
planning_slot = self.env['planning.slot'].search([
('resource_id.name', '=', employee.name),
('start_datetime', '>=', slot_start),
('start_datetime', '<', slot_end),
], limit=1)
template = self.env['planning.slot.template'].search([('planning_name', '=', planning_slot.planning_name)], limit=1)
if template:
sch_check_in_time, sch_check_out_time = self.normal_clock_in_time(template, current_date)
is_lactating = employee.id in breastfeeding_employee_ids
if is_lactating:
reminder_plan_times = self.get_reminder_lactating_personnel(employee.id, current_date, sch_check_in_time, sch_check_out_time)
reminder_plan_types = ['work', 'offWork']
shift_name = f"{template.planning_name}(哺乳)"
else:
reminder_plan_times.append(sch_check_in_time - timedelta(minutes=5))
reminder_plan_types.append('work')
reminder_plan_times.append(sch_check_out_time)
reminder_plan_types.append('offWork')
shift_name = template.planning_name
if not planning_slot:
continue
for reminder_time, reminder_type in zip(reminder_plan_times, reminder_plan_types):
task_plan_vals = {
'name': employee.name,
'department': employee.department_id.name,
'company': employee.company_id.name,
'shift_name': shift_name,
'current_data': (reminder_time + relativedelta(hours=8)).date(),
'type': reminder_type,
'reminder_time': fields.Datetime.to_string(reminder_time),
}
task_vals_list.append(task_plan_vals)
template = self.env['planning.slot.template'].search([
('planning_name', '=', planning_slot.planning_name)
], limit=1)
self.env['message.reminder.tasks'].create(task_vals_list)
if not template:
continue
c_in, c_out = self.normal_clock_in_time(template, current_date)
is_lactating = employee.id in breastfeeding_employee_ids
if is_lactating:
r_times = self.get_reminder_lactating_personnel(employee, current_date, c_in, c_out)
r_types = ['work', 'offWork']
shift_name = f"{template.planning_name}(哺乳)"
else:
r_times = [c_in - timedelta(minutes=5), c_out]
r_types = ['work', 'offWork']
shift_name = template.planning_name
for rt, rtype in zip(r_times, r_types):
current_data_val = (rt + timedelta(hours=8)).date()
exists = self.env['message.reminder.tasks'].search([
('name', '=', employee.name),
('current_data', '=', current_data_val),
('type', '=', rtype),
], limit=1)
if exists:
continue # 已存在,跳过不创建
task_plan_vals = {
'name': employee.name,
'department': employee.department_id.name,
'company': employee.company_id.name,
'shift_name': shift_name,
'current_data': current_data_val,
'type': rtype,
'reminder_time': fields.Datetime.to_string(rt),
}
task_vals_list.append(task_plan_vals)
# 创建数据
if task_vals_list:
self.env['message.reminder.tasks'].create(task_vals_list)
def get_reminder_lactating_personnel(self, employee, current_data, sch_check_in_time, sch_check_out_time):
@@ -66,22 +66,31 @@ class OvertimeApprovals(models.Model):
@api.constrains('start_time')
def _check_start_time(self):
for rec in self:
workflow_records = self.env['soong.workflow.record'].search([
('workflow_id.name', '=', '加班'),
('create_uid', '=', self.env.user.id),
('reclaim_state', '=', False),
])
if not workflow_records:
if not rec.employee_ids or not rec.start_time:
continue
for wf_rec in workflow_records:
if wf_rec.res_model == rec._name and wf_rec.res_id == rec.id:
current_date = (rec.start_time + timedelta(hours=8)).date()
for employee in rec.employee_ids:
if not employee.user_id:
continue
target_model = self.env[wf_rec.res_model]
target_record = target_model.browse(wf_rec.res_id)
if not target_record.exists() or not target_record.start_time:
continue
if (target_record.start_time + timedelta(hours=8)).date() == (rec.start_time + timedelta(hours=8)).date():
raise UserError('当天已有加班审批,请重新选择开始时间')
workflow_records = self.env['soong.workflow.record'].search([
('workflow_id.name', '=', '加班'),
('create_uid', '=', employee.user_id.id),
('reclaim_state', '=', False),
])
for wf_rec in workflow_records:
# 跳过自己这条记录
if wf_rec.res_model == rec._name and wf_rec.res_id == rec.id:
continue
target_model = self.env[wf_rec.res_model]
target_record = target_model.browse(wf_rec.res_id)
if not target_record.exists() or not target_record.start_time:
continue
exist_date = (target_record.start_time + timedelta(hours=8)).date()
if exist_date == current_date:
raise UserError('员工【%s】在【%s】已有加班记录,不可重复提交!' % (
employee.name,
current_date
))
for employee in rec.employee_ids:
holiday_status_id = self.env['hr.leave.type'].search([('name', '=', '补休')])
start_year = rec.start_time.date().year
@@ -172,13 +181,20 @@ class OvertimeApprovals(models.Model):
corrected_end_time = max_allowed_end_time - timedelta(hours=8)
rec.end_time = corrected_end_time
@api.depends('users_id', 'duration', 'start_time')
@api.depends('employee_ids', 'duration', 'start_time')
def _compute_overtime_hours(self):
for rec in self:
rec.overtime_hours = 0.0
if not rec.users_id or not rec.start_time:
if not rec.employee_ids or len(rec.employee_ids) != 1:
continue
now = rec.start_time # 获取当前时间(datetime类型)
employee = rec.employee_ids[0]
if not employee.user_id:
continue
if not rec.start_time:
continue
now = rec.start_time
first_datetime = datetime(now.year, now.month, 1, 0, 0, 0)
if now.month == 12:
next_month_datetime = datetime(now.year + 1, 1, 1, 0, 0, 0)
@@ -187,20 +203,20 @@ class OvertimeApprovals(models.Model):
last_datetime = next_month_datetime - timedelta(seconds=1)
workflow_records = self.env['soong.workflow.record'].search([
('workflow_id.name', '=', '加班'),
('create_uid', '=', rec.users_id.id),
('create_uid', '=', employee.user_id.id),
('approve_state', 'in', ['pending', 'end']),
('reclaim_state', '!=', True),
])
overtime_name_list = []
total_overtime = 0.0
for workflow in workflow_records:
related_model = self.env[workflow.res_model].browse(workflow.res_id)
if not related_model.exists():
continue
start_time = related_model.start_time
if first_datetime <= start_time <= last_datetime:
if related_model.duration:
total_overtime += related_model.duration
overtime_name_list.append(related_model.name)
rec.overtime_hours = total_overtime
rec.overtime_hours = total_overtime
@api.constrains('users_id')
def _check_overtime_hours(self):
@@ -5,88 +5,11 @@ from odoo.exceptions import UserError
class YuthonCustomerPlanning(models.Model):
_name = 'yuthon.customer.planning'
_description = '客户排班计划'
_description = '客服值班计划'
name = fields.Char(string='计划名称', required=True)
start_index_time = fields.Date(string='生效时间', default=fields.Date.today())
end_index_time = fields.Date(string='失效时间')
is_enable = fields.Boolean(string="启用", default=True)
plan_line_ids = fields.One2many('customer.plan.line', 'customer_plan_id', string='关联计划明细')
notice = fields.Text(string='计划说明')
select_time = fields.Selection([('1', '一月'), ('2', '二月'), ('3', '三月'), ('4', '四月'),
('5', '五月'), ('6', '六月'), ('7', '七月'), ('8', '八月'),
('9', '九月'), ('10', '十月'), ('11', '十一月'),
('12', '十二月')], string='选择月份', default=lambda self: self._get_default_month())
employee_ids = fields.Many2many('hr.employee', string="员工")
date = fields.Date(string='日期')
def auto_customer_plan(self, next_date=False):
selected_month = int(self.select_time)
year = datetime.now().year
if not next_date:
start_date = datetime(year, selected_month, 1)
# 计算结束日期(下个月的第一天)
next_month = (selected_month % 12) + 1
next_year = year + (1 if next_month == 1 else 0) # 如果是12月,年份加1
end_date = datetime(next_year, next_month, 1)
existing_slots = self.env['planning.slot'].search([
('role_id.name', '=', '客服中心'),
('start_datetime', '>=', start_date),
('start_datetime', '<=', end_date),
])
if len(existing_slots) > 0:
raise UserError(f"该月已经有排程,请选择其他时间。")
else:
next_month = (selected_month % 12) + 1
next_year = year + (1 if next_month == 1 else 0)
start_date = datetime(next_year, next_month, 1)
next_next_month = (next_month % 12) + 1
next_next_year = next_year + (1 if next_next_month == 1 else 0)
end_date = datetime(next_next_year, next_next_month, 1)
existing_slots = self.env['planning.slot'].search([
('role_id.name', '=', '客服中心'),
('start_datetime', '>=', start_date),
('start_datetime', '<=', end_date),
])
if len(existing_slots) > 0:
raise UserError(f"该月已经有排程,请选择其他时间。")
role_a = self.env['planning.role'].search([('name', '=', '客服中心')], limit=1)
role_template_a = self.env['planning.slot.template'].search([('planning_name', '=', '白班'), ('role_id', '=', role_a.id)])
role_template_b = self.env['planning.slot.template'].search([('planning_name', '=', '值班'), ('role_id', '=', role_a.id)])
current_date = start_date
while current_date < end_date:
day_of_week = current_date.weekday()
if day_of_week < 6:
if day_of_week < 5:
for employee in self.plan_line_ids:
self.create_planning_slot(employee, role_a, role_template_a, current_date)
elif day_of_week == 5:
current_employee = self.plan_line_ids[current_date.day % len(self.plan_line_ids)]
self.create_planning_slot(current_employee, role_a, role_template_b, current_date)
current_date += timedelta(days=1)
def customer_next_plan(self):
self.auto_customer_plan(next_date=True)
def create_planning_slot(self, record, role, role_template, current_date):
start_hour = int(role_template.start_time)
end_hour = int(role_template.end_time)
current_datetime = datetime.combine(current_date, time(0, 0, 0))
start_time = current_datetime.replace(hour=start_hour) + timedelta(hours=16) - timedelta(days=1)
if role_template.end_time < role_template.start_time:
end_time = current_datetime.replace(hour=end_hour) + timedelta(days=1, hours=16) - timedelta(days=1)
else:
end_time = current_datetime.replace(hour=end_hour) + timedelta(hours=16) - timedelta(days=1)
self.env['planning.slot'].create({
'resource_id': record.employee_id.resource_id.id,
'template_id': role_template.id,
'role_id': role.id,
'start_datetime': start_time,
'end_datetime': end_time,
'allocated_hours': 8,
})
def _get_default_month(self):
current_month = datetime.now().month
return str(current_month) # 返回字符串类型的月份
class CustomerPlanLine(models.Model):
_name = 'customer.plan.line'
@@ -145,6 +145,9 @@ class YuthonVacationReports(models.Model):
duty_schedule_env = self.env['duty.adjustment.schedule']
planning_slot_env = self.env['planning.slot']
# 新增:客服值班计划模型
customer_planning_env = self.env['yuthon.customer.planning']
for record in self:
if not record.employee_id or not record.vacation_date:
record.duty_ids = False
@@ -157,29 +160,32 @@ class YuthonVacationReports(models.Model):
last_day = calendar.monthrange(year, month)[1]
month_end = date(year, month, last_day)
# ====================== 原有逻辑 ======================
# 1. 删除非调休的 duty 记录
all_duty_records = duty_schedule_env.search([
('vacation_id', '=', record.id),
('duty_date', '>=', month_start),
('duty_date', '<=', month_end)
])
# 过滤出:非已调休的记录 → 批量删除
non_adjusted_duties = all_duty_records.filtered(lambda d: not d.is_adjustment)
if non_adjusted_duties:
non_adjusted_duties.unlink()
# 2. 从 planning.slot 白值生成值班记录
white_duty_slots = planning_slot_env.search([
('employee_id', '=', emp_id),
('planning_name', '=', '白值'),
('start_datetime', '>=', datetime.combine(month_start, datetime.min.time())),
('start_datetime', '<=', datetime.combine(month_end, datetime.max.time()))
])
existing_duty_dates = {
d.duty_date for d in duty_schedule_env.search([
('vacation_id', '=', record.id),
('duty_date', '>=', month_start),
('duty_date', '<=', month_end)
])
}
# 已存在的日期
existing_duty_dates = {d.duty_date for d in duty_schedule_env.search([
('vacation_id', '=', record.id),
('duty_date', '>=', month_start),
('duty_date', '<=', month_end)
])}
create_vals = []
for slot in white_duty_slots:
if not slot.start_datetime:
@@ -193,8 +199,27 @@ class YuthonVacationReports(models.Model):
'duty_date': duty_date,
'is_adjustment': False
})
# 从客服值班计划取日期
customer_planning_records = customer_planning_env.search([
('date', '>=', month_start),
('date', '<=', month_end),
('employee_ids', 'in', emp_id),
])
for plan in customer_planning_records:
duty_date = plan.date
if duty_date in existing_duty_dates:
continue
create_vals.append({
'employee_id': emp_id,
'vacation_id': record.id,
'duty_date': duty_date,
'is_adjustment': False
})
if create_vals:
duty_schedule_env.create(create_vals)
record.duty_ids = duty_schedule_env.search([
('vacation_id', '=', record.id),
('duty_date', '>=', month_start),
@@ -318,6 +343,20 @@ class YuthonVacationReports(models.Model):
self_meal = rec.meal_fee_ids.filtered(lambda o: not o.workflow_record_id and o.text == '上月额度继承')
self_meal.update_lines = rec.remain_missed_meals
add_overtime_lines = rec.overtime_ids.filtered(
lambda o: o.text != '上月额度继承' and o.update_type == 'add' and o.is_void != True)
# 计算补休新增总天数
workday_overtime_add = sum((line.workday or 0) for line in add_overtime_lines) / 8
holidays_overtime_add = sum((line.public_holidays or 0) for line in add_overtime_lines) / 8
rec.this_year1 += workday_overtime_add + holidays_overtime_add
rec.year_workday += workday_overtime_add
rec.year_holiday += holidays_overtime_add
# 计算年假新增总天数
add_leave_lines = rec.leave_ids.filtered(
lambda o: o.text != '上月额度继承' and o.update_type == 'add' and o.is_void != True)
dynamic_remainder_year = sum((line.update_lines or 0) for line in add_leave_lines) / 8
rec.dynamic_remainder_year += dynamic_remainder_year
valid_leave_lines = rec.leave_ids.filtered(
lambda l: l.workflow_record_id and l.update_type == 'reduce' and l.is_void != True
)
@@ -366,19 +405,6 @@ class YuthonVacationReports(models.Model):
rec.used_this_year1 += deduct_from_current1 # 累加到本年已休
remaining_overtime_deduct -= deduct_from_current1
add_overtime_lines = rec.overtime_ids.filtered(lambda o: o.text != '上月额度继承' and o.update_type == 'add' and o.is_void != True)
# 计算补休新增总天数
workday_overtime_add = sum((line.workday or 0) for line in add_overtime_lines) / 8
holidays_overtime_add = sum((line.public_holidays or 0) for line in add_overtime_lines) / 8
rec.this_year1 += workday_overtime_add + holidays_overtime_add
rec.year_workday += workday_overtime_add
rec.year_holiday += holidays_overtime_add
# 计算年假新增总天数
add_leave_lines = rec.leave_ids.filtered(
lambda o: o.text != '上月额度继承' and o.update_type == 'add' and o.is_void != True)
dynamic_remainder_year = sum((line.update_lines or 0) for line in add_leave_lines) / 8
rec.dynamic_remainder_year += dynamic_remainder_year
rec._compute_leave()
rec._compute_meal_fee()
rec._compute_working_overtime()
@@ -102,4 +102,5 @@ access_overtime_carryover_line,overtime.carryover.line,model_overtime_carryover_
access_yuthon_meal_line,yuthon.meal.line,model_yuthon_meal_line,base.group_user,1,1,1,1
access_vacation_meal_active_wizard,vacation.meal.active.wizard,model_vacation_meal_active_wizard,base.group_user,1,1,1,1
access_vacation_overtime_active_wizard,vacation.overtime.active.wizard,model_vacation_overtime_active_wizard,base.group_user,1,1,1,1
access_vacation_overtime_active_wizard,vacation.overtime.active.wizard,model_vacation_overtime_active_wizard,base.group_user,1,1,1,1
access_customer_create_wizard,customer.create.wizard,model_customer_create_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
102
103
104
105
106
@@ -0,0 +1,29 @@
/** @odoo-module */
import { ListController } from "@web/views/list/list_controller";
import { registry } from '@web/core/registry';
import { listView } from '@web/views/list/list_view';
export class CustomAttendanceCustomerConfirmController extends ListController {
setup() {
super.setup();
}
openCustomerWizard() {
this.actionService.doAction({
type: 'ir.actions.act_window',
res_model: 'customer.create.wizard',
name: '客服排班生成',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new', // 以弹窗形式打开
context: {},
});
}
}
registry.category("views").add("custom_customer_confirm_button", {
...listView,
Controller: CustomAttendanceCustomerConfirmController,
buttonTemplate: "customer_confirm_module.ListView.Buttons",
});
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="customer_confirm_module.ListView.Buttons" t-inherit="web.ListView.Buttons">
<xpath expr="//div[hasclass('o_list_buttons')]" position="after">
<button type="button" class="btn btn-primary" style="margin-left: 10px;" t-on-click="openCustomerWizard">
生成
</button>
</xpath>
</t>
</templates>
@@ -158,11 +158,6 @@
<!-- parent="menu_hr_attendance_customer_board"-->
<!-- action="action_planning_slot_form7"-->
<!-- sequence="10"/>-->
<!-- <menuitem id="menu_hr_attendance_customer_flow_board"-->
<!-- name="排班规则"-->
<!-- parent="menu_hr_attendance_customer_board"-->
<!-- action="yuthon_customer_planning_view_action"-->
<!-- sequence="12"/>-->
<menuitem id="menu_hr_attendance_duty_summary_menu_board"
name="粮库计划"
parent="yuthon_attendance_planning_board"
@@ -177,11 +172,16 @@
parent="menu_hr_attendance_duty_summary_menu_board"
action="view_yuthon_duty_plan_action"
sequence="5"/>
<menuitem id="menu_hr_attendance_customer_flow_board"
name="客服值班"
parent="yuthon_attendance_planning_board"
action="yuthon_customer_planning_view_action"
sequence="15"/>
<!--流程-->
<menuitem id="menu_yuthon_customer_process_board"
name="流程"
parent="yuthon_attendance_planning_board"
sequence="15"/>
sequence="20"/>
<menuitem id="menu_yuthon_shift_approval_board"
name="调替班申请"
parent="menu_yuthon_customer_process_board"
@@ -113,7 +113,7 @@
<group col="2">
<group>
<field name="is_thwg" invisible="1"/>
<field name="overtime_hours" readonly="1" invisible="overtime_hours1"/>
<field name="overtime_hours" readonly="1" invisible="overtime_hours1 or overtime_hours == 0.0"/>
<field name="overtime_hours1" invisible="not overtime_hours1" readonly="1"/>
<field name="meal" required="1" invisible="is_thwg or is_lc"/>
</group>
@@ -4,59 +4,14 @@
<field name="name">yuthon.customer.planning.tree</field>
<field name="model">yuthon.customer.planning</field>
<field name="arch" type="xml">
<tree string="排版规则">
<field name="name"/>
<field name="start_index_time"/>
<field name="end_index_time"/>
<field name="is_enable"/>
<tree string="客服值班" js_class="custom_customer_confirm_button" editable="bottom">
<field name="employee_ids" widget="many2many_tags"/>
<field name="date"/>
</tree>
</field>
</record>
<record id="view_yuthon_customer_planning_form" model="ir.ui.view">
<field name="name">yuthon.customer.planning.form</field>
<field name="model">yuthon.customer.planning</field>
<field name="arch" type="xml">
<form string="排版规则" create="0">
<header>
<button name="auto_customer_plan" type="object" string="安排值班" class="oe_highlight"/>
<button name="customer_next_plan" type="object" string="安排下个月值班" class="oe_highlight"/>
</header>
<sheet>
<div class="oe_title">
<label for="name" placeholder="请输入标题"/>
<h1>
<field name="name" required="1"/>
</h1>
</div>
<group>
<group>
<field name="notice"/>
<field name="select_time" required="1"/>
</group>
<group>
<field name="start_index_time"/>
<field name="end_index_time"/>
<field name="is_enable"/>
</group>
</group>
<notebook>
<page string="值班人员">
<field name="plan_line_ids">
<tree editable="bottom">
<field name="customer_plan_id" column_invisible="1"/>
<field name="sequence" widget="handle"/>
<field name="employee_id" required="1"
domain="[('department_id.name', '=', '客户部')]" widget="ztree_select"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="yuthon_customer_planning_view_action" model="ir.actions.act_window">
<field name="name">排版规则</field>
<field name="name">客服值班</field>
<field name="res_model">yuthon.customer.planning</field>
<field name="view_mode">tree,form</field>
</record>
@@ -619,6 +619,22 @@
<field name="text"/>
</tree>
</field>
</page>
<page string="值班明细">
<field name="duty_ids" context="{'default_employee_id': employee_id}">
<tree editable="bottom" delete="false" default_order="duty_date desc">
<field name="vacation_id" column_invisible="1"/>
<field name="employee_id" column_invisible="1"/>
<field name="company_id" column_invisible="1"/>
<field name="duty_date"/>
<field name="week_day"/>
<field name="hide_selection" string=" "/>
<field name="is_hide" column_invisible="1"/>
<field name="is_adjustment" column_invisible="1"/>
<button name="compensation" string="调休" type="object" class="btn-primary" invisible="is_adjustment or is_hide"/>
<button name="revoke" string="撤销" type="object" class="btn-danger" invisible="not is_adjustment"/>
</tree>
</field>
</page>
<page string="体检费">
<field name="medical_fee_ids" context="{'default_employee_id': employee_id}">
+2 -1
View File
@@ -14,4 +14,5 @@ from . import export_date_wizard
from . import yuthon_vacation_import_wizard
from . import overtime_change_wizard
from . import vacation_meal_active_wizard
from . import vacation_overtime_active_wizard
from . import vacation_overtime_active_wizard
from . import customer_create_wizard
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
import calendar
from odoo import models, api, fields, _
from odoo.tools.float_utils import float_round
from odoo.exceptions import ValidationError, UserError
from datetime import date
from datetime import timedelta
from datetime import datetime, time
class CustomerCreateWizard(models.TransientModel):
_name = 'customer.create.wizard'
_description = '客服排班生成弹窗'
select_month = fields.Selection([('1', '一月'), ('2', '二月'), ('3', '三月'), ('4', '四月'),
('5', '五月'), ('6', '六月'), ('7', '七月'), ('8', '八月'),
('9', '九月'), ('10', '十月'), ('11', '十一月'),
('12', '十二月')], string='月份',
default=lambda self: str(datetime.now().month))
select_year = fields.Selection(selection=[(str(i), str(i)) for i in range(2026, 2060)], string='年份',
default=lambda self: str(datetime.now().year))
def create_customer(self):
year = int(self.select_year)
month = int(self.select_month)
saturdays = []
current_date = datetime(year, month, 1)
if month == 12:
next_month = datetime(year + 1, 1, 1)
else:
next_month = datetime(year, month + 1, 1)
last_day = next_month - timedelta(days=1)
while current_date <= last_day:
if current_date.weekday() == 5:
saturdays.append(current_date.strftime('%Y-%m-%d'))
current_date += timedelta(days=1)
# 3. 批量创建值班计划
planning_vals_list = []
for date in saturdays:
planning_vals_list.append({
'date': date,
})
if planning_vals_list:
self.env['yuthon.customer.planning'].create(planning_vals_list)
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_customer_create_wizard" model="ir.ui.view">
<field name="name">生成弹窗</field>
<field name="model">customer.create.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="select_year"/>
<field name="select_month"/>
</group>
<footer>
<button name="create_customer" string="确认" type="object" class="oe_highlight"/>
<button string="返回" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -26,7 +26,7 @@ class YuthonHrEmployee(models.Model):
对原模型数据的隐藏,字段的开发和添加
"""
_inherit = "hr.employee"
_order = 'user_number asc'
_order = 'number asc'
# name = fields.CharWithPinyin(default_export_compatible=True)
user_role_ids_changed = fields.Boolean(string="xxx") # bug
@@ -78,6 +78,18 @@ class YuthonHrEmployee(models.Model):
bank_account = fields.Char(string="银行卡号")
user_location_type = fields.Selection([('formal', '本地农业'), ('labor', '本地非农业'),
('part_time', '外地农业'), ('practice', '外地非农业')], string="户籍类型")
is_local_location = fields.Char(
string="是否外地",
compute='_compute_is_local_location',
)
@api.depends('user_location_type')
def _compute_is_local_location(self):
for rec in self:
if rec.user_location_type in ('formal', 'labor'):
rec.is_local_location = '否'
else:
rec.is_local_location = '是'
duty_replacement_card = fields.Integer(string="值班补卡次数(月)")
# 工作信息
entry_date = fields.Date(string="入职日期")
@@ -589,11 +589,18 @@
<field name="name">hr.employee.member</field>
<field name="model">hr.employee</field>
<field name="arch" type="xml">
<tree string="会员列表" create="false" delete="false" editable="bottom">
<tree string="会员列表" create="false" delete="false" editable="bottom" default_order="number asc">
<field name="name" readonly="1"/>
<field name="work_phone" readonly="1"/>
<field name="department_id" readonly="1" options='{"no_open": True, "no_create": True}'/>
<field name="job_id" readonly="1" options='{"no_open": True, "no_create": True}'/>
<field name="user_type" readonly="1"/>
<field name="is_local_location" readonly="1" string="是否外地"/>
<field name="identification_id" readonly="1"/>
<field name="entry_date" readonly="1"/>
<field name="departure_date" readonly="1"/>
<field name="mobile_phone" readonly="1"/>
<field name="marital" readonly="1"/>
<field name="is_company" column_invisible="1"/>
<field name="company_id" column_invisible="1"/>
</tree>
@@ -265,8 +265,8 @@ class YuthonOnboardApproval(models.Model):
})
vacation_date = self.duration_employment.replace(day=1)
self.env['yuthon.vacation.reports'].search([('employee_id', '=', employee_id.id), ('vacation_date', '=', vacation_date)])
if not vacation_date:
report_id = self.env['yuthon.vacation.reports'].search([('employee_id', '=', employee_id.id), ('vacation_date', '=', vacation_date)])
if not report_id:
self.env['yuthon.vacation.reports'].sudo().create({
'employee_id': employee_id.id,
'vacation_date': vacation_date,
+16 -5
View File
@@ -99,11 +99,16 @@ class RoomBooking(models.Model):
meet.issue_library_id.issue_state = False
if self.code:
self.env['issue.library.approval.line'].create({
'issue_library_id': meet.issue_library_id.id,
'code': self.code,
'approval_date': fields.Datetime.now() + timedelta(hours=8),
})
exists = self.env['issue.library.approval.line'].search([
('issue_library_id', '=', meet.issue_library_id.id),
('code', '=', self.code)
], limit=1)
if not exists:
self.env['issue.library.approval.line'].create({
'issue_library_id': meet.issue_library_id.id,
'code': self.code,
'approval_date': fields.Datetime.now() + timedelta(hours=8),
})
@api.depends_context('uid', 'company')
@api.depends('company_id')
@@ -279,6 +284,12 @@ class IssueMeetingLine(models.Model):
sequence = fields.Char(string='序号', store=True)
issue_library_id = fields.Many2one('issue.library', string='议题')
issue_library_name = fields.Char(related='issue_library_id.name', string='议题')
issue_library_text = fields.Text(string='议题内容', compute='_compute_issue_library_text', store=False)
@api.depends('issue_library_id')
def _compute_issue_library_text(self):
for rec in self:
rec.issue_library_text = rec.issue_library_id.name or ''
employee_id = fields.Many2one('hr.employee', string='传达人')
designated_person_ids = fields.Many2many(
'hr.employee',
@@ -4,7 +4,7 @@
<field name="name">issue.meeting.minutes.tree</field>
<field name="model">issue.meeting.minutes</field>
<field name="arch" type="xml">
<tree string="会议纪要">
<tree string="会议纪要" default_order="meeting_date desc">
<field name="meeting_id"/>
<field name="code"/>
<field name="meeting_type_id"/>
+18 -23
View File
@@ -135,10 +135,11 @@ class YuthonNotice(models.Model):
notice.with_context(skip_constrains=True).users_ids = [(6, 0, list(all_users))]
if notice.users_ids and notice.is_change_notice:
conf_old = self.env['yuthon.confirm.users'].search([('notice_id', '=', notice.id)])
conf_old = self.env['yuthon.confirm.users'].sudo().search([('notice_id', '=', notice.id)])
old_employee_ids = conf_old.mapped('employee_id.id')
new_employee_ids = notice.users_ids.mapped('employee_ids.id')
new_employee_ids = self.env['hr.employee'].sudo().search([
('user_id', 'in', notice.users_ids.ids)
]).ids
add_employee_ids = list(set(new_employee_ids) - set(old_employee_ids))
del_employee_ids = list(set(old_employee_ids) - set(new_employee_ids))
@@ -148,11 +149,13 @@ class YuthonNotice(models.Model):
del_conf.unlink()
if add_employee_ids:
# add_users = self.users_ids.filtered(lambda u: u.employee_ids[0].id in add_employee_ids)
for user in self.users_ids:
all_employees = self.env['hr.employee'].sudo().search([
('user_id', 'in', self.users_ids.ids)
])
for employee in all_employees:
self.env['yuthon.confirm.users'].create({
'notice_id': self.id,
'employee_id': user.employee_id.id,
'employee_id': employee.id,
'notice_code': self.code,
'notice_name': self.name,
'release_date': self.create_date1,
@@ -198,9 +201,9 @@ class YuthonNotice(models.Model):
self.ensure_one()
if self.is_change_notice:
self.notice_state = 'yes'
conf_old = self.env['yuthon.confirm.users'].search([
conf_old = self.env['yuthon.confirm.users'].sudo().search([
('notice_id', '=', self.id),
('employee_id', 'in', self.users_ids.mapped('employee_ids.id'))
('employee_id.user_id', 'in', self.users_ids.ids)
])
if conf_old:
conf_old.write({'state': 'no'})
@@ -212,33 +215,30 @@ class YuthonNotice(models.Model):
url = f'https://phone.thtzjt.com/phone/Notice/{self.id}?employee_id={employee.id}'
result_data = self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='notice',
touser=user_id.employee_id.wecom_userid,
touser=employee.wecom_userid,
title="通知公告",
description=f"{self.name} 发布人: {self.users_id.name}",
description=f"{self.name} 发布人: {self.users_id.name if self.users_id else ''}",
url=url,
btntxt="详细信息"
)
msg_id = None
if result_data and isinstance(result_data, tuple) and len(result_data) >= 2:
if result_data:
res_dict = result_data[1]
if res_dict.get('errcode') == 0 and res_dict.get('errmsg') == 'ok' and 'msgid' in res_dict:
msg_id = res_dict['msgid']
msg_id = res_dict.get('msgid')
user_wecom_mapping.append({
'emp_id': employee.id,
'msg_id': msg_id
})
for mapping in user_wecom_mapping:
emp_id = mapping['emp_id']
msg_id = mapping['msg_id']
is_exist = self.env['yuthon.confirm.users'].search([
is_exist = self.env['yuthon.confirm.users'].sudo().search([
('notice_id', '=', self.id),
('notice_code', '=', self.code),
('employee_id', '=', emp_id)
], limit=1)
if not is_exist:
self.env['yuthon.confirm.users'].create({
self.env['yuthon.confirm.users'].sudo().create({
'notice_id': self.id,
'employee_id': emp_id,
'notice_code': self.code,
@@ -248,12 +248,7 @@ class YuthonNotice(models.Model):
'msg_id': msg_id,
})
else:
is_exist.write({
'notice_id': self.id,
'employee_id': emp_id,
'notice_code': self.code,
'notice_name': self.name,
'release_date': self.create_date1,
is_exist.sudo().write({
'state': 'no',
'msg_id': msg_id,
})
+19 -16
View File
@@ -6,7 +6,7 @@
<field name="arch" type="xml">
<tree string="公告管理" decoration-success="notice_state == 'yes'"
decoration-info="notice_state == 'no'"
decoration-muted="notice_state == 'stop'">
decoration-muted="notice_state == 'stop'" default_order="create_date1 desc">
<field name="name" required="1"/>
<field name="notice_type_id" string="类型" class="list_ids_width"/>
<field name="users_id" string="发布人" class="list_ids_width"/>
@@ -84,20 +84,23 @@
<group>
<field name="documents_ids" widget="preview_many2many"/>
</group>
<group col="4" string="发布范围">
<group>
<field name="department_ids" widget="many2many_tags" context="{'tree_view_ref':'yuthon_notice.view_yuthon_cs_tree'}" domain = "[('company_id', '=', company_id)] if is_this_company else []" options="{'no_create': True}"/>
</group>
<group>
<field name="users_role_group_ids" widget="many2many_tags" domain = "[('company_id', '=', company_id)] if is_this_company else []" options="{'no_create': True}"/>
</group>
<group>
<field name="users_role_ids" widget="many2many_tags" domain = "[('company_id', '=', company_id)] if is_this_company else []" options="{'no_create': True}"/>
</group>
<group>
<field name="employee_ids" widget="many2many_tags" context="{'tree_view_ref':'yuthon_notice.view_yuthon_cs_tree'}" options="{'no_create': True}" domain = "[('company_id', '=', company_id), ('user_id.name', '!=', 'Administrator')] if is_this_company else []"/>
<field name="users_ids" invisible="1" widget="many2many_tags"/>
</group>
<!--<group col="3" string="发布范围">-->
<group string="发布范围">
<field name="department_ids" widget="many2many_tags" context="{'tree_view_ref':'yuthon_notice.view_yuthon_cs_tree'}" domain="[('company_id', '=', company_id)] if is_this_company else []" options="{'no_create': True}"/>
</group>
<!--<group>-->
<!-- <field name="users_role_group_ids" widget="many2many_tags" domain="[('company_id', '=', company_id)] if is_this_company else []" options="{'no_create': True}"/>-->
<!--</group>-->
<!--</group>-->
<group>
<field name="users_role_group_ids" widget="many2many_tags" domain="[('company_id', '=', company_id)] if is_this_company else []" options="{'no_create': True}"/>
</group>
<group>
<field name="users_role_ids" widget="many2many_tags" domain="[('company_id', '=', company_id)] if is_this_company else []" options="{'no_create': True}"/>
</group>
<group>
<field name="employee_ids" widget="many2many_tags" context="{'tree_view_ref':'yuthon_notice.view_yuthon_cs_tree'}" options="{'no_create': True}" domain="[('company_id', '=', company_id), ('user_id.name', '!=', 'Administrator')] if is_this_company else []"/>
<field name="users_ids" widget="many2many_tags" invisible="1"/>
</group>
<group col="3">
<group>
@@ -122,7 +125,7 @@
<tree string="公告管理" edit="false" delete="false" create="false"
decoration-danger="is_notice_roger == 'no'"
decoration-success="is_notice_roger == 'yes'"
js_class="custom_notice_tree_button">
js_class="custom_notice_tree_button" default_order="create_date1 desc">
<field name="name"/>
<field name="notice_type_id" string="类型"/>
<field name="users_id" string="发布人" class="list_ids_width"/>
@@ -66,11 +66,13 @@ class BatchInvoiceExportWizard(models.TransientModel):
sheet['B2'] = f'{title_year}年{title_month}月'
# 统计最常见公司(用于C2)
# 统计最常见公司(用于C2,取合同出租方公司简称)
company_list = []
for record in receivable_records:
if record.contract_id and record.contract_id.company_id:
company_list.append(record.contract_id.company_id.name)
if record.contract_id and record.contract_id.lessor_id:
referred = record.contract_id.lessor_id.referred
if referred:
company_list.append(referred)
if company_list:
from collections import Counter
@@ -80,6 +82,19 @@ class BatchInvoiceExportWizard(models.TransientModel):
sheet['C2'] = most_common_company
# 统计最常见权属类型(用于文件名)
tenure_type_map = {'self': '自有', 'agency': '代管'}
tenure_list = []
for record in receivable_records:
if record.contract_id and record.contract_id.tenure_type:
tenure_list.append(record.contract_id.tenure_type)
if tenure_list:
from collections import Counter
most_common_tenure = Counter(tenure_list).most_common(1)[0][0]
tenure_type_label = tenure_type_map.get(most_common_tenure, most_common_tenure)
else:
tenure_type_label = ''
# 从第4行开始写入数据
row = 4
template_row_idx = 4
@@ -123,8 +138,8 @@ class BatchInvoiceExportWizard(models.TransientModel):
date_range = record.due_date.strftime('%Y-%m-%d')
sheet.cell(row=row, column=2, value=date_range)
# C列:金额(当期金额)
sheet.cell(row=row, column=3, value=record.original_amount or 0)
# C列:金额(当期金额 - 减免金额)
sheet.cell(row=row, column=3, value=(record.original_amount or 0) - (record.discount_amount or 0))
# D列:物业名称
sheet.cell(row=row, column=4, value=record.property_name or '')
@@ -182,7 +197,7 @@ class BatchInvoiceExportWizard(models.TransientModel):
file_data = base64.b64encode(output.read()).decode('utf-8')
attachment = self.env['ir.attachment'].create({
'name': f'批量开票_{title_year}年{title_month}月.xlsx',
'name': f'{most_common_company}_{tenure_type_label}_{title_year}年{title_month}月批量开票.xlsx',
'type': 'binary',
'datas': file_data,
'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -952,7 +952,7 @@ class PropertyAccountsReceivable(models.Model):
@api.depends('contract_id')
def _compute_property_name(self):
for record in self:
record.property_name = record.contract_id.property_name
record.property_name = record.contract_id.use_address
@api.depends('property_ids')
def _compute_title_deed(self):
@@ -19,6 +19,14 @@ class PropertyBillsList(models.Model):
workflow_record_id = fields.Many2one('soong.workflow.record', string="流程")
fee_id = fields.Many2one('property.fee.form', string="费用")
remake = fields.Text(string="备注")
pay_state = fields.Selection([('unpaid', '待付'), ('paid', '已付')], string="支付状态", default="unpaid")
def update_pay_state(self):
bills_ids = self.env["property.bills.list"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for record in bills_ids:
if record.actual_date:
record.pay_state = 'paid'
def update_lessor_id(self):
bills_ids = self.env["property.bills.list"].browse(
@@ -213,13 +213,6 @@ class PropertyLeaseRenewal(models.Model):
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
})
if not rec.is_full_refund:
penalty_amount = rec.deposit - rec.refund_amount - rec.total_receivables
if penalty_amount > 0:
@@ -292,6 +285,13 @@ class PropertyLeaseRenewal(models.Model):
('workflow_record_id', '=', workflow_record_id.id),
], limit=1)
if not existing_bank_statement:
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
})
bank_id = self.env['property.bank.statements'].sudo().create({
'start_time': start_time,
'name': bank_accounts,
@@ -4,7 +4,7 @@
<field name="name">property.bills.list.tree</field>
<field name="model">property.bills.list</field>
<field name="arch" type="xml">
<tree string="应收账单" editable="bottom">
<tree string="应收账单" editable="bottom" default_order="due_date desc">
<field name="due_date"/>
<field name="actual_date"/>
<field name="management_type_id"/>
@@ -24,10 +24,25 @@
</tree>
</field>
</record>
<record id="view_property_bills_list_search" model="ir.ui.view">
<field name="name">property.bills.list.search</field>
<field name="model">property.bills.list</field>
<field name="arch" type="xml">
<search>
<field name="contract_id"/>
<field name="property_name"/>
<field name="lessee_id"/>
<group expand="0" string="分组">
<filter string="状态" name="group_pay_state" context="{'group_by':'pay_state'}"/>
</group>
</search>
</field>
</record>
<record id="view_property_bills_list_action" model="ir.actions.act_window">
<field name="name">应收账单</field>
<field name="res_model">property.bills.list</field>
<field name="view_mode">tree</field>
<field name="view_mode">tree,search</field>
<field name="context">{'search_default_group_pay_state': 1}</field>
</record>
<record id="all_update_lessor_id" model="ir.actions.server">
<field name="name">更新支付公司</field>
@@ -36,6 +51,13 @@
<field name="state">code</field>
<field name="code">action = model.update_lessor_id()</field>
</record>
<record id="all_update_pay_state" model="ir.actions.server">
<field name="name">一次性</field>
<field name="model_id" ref="yuthon_property_approval.model_property_bills_list"/>
<field name="binding_model_id" ref="yuthon_property_approval.model_property_bills_list"/>
<field name="state">code</field>
<field name="code">action = model.update_pay_state()</field>
</record>
</odoo>
@@ -23,6 +23,7 @@ class PropertyBillsWizard(models.TransientModel):
if self.actual_date:
self.bills_list_id.actual_date = self.actual_date
self.bills_list_id.pay_state = 'paid'
receivable = self.env['property.accounts.receivable'].search([('contract_id', '=', self.bills_list_id.contract_id.id),
('management_type_id', '=', self.bills_list_i.management_type_id.id)
])
@@ -20,6 +20,7 @@ class InheritPurchaseOrder(models.Model):
code = fields.Char(string="流水号", default=lambda self: self.env['ir.sequence'].next_by_code('purchase_workflow_approval_code'))
state = fields.Selection(selection_add=[('申请人', '申请人'), ('部门经理', '部门经理'), ('工会主席', '工会主席'),
('协办', '协办'),
('财务', '财务'),
('复核', '复核'),
('分管领导', '分管领导'),
('总经理', '总经理'), ('归档', '归档')], default='申请人')
@@ -235,9 +236,9 @@ class InheritPurchaseOrder(models.Model):
def action_print_purchase_order(self):
"""打印采购审批单"""
self.ensure_one()
report_ref = self.env.ref('yuthon_purchase.action_purchase_order_print_report')
# 使用 sudo 以便打印时绕过联系人等模型的访问限制
return report_ref.report_action(self.sudo())
return report_ref.sudo().report_action(self)
class InheritPurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
@@ -72,24 +72,31 @@
<div class="section">采购明细</div>
<table class="po">
<tr class="subhead">
<td width="25%">产品</td>
<td width="20%">规格</td>
<td width="12%">数量</td>
<td width="12%">单位</td>
<td width="16%">单价</td>
<td width="15%">金额</td>
<td width="15%">产品</td>
<td width="10%">品牌/规格</td>
<td width="15%">描述</td>
<td width="8%">数量</td>
<td width="8%">计量单位</td>
<td width="8%">包装数量</td>
<td width="10%">包装</td>
<td width="10%">单价</td>
<td width="8%">税项</td>
<td width="8%">金额</td>
</tr>
<t t-if="doc.order_line">
<t t-foreach="doc.order_line" t-as="line">
<tr>
<td><span t-esc="line.product_id.name or ''"/></td>
<td><span t-esc="line.name or ''"/></td>
<td><span t-esc="line.product_qty"/></td>
<td><span t-esc="line.product_uom.name"/></td>
<td><span t-esc="'%.2f' % (line.price_unit or 0.0)"/></td>
<td><span t-esc="'%.2f' % (line.price_subtotal or 0.0)"/></td>
</tr>
</t>
<t t-foreach="doc.order_line" t-as="line">
<tr>
<td><span t-esc="line.product_id.name or ''"/></td>
<td><span t-esc="line.brand_spec or ''"/></td>
<td><span t-esc="line.name or ''"/></td>
<td><span t-esc="line.product_qty or 0"/></td>
<td><span t-esc="line.product_uom.name or ''"/></td>
<td><span t-esc="line.product_packaging_qty or ''"/></td>
<td><span t-esc="line.product_packaging_id.name or ''"/></td>
<td><span t-esc="'%.2f' % (line.price_unit or 0.0)"/></td>
<td><t t-foreach="line.taxes_id" t-as="tax"><span t-esc="tax.name"/><t t-if="not tax_last">, </t></t></td>
<td><span t-esc="'%.2f' % (line.price_subtotal or 0.0)"/></td>
</tr>
</t>
</table>
<t t-if="doc.pay_way == 'installment'">
@@ -90,6 +90,7 @@ class YuthonSalaryActive(models.Model):
one_year_sum = fields.Float(string='一次性年终奖应发合计')
one_year_tax = fields.Float(string='一次性年终奖计税')
one_year_net = fields.Float(string='一次性年终奖实发')
enterprise_promotion_bonus_2025 = fields.Float(string='2025年企业宣传奖励')
insurance = fields.Float(string='保险', compute='_compute_insurance')
@api.depends("basic_endowment_insurance", "basic_medical_insurance", "unemployment_insurance",)
@@ -70,6 +70,7 @@
<field name="balance_salary" string="2025年结余工资"/>
<field name="liquidation_salary_staff" string="2024年清算工资(职工)"/>
<field name="business_special_bonus" string="2025经营特别奖"/>
<field name="enterprise_promotion_bonus_2025" string="2025企业宣传奖励"/>
<field name="business_base" string="2025年基础年终奖"/>
<field name="one_year_sum" string="一次性年终奖应发合计"/>
<field name="one_year_tax" string="一次性年终奖计税"/>
+2 -1
View File
@@ -1 +1,2 @@
from . import models
from . import models
from . import wizard
+2
View File
@@ -9,6 +9,8 @@
'depends': ['base', 'stock', 'purchase_stock', 'documents', 'maintenance', 'funenc_json_widget'],
'data': [
'security/ir.model.access.csv',
'security/company_rule.xml',
'wizard/stock_picking_transfer_wizard_views.xml',
'views/menu.xml',
'views/inherit_stock_picking.xml',
'views/Inherit_stock_picking_type_views.xml',
+55 -9
View File
@@ -2,6 +2,7 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
import json
from lxml import etree
class StockPicking(models.Model):
@@ -24,7 +25,10 @@ class StockPicking(models.Model):
employee_id = fields.Many2one('hr.employee', string="验收人")
department_id = fields.Many2one('hr.department', string="部门")
certifier_id = fields.Many2one('res.users', string="证明人")
user_id = fields.Many2one('res.users', string="采购人员", compute="_compute_origin")
acceptance_user_id = fields.Many2one('res.users', string="指定验收人")
certifier_user_id = fields.Many2one('res.users', string="指定证明人")
is_admin = fields.Boolean(string='是否管理员', compute='_compute_is_admin')
user_id = fields.Many2one('res.users', string="采购人员", compute="_compute_origin", store=True)
receipt_status = fields.Selection([
('acceptance', '验收'),
('certification', '证明'),
@@ -65,6 +69,11 @@ class StockPicking(models.Model):
store=False
)
def _compute_is_admin(self):
is_admin = self.env.user.has_group('base.group_system')
for rec in self:
rec.is_admin = is_admin
@api.depends('user_id', 'employee_id')
def _compute_can_certify(self):
"""计算当前用户是否可以证明(不是采购人也不是验收人)"""
@@ -240,16 +249,19 @@ class StockPicking(models.Model):
record.user_id = False
def button_validate(self):
"""重写确认入库方法,改名为证明按钮"""
"""重写确认入库方法,改名为证明按鈕"""
for record in self:
if record.receipt_status == 'certification':
# 使用write方法强制写入只读字段
# 如果已指定证明人,检查当前用户是否是指定证明人
if record.certifier_user_id and record.certifier_user_id.id != self.env.user.id:
raise UserError('只有指定的证明人才能执行证明!')
certifier = record.certifier_user_id or self.env.user
# 自动填充证明人和时间,状态变为入库
record.write({
'certifier_id': self.env.user.id,
'certifier_id': certifier.id,
'certification_date': fields.Datetime.now(),
'receipt_status': 'receipt' # 状态变为入库
'receipt_status': 'receipt'
})
# 自动为标签含"固定资产"的产品创建 maintenance.equipment 记录
for move in record.move_ids_without_package:
product = move.product_id
if product and '固定资产' in product.product_tag_ids.mapped('name'):
@@ -311,15 +323,49 @@ class StockPicking(models.Model):
return True
def action_acceptance(self):
"""验收按钮:自动填充验收人和验收时间"""
"""验收按鈕:检查是否指定验收人,自动填充验收人和验收时间"""
self.ensure_one()
# 获取当前用户对应的员工记录
current_employee = self.env.user.employee_ids[:1] if self.env.user.employee_ids else False
# 如果已指定验收人,检查当前用户是否是指定验收人
if self.acceptance_user_id and self.acceptance_user_id.id != self.env.user.id:
raise UserError('只有指定的验收人才能执行验收!')
# 用指定验收人填充,没有指定则取当前用户
acceptor = self.acceptance_user_id or self.env.user
current_employee = acceptor.employee_ids[:1] if acceptor.employee_ids else False
self.acceptance_date = fields.Datetime.now()
if current_employee:
self.employee_id = current_employee.id
self.receipt_status = 'certification' # 状态变为证明
return True
def action_transfer_acceptance(self):
"""转交验收:弹出选人 wizard"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': '转交验收',
'res_model': 'stock.picking.transfer.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_picking_id': self.id,
'default_transfer_type': 'acceptance',
},
}
def action_transfer_certifier(self):
"""转交证明:弹出选人 wizard"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': '转交证明',
'res_model': 'stock.picking.transfer.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_picking_id': self.id,
'default_transfer_type': 'certifier',
},
}
def _generate_sh_sequence(self, date=None):
"""生成 SH+年月+三位序号 格式的编号,按月重置"""
@@ -1 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_picking_transfer_wizard,stock.picking.transfer.wizard,model_stock_picking_transfer_wizard,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_picking_transfer_wizard stock.picking.transfer.wizard model_stock_picking_transfer_wizard 1 1 1 1
+40 -6
View File
@@ -25,16 +25,37 @@
<field name="state" invisible="1"/>
<field name="can_certify" invisible="1"/>
<field name="trigger_auto_fill" invisible="1"/>
<field name="acceptance_user_id" invisible="1"/>
<field name="certifier_user_id" invisible="1"/>
<field name="is_admin" invisible="1"/>
</xpath>
<xpath expr="//page[@name='operations']//field[@name='move_ids_without_package']//field[@name='product_id']" position="attributes">
<attribute name="options">{'no_create': True}</attribute>
<attribute name="domain">[('id', 'in', parent.available_product_ids)]</attribute>
</xpath>
<xpath expr="//header/button[@name='%(stock.act_stock_return_picking)d']" position="after">
<button name="action_acceptance" string="验收" type="object" class="btn-primary" invisible="receipt_status != 'acceptance'"/>
<!-- <button name="action_revoke_receipt" string="撤回入库" type="object" class="btn-secondary"-->
<!-- invisible="receipt_status != 'receipt'"-->
<!-- groups="base.group_system"/>-->
<xpath expr="//button[@name='%(stock.act_stock_return_picking)d']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//button[@name='action_confirm']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//button[@name='action_cancel']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//header" position="inside">
<!-- 转交验收:管理员且尚未转交时可见 -->
<button name="action_transfer_acceptance" string="转交验收" type="object" class="btn-secondary"
invisible="receipt_status != 'acceptance' or acceptance_user_id != False"/>
<!-- 验收按钮:只有指定验收人可见,且当前用户是指定人 -->
<button name="action_acceptance" string="验收" type="object" class="btn-primary"
invisible="receipt_status != 'acceptance' or acceptance_user_id == False or acceptance_user_id != uid"/>
<!-- 转交证明:验收人才能转交,且尚未转交证明时可见 -->
<button name="action_transfer_certifier" string="转交证明" type="object" class="btn-secondary"
invisible="receipt_status != 'certification' or certifier_user_id != False or acceptance_user_id != uid"/>
<!-- 撤回入库:管理员且入库状态时可见 -->
<button name="action_revoke_receipt" string="撤回入库" type="object" class="btn-secondary"
invisible="receipt_status != 'receipt' or not is_admin"/>
</xpath>
<xpath expr="//page[@name='operations']/field[@name='move_ids_without_package']" position="after">
<group>
@@ -55,7 +76,7 @@
</xpath>
<xpath expr="//button[@name='button_validate' and @class='oe_highlight']" position="attributes">
<attribute name="string">证明</attribute>
<attribute name="invisible">receipt_status != 'certification' or not can_certify</attribute>
<attribute name="invisible">receipt_status != 'certification' or certifier_user_id == False or certifier_user_id != uid</attribute>
</xpath>
<xpath expr="//field[@name='location_dest_id'][@groups='stock.group_stock_multi_locations']" position="attributes">
<attribute name="invisible">1</attribute>
@@ -268,6 +289,19 @@
<record id="stock.action_picking_tree_incoming" model="ir.actions.act_window">
<field name="name">收货</field>
<field name="context">{'contact_display': 'partner_address', 'default_company_id': allowed_company_ids[0], 'restricted_picking_type_code': 'incoming', 'search_default_reception': 1, 'search_default_my_pickings': 1}</field>
</record>
<record id="inherit_view_picking_internal_search_my" model="ir.ui.view">
<field name="name">inherit.view.picking.internal.search.my</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_internal_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='reception']" position="after">
<filter name="my_pickings" string="我的收货"
domain="['|', '|', ('user_id', '=', uid), ('employee_id.user_id', '=', uid), ('certifier_id', '=', uid)]"/>
</xpath>
</field>
</record>
</odoo>
+2
View File
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import stock_picking_transfer_wizard
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from odoo.exceptions import UserError
class StockPickingTransferWizard(models.TransientModel):
_name = 'stock.picking.transfer.wizard'
_description = '入库单转交向导'
picking_id = fields.Many2one('stock.picking', string='入库单', required=True)
transfer_type = fields.Selection([
('acceptance', '转交验收'),
('certifier', '转交证明'),
], string='转交类型', required=True)
user_id = fields.Many2one('res.users', string='转交给', required=True,
domain=[('active', '=', True)])
def action_confirm(self):
self.ensure_one()
picking = self.picking_id
if self.transfer_type == 'acceptance':
if picking.receipt_status != 'acceptance':
raise UserError('当前入库单状态不是验收状态,无法转交验收!')
picking.write({'acceptance_user_id': self.user_id.id})
elif self.transfer_type == 'certifier':
if picking.receipt_status != 'certification':
raise UserError('当前入库单状态不是证明状态,无法转交证明!')
picking.write({'certifier_user_id': self.user_id.id})
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="stock_picking_transfer_wizard_form" model="ir.ui.view">
<field name="name">stock.picking.transfer.wizard.form</field>
<field name="model">stock.picking.transfer.wizard</field>
<field name="arch" type="xml">
<form string="转交">
<group>
<field name="picking_id" invisible="1"/>
<field name="transfer_type" invisible="1"/>
<field name="user_id" string="转交给" options="{'no_create': True}"/>
</group>
<footer>
<button name="action_confirm" string="确认转交" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
+9 -3
View File
@@ -54,7 +54,7 @@ class YuthonWillTask(models.Model):
priority = fields.Selection([('important', '重要不紧急'), ('urgent', '紧急不重要'), ('very_urgent', '重要且紧急')], string='优先级')
code = fields.Char(string="任务编号", default=lambda self: self.env['ir.sequence'].next_by_code('task_code'))
user_id = fields.Many2one('res.users', string="创建人", default=lambda self: self.env.user)
company_id = fields.Many2one(related="user_id.employee_id.company_id", string="公司", store=True)
company_id = fields.Many2one(related="employee_id.company_id", string="公司", store=True)
name = fields.Text(string='任务内容')
remarks = fields.Text(string='备注')
employee_ids_short = fields.Char(string='参与人简略', compute='_compute_employee_ids_short')
@@ -259,7 +259,10 @@ class YuthonWillTask(models.Model):
for employee_id in task.employee_ids:
url = f'https://phone.thtzjt.com/phone/Schedule/{task.yuthon_will_task_id.id}/{employee_id.user_id.id}'
request_hour_from = dict(task.yuthon_will_task_id._fields['request_hour_from'].selection).get(task.yuthon_will_task_id.request_hour_from, '') + '\n'
data_date = '开始时间:' + str(task.yuthon_will_task_id.start_date) + '--' + str(request_hour_from) + '\n'
_weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
_start = task.yuthon_will_task_id.start_date
_week_str = _weekdays[_start.weekday()] if _start else ''
data_date = '开始时间:' + str(_start) + ' ' + _week_str + '--' + str(request_hour_from) + '\n'
data_name = "事项名称: " + task.yuthon_will_task_id.name + '\n'
data_user_ids = '参与人: ' + ', '.join(will_id.employee_ids.mapped('name')) + '\n'
data_employee_id = ('司机:' + ', '.join(will_id.employee_ids2.mapped('name')) + '\n') if will_id.employee_ids2 else ''
@@ -273,7 +276,10 @@ class YuthonWillTask(models.Model):
def button_add_comment(self):
for employee_id in self.employee_ids + self.employee_ids2:
data_date = '开始时间:' + str(self.start_date) + str(self.request_hour_from) + '\n'
_weekdays2 = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
_week_str2 = _weekdays2[self.start_date.weekday()] if self.start_date else ''
_hour_from = dict(self._fields['request_hour_from'].selection).get(self.request_hour_from, '')
data_date = '开始时间:' + str(self.start_date) + ' ' + _week_str2 + '--' + _hour_from + '\n'
data_name = "事项名称: " + self.name + '\n'
data_user_ids = '参与人: ' + ', '.join(self.employee_ids.mapped('name')) + '\n'
data_employee_id = ('司机:' + ', '.join(self.employee_ids2.mapped('name')) + '\n') if self.employee_ids2 else ''
@@ -1,6 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_yuthon_task_time,yuthon_task_time,model_yuthon_task_time,yuthon_will_task.group_yuthon_will_task_user,1,1,1,1
access_yuthon_will_task,yuthon_will_task,model_yuthon_will_task,yuthon_will_task.group_yuthon_will_task_user,1,1,1,1
access_yuthon_dates_task,yuthon_dates_task,model_yuthon_dates_task,yuthon_will_task.group_yuthon_will_task_user,1,1,1,1
access_yuthon_task_time,yuthon_task_time,model_yuthon_task_time,base.group_user,1,1,1,1
access_yuthon_will_task,yuthon_will_task,model_yuthon_will_task,base.group_user,1,1,1,1
access_yuthon_dates_task,yuthon_dates_task,model_yuthon_dates_task,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_yuthon_task_time yuthon_task_time model_yuthon_task_time yuthon_will_task.group_yuthon_will_task_user base.group_user 1 1 1 1
3 access_yuthon_will_task yuthon_will_task model_yuthon_will_task yuthon_will_task.group_yuthon_will_task_user base.group_user 1 1 1 1
4 access_yuthon_dates_task yuthon_dates_task model_yuthon_dates_task yuthon_will_task.group_yuthon_will_task_user base.group_user 1 1 1 1
5
6
@@ -239,6 +239,9 @@
</div>
</group>
</group>
<group>
<field name="task_time_ids" widget="many2many_tags" required="1"/>
</group>
<group>
<field name="employee_id" domain="[('company_id', '=', company_id)]"/>
<field name="company_id" invisible="1"/>