diff --git a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml index 91fe581b0..cef92211d 100644 --- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml +++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml @@ -395,7 +395,7 @@ - + diff --git a/yuthon_property_approval/views/property_open_rent_views.xml b/yuthon_property_approval/views/property_open_rent_views.xml index eda1c2229..8bcae0a47 100644 --- a/yuthon_property_approval/views/property_open_rent_views.xml +++ b/yuthon_property_approval/views/property_open_rent_views.xml @@ -37,6 +37,7 @@ + diff --git a/yuthon_salary/__manifest__.py b/yuthon_salary/__manifest__.py index 32ce24331..94eda76d7 100644 --- a/yuthon_salary/__manifest__.py +++ b/yuthon_salary/__manifest__.py @@ -28,6 +28,7 @@ 'views/yuthon_individual_tax_views.xml', 'views/yuthon_salary_asset_views.xml', 'views/salary_payroll_distribution_viwes.xml', + 'views/salary_field_config_views.xml', 'views/yuthon_salary_project_views.xml', 'views/yuthon_payroll_labor_views.xml', 'views/yuthon_salary_documents_views.xml', diff --git a/yuthon_salary/models/__init__.py b/yuthon_salary/models/__init__.py index 237ec8bea..4b68ce449 100644 --- a/yuthon_salary/models/__init__.py +++ b/yuthon_salary/models/__init__.py @@ -17,4 +17,5 @@ from . import inherit_hr_payslip_line from . import yuthon_salary_party from . import yuthon_individual_tax from . import yuthon_meal_expenses -from . import yuthon_salary_active \ No newline at end of file +from . import yuthon_salary_active +from . import salary_field_config \ No newline at end of file diff --git a/yuthon_salary/models/salary_field_config.py b/yuthon_salary/models/salary_field_config.py new file mode 100644 index 000000000..940c461dd --- /dev/null +++ b/yuthon_salary/models/salary_field_config.py @@ -0,0 +1,131 @@ +from odoo import models, fields, _ + + +class SalaryFieldConfig(models.TransientModel): + _name = 'salary.field.config' + _description = '薪资自定义字段配置' + + _dynamic_field_model = 'yuthon.salary.active' + _dynamic_field_config = 'salary.field.config' + _dynamic_field_prefix = 'x_dynamic_' + + def _get_target_model_fields(self): + fields = self.env['ir.model.fields'].search([ + ('model', '=', self._dynamic_field_model), + ('ttype', 'not in', ['many2many', 'one2many', 'reference']) + ]) + return [(f.name, f.field_description) for f in fields] + field_type = fields.Selection([ + ('char', '字符串'), + ('float', '数值(浮点)'), + ], string='字段类型', default='float', required=True) + + name = fields.Char(string='字段名称', required=True) + symbol = fields.Char(string='字段标识', index=True) + active = fields.Boolean(string='激活', default=True) + + _sql_constraints = [ + ('symbol_uniq', 'unique(symbol)', '字段标识必须唯一'), + ] + + def action_sync(self): + config_fields = self.env[self._dynamic_field_config].search([('active', 'in', [True, False])]) + model_id = self.env['ir.model'].search([('model', '=', self._dynamic_field_model)]) + + if not config_fields or not model_id: + return + IrModelFields = self.env['ir.model.fields'] + dynamic_fields = IrModelFields.search( + [('model', '=', self._dynamic_field_model), ('name', 'like', self._dynamic_field_prefix)] + ) + + def create_dynamic_fields(new_config_fields): + new_fields = [] + for field in new_config_fields: + new_fields.append({ + 'name': f'{self._dynamic_field_prefix}{field.id}', + 'field_description': field.name, + 'model': self._dynamic_field_model, + 'model_id': model_id.id, + 'ttype': field.field_type, + }) + IrModelFields.create(new_fields) + for field in new_config_fields: + field.symbol = f'{self._dynamic_field_prefix}{field.id}' + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('同步字段'), + 'message': _(f'成功同步 {len(new_fields)} 个薪资自定义字段。'), + 'type': 'success', + 'sticky': False, + 'next': {'type': 'ir.actions.client', 'tag': 'reload'}, + }, + } + + def update_dynamic_fields(updated_config_fields): + updated_count = 0 + for field in updated_config_fields: + if field.symbol: + ir_field = IrModelFields.search([ + ('model', '=', self._dynamic_field_model), + ('name', '=', field.symbol) + ]) + if ir_field: + ir_field.write({ + 'field_description': field.name, + 'ttype': field.field_type, + }) + updated_count += 1 + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('同步字段'), + 'message': _(f'成功更新 {updated_count} 个薪资自定义字段。'), + 'type': 'success', + 'sticky': False, + 'next': {'type': 'ir.actions.client', 'tag': 'reload'}, + }, + } + + exist_dynamic_names = dynamic_fields.mapped('name') + exist_ids = [int(e.replace(self._dynamic_field_prefix, '')) for e in exist_dynamic_names if + e.replace(self._dynamic_field_prefix, '').isdigit()] + + # 处理新字段 + new_config_ids = [field.id for field in config_fields if field.id not in exist_ids] + if new_config_ids: + create_dynamic_fields(self.search([('id', 'in', new_config_ids)])) + + # 处理更新字段 + updated_config_ids = [field.id for field in config_fields if field.id in exist_ids] + if updated_config_ids: + update_dynamic_fields(self.search([('id', 'in', updated_config_ids)])) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('同步完成'), + 'message': _('所有薪资自定义字段已同步完成!'), + 'type': 'success', + 'sticky': False, + 'next': {'type': 'ir.actions.client', 'tag': 'reload'}, + }, + } + + def unlink(self): + IrModelFields = self.env['ir.model.fields'] + for record in self: + if record.symbol: + ir_field = IrModelFields.search([ + ('model', '=', self._dynamic_field_model), + ('name', '=', record.symbol) + ]) + if ir_field: + ir_field.unlink() + return super().unlink() \ No newline at end of file diff --git a/yuthon_salary/models/yuthon_salary_active.py b/yuthon_salary/models/yuthon_salary_active.py index 093914bc0..4533ff9c6 100644 --- a/yuthon_salary/models/yuthon_salary_active.py +++ b/yuthon_salary/models/yuthon_salary_active.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from dateutil.relativedelta import relativedelta +from lxml import etree from odoo import models, fields, api, _, SUPERUSER_ID from odoo.exceptions import ValidationError, UserError from odoo.exceptions import ValidationError @@ -93,6 +94,26 @@ class YuthonSalaryActive(models.Model): enterprise_promotion_bonus_2025 = fields.Float(string='2025年企业宣传奖励') insurance = fields.Float(string='保险', compute='_compute_insurance') + @property + def _dynamic_fields(self): + active_fields = ( + self.env['salary.field.config'] + .search([('active', '=', True), ('symbol', '!=', False)]) + .mapped('symbol') + ) + return active_fields + + @api.model + def get_views(self, views, options=None): + res = super().get_views(views, options) + if list_view := res['views'].get('list'): + arch = etree.fromstring(list_view['arch']) + tree_el = arch.xpath('//tree')[0] + for field in self._dynamic_fields: + tree_el.append(etree.Element('field', name=field)) + list_view['arch'] = etree.tostring(arch, encoding='unicode') + return res + @api.depends("basic_endowment_insurance", "basic_medical_insurance", "unemployment_insurance",) def _compute_insurance(self): for rec in self: diff --git a/yuthon_salary/security/ir.model.access.csv b/yuthon_salary/security/ir.model.access.csv index 8d3da4980..3b227ac7e 100644 --- a/yuthon_salary/security/ir.model.access.csv +++ b/yuthon_salary/security/ir.model.access.csv @@ -52,3 +52,4 @@ access_salary_active_wizard,access__salary_active_wizard,model_salary_active_wiz access_salary_active_line,access_salary_active_line,model_salary_active_line,base.group_user,1,1,1,1 access_meal_expenses_review_wizard,access_meal_expenses_review_wizard,model_meal_expenses_review_wizard,base.group_user,1,1,1,1 access_party_expenses_review_wizard,access_party_expenses_review_wizard,model_party_expenses_review_wizard,base.group_user,1,1,1,1 +access_salary_field_config,access_salary_field_config,model_salary_field_config,base.group_user,1,1,1,1 diff --git a/yuthon_salary/views/salary_field_config_views.xml b/yuthon_salary/views/salary_field_config_views.xml new file mode 100644 index 000000000..79f765eb2 --- /dev/null +++ b/yuthon_salary/views/salary_field_config_views.xml @@ -0,0 +1,42 @@ + + + + salary.field.config.view.tree + salary.field.config + + +
+
+ + + + +
+
+
+ + + salary.field.config.view.search + salary.field.config + + + + + + + + + + + + + + 薪资自定义字段配置 + salary.field.config + tree + + {'search_default_filter_all': 1} + + +
\ No newline at end of file diff --git a/yuthon_work_brand/views/hr_menus.xml b/yuthon_work_brand/views/hr_menus.xml index f598a364a..7695d3efa 100644 --- a/yuthon_work_brand/views/hr_menus.xml +++ b/yuthon_work_brand/views/hr_menus.xml @@ -404,6 +404,11 @@ parent="human_resources_salary_root_board" action="yuthon_salary.yuthon_salary_active_action" sequence="99"/> +