diff --git a/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py b/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py
index 0dd2cc03a..180da6232 100644
--- a/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py
+++ b/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py
@@ -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',
diff --git a/ks_dashboard_ninja/views/ks_dashboard_ninja_item_view.xml b/ks_dashboard_ninja/views/ks_dashboard_ninja_item_view.xml
index da049c03b..a6c0759a2 100644
--- a/ks_dashboard_ninja/views/ks_dashboard_ninja_item_view.xml
+++ b/ks_dashboard_ninja/views/ks_dashboard_ninja_item_view.xml
@@ -5,7 +5,8 @@
Dashboard Items
ks_dashboard_ninja.item
-
+
+
diff --git a/soong_workflow/models/soong_workflow_node.py b/soong_workflow/models/soong_workflow_node.py
index dcad702a1..0e06899e9 100644
--- a/soong_workflow/models/soong_workflow_node.py
+++ b/soong_workflow/models/soong_workflow_node.py
@@ -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):
diff --git a/soong_workflow/templates/soong_workflow_templates.xml b/soong_workflow/templates/soong_workflow_templates.xml
index cf2f0ed07..ed11c759d 100644
--- a/soong_workflow/templates/soong_workflow_templates.xml
+++ b/soong_workflow/templates/soong_workflow_templates.xml
@@ -34,8 +34,13 @@
-
+
@@ -92,8 +97,13 @@
-
+
diff --git a/soong_workflow/views/soong_workflow_node_views.xml b/soong_workflow/views/soong_workflow_node_views.xml
index 40439b58f..82f6f1de2 100644
--- a/soong_workflow/views/soong_workflow_node_views.xml
+++ b/soong_workflow/views/soong_workflow_node_views.xml
@@ -17,6 +17,8 @@
+
+
diff --git a/soong_workflow/wizard/soong_workflow_comment_wizard.py b/soong_workflow/wizard/soong_workflow_comment_wizard.py
index 77613e46b..ae59dd19d 100644
--- a/soong_workflow/wizard/soong_workflow_comment_wizard.py
+++ b/soong_workflow/wizard/soong_workflow_comment_wizard.py
@@ -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),
diff --git a/yuthon_catering/models/yuthon_catering.py b/yuthon_catering/models/yuthon_catering.py
index a4860cdf2..b18e30bf1 100644
--- a/yuthon_catering/models/yuthon_catering.py
+++ b/yuthon_catering/models/yuthon_catering.py
@@ -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
diff --git a/yuthon_catering/views/yuthon_catering.xml b/yuthon_catering/views/yuthon_catering.xml
index 81beed9a8..5f1152250 100644
--- a/yuthon_catering/views/yuthon_catering.xml
+++ b/yuthon_catering/views/yuthon_catering.xml
@@ -10,7 +10,7 @@
diff --git a/yuthon_contract/__manifest__.py b/yuthon_contract/__manifest__.py
index efa983a9b..3a6392621 100644
--- a/yuthon_contract/__manifest__.py
+++ b/yuthon_contract/__manifest__.py
@@ -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,
diff --git a/yuthon_contract/models/__init__.py b/yuthon_contract/models/__init__.py
index b02a0e56e..57079bb8a 100644
--- a/yuthon_contract/models/__init__.py
+++ b/yuthon_contract/models/__init__.py
@@ -1 +1,2 @@
from . import inherit_hr_contract
+from . import inherit_hr_employee
diff --git a/yuthon_contract/models/inherit_hr_employee.py b/yuthon_contract/models/inherit_hr_employee.py
new file mode 100644
index 000000000..9f1bd5ca1
--- /dev/null
+++ b/yuthon_contract/models/inherit_hr_employee.py
@@ -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',
+ }
diff --git a/yuthon_contract/views/inherit_hr_contract_views.xml b/yuthon_contract/views/inherit_hr_contract_views.xml
index 2e8ec5b32..a8b08f4df 100644
--- a/yuthon_contract/views/inherit_hr_contract_views.xml
+++ b/yuthon_contract/views/inherit_hr_contract_views.xml
@@ -172,4 +172,16 @@
code
action = model.action_hr_contract()
+
+
+
+ hr.employee.member.inherit.party
+ hr.employee
+
+
+
+
+
+
+
diff --git a/yuthon_contract/wizard/member_list_export_wizard_views.xml b/yuthon_contract/wizard/member_list_export_wizard_views.xml
new file mode 100644
index 000000000..be3cd503d
--- /dev/null
+++ b/yuthon_contract/wizard/member_list_export_wizard_views.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ 会员列表导出
+
+
+ code
+
+action = model.action_member_list_export()
+
+
+
diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py
index 67f7d648a..ac2b082be 100644
--- a/yuthon_hr_attendance/__manifest__.py
+++ b/yuthon_hr_attendance/__manifest__.py
@@ -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',
diff --git a/yuthon_hr_attendance/models/hr_attendance.py b/yuthon_hr_attendance/models/hr_attendance.py
index f9c6e0508..bdcdc56bb 100644
--- a/yuthon_hr_attendance/models/hr_attendance.py
+++ b/yuthon_hr_attendance/models/hr_attendance.py
@@ -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'))):
diff --git a/yuthon_hr_attendance/models/message_reminder_tasks.py b/yuthon_hr_attendance/models/message_reminder_tasks.py
index c3e8715b2..4da0b447c 100644
--- a/yuthon_hr_attendance/models/message_reminder_tasks.py
+++ b/yuthon_hr_attendance/models/message_reminder_tasks.py
@@ -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):
diff --git a/yuthon_hr_attendance/models/overtime_approval.py b/yuthon_hr_attendance/models/overtime_approval.py
index 2fc420ee1..2fb4a4be3 100644
--- a/yuthon_hr_attendance/models/overtime_approval.py
+++ b/yuthon_hr_attendance/models/overtime_approval.py
@@ -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):
diff --git a/yuthon_hr_attendance/models/yuthon_customer_planning.py b/yuthon_hr_attendance/models/yuthon_customer_planning.py
index c006ca9f7..c6db1be24 100644
--- a/yuthon_hr_attendance/models/yuthon_customer_planning.py
+++ b/yuthon_hr_attendance/models/yuthon_customer_planning.py
@@ -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'
diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
index 4ca3f374c..7d4cb2a5f 100644
--- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py
+++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
@@ -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()
diff --git a/yuthon_hr_attendance/security/ir.model.access.csv b/yuthon_hr_attendance/security/ir.model.access.csv
index b179dbd83..3259b634b 100644
--- a/yuthon_hr_attendance/security/ir.model.access.csv
+++ b/yuthon_hr_attendance/security/ir.model.access.csv
@@ -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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/yuthon_hr_attendance/static/src/js/button_customer_confirm.js b/yuthon_hr_attendance/static/src/js/button_customer_confirm.js
new file mode 100644
index 000000000..44e037c0b
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/js/button_customer_confirm.js
@@ -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",
+});
\ No newline at end of file
diff --git a/yuthon_hr_attendance/static/src/xml/button_customer_confirm.xml b/yuthon_hr_attendance/static/src/xml/button_customer_confirm.xml
new file mode 100644
index 000000000..a7d9d8e6a
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/xml/button_customer_confirm.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yuthon_hr_attendance/views/all_attendance_menu.xml b/yuthon_hr_attendance/views/all_attendance_menu.xml
index 620d0b59c..5b8dae47b 100644
--- a/yuthon_hr_attendance/views/all_attendance_menu.xml
+++ b/yuthon_hr_attendance/views/all_attendance_menu.xml
@@ -158,11 +158,6 @@
-
-
-
-
-
+
+ sequence="20"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yuthon_hr_attendance/wizard/__init__.py b/yuthon_hr_attendance/wizard/__init__.py
index 15a002755..3091dcb00 100644
--- a/yuthon_hr_attendance/wizard/__init__.py
+++ b/yuthon_hr_attendance/wizard/__init__.py
@@ -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
\ No newline at end of file
+from . import vacation_overtime_active_wizard
+from . import customer_create_wizard
\ No newline at end of file
diff --git a/yuthon_hr_attendance/wizard/customer_create_wizard.py b/yuthon_hr_attendance/wizard/customer_create_wizard.py
new file mode 100644
index 000000000..8e6260df5
--- /dev/null
+++ b/yuthon_hr_attendance/wizard/customer_create_wizard.py
@@ -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)
+
diff --git a/yuthon_hr_attendance/wizard/customer_create_wizard_views.xml b/yuthon_hr_attendance/wizard/customer_create_wizard_views.xml
new file mode 100644
index 000000000..e68827272
--- /dev/null
+++ b/yuthon_hr_attendance/wizard/customer_create_wizard_views.xml
@@ -0,0 +1,21 @@
+
+
+
+ 生成弹窗
+ customer.create.wizard
+
+
+
+
+
+
+
diff --git a/yuthon_hr_employee/models/yuthon_hr_employee.py b/yuthon_hr_employee/models/yuthon_hr_employee.py
index af39090d4..d5b0b3356 100644
--- a/yuthon_hr_employee/models/yuthon_hr_employee.py
+++ b/yuthon_hr_employee/models/yuthon_hr_employee.py
@@ -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="入职日期")
diff --git a/yuthon_hr_employee/views/yuthon_hr_employee_views.xml b/yuthon_hr_employee/views/yuthon_hr_employee_views.xml
index c7a92cd7c..3c4b2b56c 100644
--- a/yuthon_hr_employee/views/yuthon_hr_employee_views.xml
+++ b/yuthon_hr_employee/views/yuthon_hr_employee_views.xml
@@ -589,11 +589,18 @@
hr.employee.member
hr.employee
-
+
+
+
+
+
+
+
+
diff --git a/yuthon_hr_employee_approval/models/yuthon_onboard_approval.py b/yuthon_hr_employee_approval/models/yuthon_onboard_approval.py
index 1ad9fcdde..9c4a43784 100644
--- a/yuthon_hr_employee_approval/models/yuthon_onboard_approval.py
+++ b/yuthon_hr_employee_approval/models/yuthon_onboard_approval.py
@@ -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,
diff --git a/yuthon_issue/models/inherit_room_booking.py b/yuthon_issue/models/inherit_room_booking.py
index 2df2955a0..1f72f07fb 100644
--- a/yuthon_issue/models/inherit_room_booking.py
+++ b/yuthon_issue/models/inherit_room_booking.py
@@ -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',
diff --git a/yuthon_issue/views/issue_meeting_minutes_views.xml b/yuthon_issue/views/issue_meeting_minutes_views.xml
index f8a7e97b9..8e2b70a7d 100644
--- a/yuthon_issue/views/issue_meeting_minutes_views.xml
+++ b/yuthon_issue/views/issue_meeting_minutes_views.xml
@@ -4,7 +4,7 @@
issue.meeting.minutes.tree
issue.meeting.minutes
-
+
diff --git a/yuthon_notice/models/yuthon_notice.py b/yuthon_notice/models/yuthon_notice.py
index e77ceb0c8..02ee5b840 100644
--- a/yuthon_notice/models/yuthon_notice.py
+++ b/yuthon_notice/models/yuthon_notice.py
@@ -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,
})
diff --git a/yuthon_notice/views/yuthon_notice_views.xml b/yuthon_notice/views/yuthon_notice_views.xml
index a931a9c7d..3c676a695 100644
--- a/yuthon_notice/views/yuthon_notice_views.xml
+++ b/yuthon_notice/views/yuthon_notice_views.xml
@@ -6,7 +6,7 @@
+ decoration-muted="notice_state == 'stop'" default_order="create_date1 desc">
@@ -84,20 +84,23 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -122,7 +125,7 @@
+ js_class="custom_notice_tree_button" default_order="create_date1 desc">
diff --git a/yuthon_property/wizard/batch_invoice_export_wizard.py b/yuthon_property/wizard/batch_invoice_export_wizard.py
index 5dae658fa..d268a6b40 100644
--- a/yuthon_property/wizard/batch_invoice_export_wizard.py
+++ b/yuthon_property/wizard/batch_invoice_export_wizard.py
@@ -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',
diff --git a/yuthon_property_approval/models/inherit_yuthon_property.py b/yuthon_property_approval/models/inherit_yuthon_property.py
index f0181a29e..5e6bbd195 100644
--- a/yuthon_property_approval/models/inherit_yuthon_property.py
+++ b/yuthon_property_approval/models/inherit_yuthon_property.py
@@ -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):
diff --git a/yuthon_property_approval/models/property_bills_list.py b/yuthon_property_approval/models/property_bills_list.py
index 6ec98bd4b..dbc315191 100644
--- a/yuthon_property_approval/models/property_bills_list.py
+++ b/yuthon_property_approval/models/property_bills_list.py
@@ -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(
diff --git a/yuthon_property_approval/models/property_lease_renewal.py b/yuthon_property_approval/models/property_lease_renewal.py
index e552303f2..27f1c5094 100644
--- a/yuthon_property_approval/models/property_lease_renewal.py
+++ b/yuthon_property_approval/models/property_lease_renewal.py
@@ -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,
diff --git a/yuthon_property_approval/views/property_bills_list_views.xml b/yuthon_property_approval/views/property_bills_list_views.xml
index 49a89742b..665911a3d 100644
--- a/yuthon_property_approval/views/property_bills_list_views.xml
+++ b/yuthon_property_approval/views/property_bills_list_views.xml
@@ -4,7 +4,7 @@
property.bills.list.tree
property.bills.list
-
+
@@ -24,10 +24,25 @@
+
+ property.bills.list.search
+ property.bills.list
+
+
+
+
+
+
+
+
+
+
+
应收账单
property.bills.list
- tree
+ tree,search
+ {'search_default_group_pay_state': 1}
更新支付公司
@@ -36,6 +51,13 @@
code
action = model.update_lessor_id()
+
+ 一次性
+
+
+ code
+ action = model.update_pay_state()
+
diff --git a/yuthon_property_approval/wizard/property_bills_wizard.py b/yuthon_property_approval/wizard/property_bills_wizard.py
index 38b4f4b93..645f5fd9e 100644
--- a/yuthon_property_approval/wizard/property_bills_wizard.py
+++ b/yuthon_property_approval/wizard/property_bills_wizard.py
@@ -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)
])
diff --git a/yuthon_purchase/models/inherit_purchase_order.py b/yuthon_purchase/models/inherit_purchase_order.py
index 9d9ebcd6e..c9e524012 100644
--- a/yuthon_purchase/models/inherit_purchase_order.py
+++ b/yuthon_purchase/models/inherit_purchase_order.py
@@ -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'
diff --git a/yuthon_purchase/report/purchase_order_print_templates.xml b/yuthon_purchase/report/purchase_order_print_templates.xml
index 111fe1258..b66a08c8d 100644
--- a/yuthon_purchase/report/purchase_order_print_templates.xml
+++ b/yuthon_purchase/report/purchase_order_print_templates.xml
@@ -72,24 +72,31 @@
采购明细
- | 产品 |
- 规格 |
- 数量 |
- 单位 |
- 单价 |
- 金额 |
+ 产品 |
+ 品牌/规格 |
+ 描述 |
+ 数量 |
+ 计量单位 |
+ 包装数量 |
+ 包装 |
+ 单价 |
+ 税项 |
+ 金额 |
-
-
-
- |
- |
- |
- |
- |
- |
-
-
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ , |
+ |
+
diff --git a/yuthon_salary/models/yuthon_salary_active.py b/yuthon_salary/models/yuthon_salary_active.py
index ce9cb97b7..093914bc0 100644
--- a/yuthon_salary/models/yuthon_salary_active.py
+++ b/yuthon_salary/models/yuthon_salary_active.py
@@ -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",)
diff --git a/yuthon_salary/views/yuthon_salary_active_views.xml b/yuthon_salary/views/yuthon_salary_active_views.xml
index 6e1a5a69f..6dd733b1c 100644
--- a/yuthon_salary/views/yuthon_salary_active_views.xml
+++ b/yuthon_salary/views/yuthon_salary_active_views.xml
@@ -70,6 +70,7 @@
+
diff --git a/yuthon_stock/__init__.py b/yuthon_stock/__init__.py
index 9a7e03ede..c536983e2 100644
--- a/yuthon_stock/__init__.py
+++ b/yuthon_stock/__init__.py
@@ -1 +1,2 @@
-from . import models
\ No newline at end of file
+from . import models
+from . import wizard
\ No newline at end of file
diff --git a/yuthon_stock/__manifest__.py b/yuthon_stock/__manifest__.py
index 11b566af9..6dfce5be3 100644
--- a/yuthon_stock/__manifest__.py
+++ b/yuthon_stock/__manifest__.py
@@ -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',
diff --git a/yuthon_stock/models/inherit_stock_picking.py b/yuthon_stock/models/inherit_stock_picking.py
index 3f66a913b..04f357e4d 100644
--- a/yuthon_stock/models/inherit_stock_picking.py
+++ b/yuthon_stock/models/inherit_stock_picking.py
@@ -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+年月+三位序号 格式的编号,按月重置"""
diff --git a/yuthon_stock/security/ir.model.access.csv b/yuthon_stock/security/ir.model.access.csv
index 97dd8b917..2b54aa105 100644
--- a/yuthon_stock/security/ir.model.access.csv
+++ b/yuthon_stock/security/ir.model.access.csv
@@ -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
diff --git a/yuthon_stock/views/inherit_stock_picking.xml b/yuthon_stock/views/inherit_stock_picking.xml
index 2c610fff8..b7976d24e 100644
--- a/yuthon_stock/views/inherit_stock_picking.xml
+++ b/yuthon_stock/views/inherit_stock_picking.xml
@@ -25,16 +25,37 @@
+
+
+
{'no_create': True}
[('id', 'in', parent.available_product_ids)]
-
-
-
-
-
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
@@ -55,7 +76,7 @@
证明
- receipt_status != 'certification' or not can_certify
+ receipt_status != 'certification' or certifier_user_id == False or certifier_user_id != uid
1
@@ -268,6 +289,19 @@
收货
+ {'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}
+
+
+
+ inherit.view.picking.internal.search.my
+ stock.picking
+
+
+
+
+
+
diff --git a/yuthon_stock/wizard/__init__.py b/yuthon_stock/wizard/__init__.py
new file mode 100644
index 000000000..1143aed35
--- /dev/null
+++ b/yuthon_stock/wizard/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import stock_picking_transfer_wizard
diff --git a/yuthon_stock/wizard/stock_picking_transfer_wizard.py b/yuthon_stock/wizard/stock_picking_transfer_wizard.py
new file mode 100644
index 000000000..1cdb42b23
--- /dev/null
+++ b/yuthon_stock/wizard/stock_picking_transfer_wizard.py
@@ -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'}
diff --git a/yuthon_stock/wizard/stock_picking_transfer_wizard_views.xml b/yuthon_stock/wizard/stock_picking_transfer_wizard_views.xml
new file mode 100644
index 000000000..9894b5e52
--- /dev/null
+++ b/yuthon_stock/wizard/stock_picking_transfer_wizard_views.xml
@@ -0,0 +1,20 @@
+
+
+
+ stock.picking.transfer.wizard.form
+ stock.picking.transfer.wizard
+
+
+
+
+
diff --git a/yuthon_will_task/models/yuthon_will_task.py b/yuthon_will_task/models/yuthon_will_task.py
index d3a2c49d6..77a3d5955 100644
--- a/yuthon_will_task/models/yuthon_will_task.py
+++ b/yuthon_will_task/models/yuthon_will_task.py
@@ -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 ''
diff --git a/yuthon_will_task/security/ir.model.access.csv b/yuthon_will_task/security/ir.model.access.csv
index 4914348d6..2de527aff 100644
--- a/yuthon_will_task/security/ir.model.access.csv
+++ b/yuthon_will_task/security/ir.model.access.csv
@@ -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
diff --git a/yuthon_will_task/views/yuthon_will_task_views.xml b/yuthon_will_task/views/yuthon_will_task_views.xml
index 86b3a04f8..861baabbe 100644
--- a/yuthon_will_task/views/yuthon_will_task_views.xml
+++ b/yuthon_will_task/views/yuthon_will_task_views.xml
@@ -239,6 +239,9 @@
+
+
+