update code : promote - approval flow works (need more testing)

This commit is contained in:
hoangvv 2025-01-20 18:36:37 +07:00
parent a6b9292857
commit bb344d53fb
8 changed files with 148 additions and 60 deletions

View File

@ -2,10 +2,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64 import base64
import logging
from odoo import api, fields, models, tools, _ from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
CATEGORY_SELECTION = [ CATEGORY_SELECTION = [
('required', 'Required'), ('required', 'Required'),
@ -97,25 +98,28 @@ class ApprovalCategory(models.Model):
model_id = fields.Many2one('ir.model', string='Model', store=True) model_id = fields.Many2one('ir.model', string='Model', store=True)
model_name = fields.Char(string='Model Name', related='model_id.model', store=True) model_name = fields.Char(string='Model Name', related='model_id.model', store=True)
invalid_minimum = fields.Boolean("Invalid Minimum", compute="_compute_invalid_minimum", store=True, readonly=False) # invalid_minimum = fields.Boolean("Invalid Minimum", compute="_compute_invalid_minimum", store=True, readonly=False)
invalid_minimum_warning = fields.Boolean("Invalid minimum Warning",compute="_compute_invalid_minimum_warning") # invalid_minimum_warning = fields.Boolean("Invalid minimum Warning",compute="_compute_invalid_minimum_warning")
domain = fields.Char(compute='_compute_domain') # domain = fields.Char(string='Conditions')
# ---------------------------------------- Methods ----------------------------------------- # ---------------------------------------- Methods -----------------------------------------
def _compute_domain(self):
for record in self:
record.domain = record.conditions_ids.domain if record.conditions_ids else '[]' # @api.depends('conditions_ids')
@api.depends('conditions_ids') # def _compute_invalid_minimum(self):
def _compute_invalid_minimum(self): # for record in self:
for record in self.filtered('conditions_ids'): # # If you expect to compute the invalid_minimum for each condition, loop through them
record.invalid_minimum = record.conditions_ids.invalid_minimum # invalid_minimums = record.conditions_ids.mapped('invalid_minimum')
@api.depends('conditions_ids') # if invalid_minimums:
def _compute_invalid_minimum_warning(self): # # Handle the logic for determining invalid_minimum (example)
for record in self.filtered('conditions_ids'): # record.invalid_minimum = any(invalid_minimums)
record.invalid_minimum_warning = record.conditions_ids.invalid_minimum_warning # @api.depends('conditions_ids')
# def _compute_invalid_minimum_warning(self):
# for record in self.filtered('conditions_ids'):
# record.invalid_minimum_warning = record.conditions_ids.invalid_minimum_warning
def _compute_request_to_validate_count(self): def _compute_request_to_validate_count(self):
domain = [('request_status', '=', 'pending'), ('approver_ids.user_id', '=', self.env.user.id)] domain = [('request_status', '=', 'pending'), ('approver_ids.user_id', '=', self.env.user.id)]
requests_data = self.env['approval.request']._read_group(domain, ['category_id'], ['__count']) requests_data = self.env['approval.request']._read_group(domain, ['category_id'], ['__count'])
@ -168,7 +172,8 @@ class ApprovalCategory(models.Model):
'company_id': vals.get('company_id'), 'company_id': vals.get('company_id'),
}) })
vals['sequence_id'] = sequence.id vals['sequence_id'] = sequence.id
return super().create(vals_list) res = super().create(vals_list)
return res
def write(self, vals): def write(self, vals):
if 'sequence_code' in vals: if 'sequence_code' in vals:
@ -188,7 +193,9 @@ class ApprovalCategory(models.Model):
for approval_category in self: for approval_category in self:
if approval_category.sequence_id: if approval_category.sequence_id:
approval_category.sequence_id.company_id = vals.get('company_id') approval_category.sequence_id.company_id = vals.get('company_id')
return super().write(vals)
res = super().write(vals)
return res
def create_request(self): def create_request(self):
self.ensure_one() self.ensure_one()

View File

@ -12,8 +12,9 @@ class ApprovalCategoryApprover(models.Model):
_rec_name = 'user_id' _rec_name = 'user_id'
_order = 'sequence' _order = 'sequence'
sequence = fields.Integer('Sequence', default=10) sequence = fields.Integer('Sequence', default=10)
category_id = fields.Many2one('approval.category', string='Approval Type', required=True) category_id = fields.Many2one('approval.category', string='Approval Type')
company_id = fields.Many2one('res.company', related='category_id.company_id') condition_id = fields.Many2one('approval.category.condition', string='Approval Condition')
company_id = fields.Many2one('res.company', related='condition_id.company_id')
user_id = fields.Many2one('res.users', string='User', ondelete='cascade', required=True, user_id = fields.Many2one('res.users', string='User', ondelete='cascade', required=True,
check_company=True, domain="[('company_ids', 'in', company_id), ('id', 'not in', existing_user_ids)]") check_company=True, domain="[('company_ids', 'in', company_id), ('id', 'not in', existing_user_ids)]")
job_title = fields.Char(string='Job Position', compute='_compute_job_title', store=True) job_title = fields.Char(string='Job Position', compute='_compute_job_title', store=True)
@ -33,7 +34,7 @@ class ApprovalCategoryApprover(models.Model):
record.job_title = employee.job_title if employee else '' record.job_title = employee.job_title if employee else ''
else: else:
record.job_title = '' record.job_title = ''
@api.depends('category_id') @api.depends('condition_id')
def _compute_existing_user_ids(self): def _compute_existing_user_ids(self):
for record in self: for record in self:
record.existing_user_ids = record.category_id.approver_ids.user_id record.existing_user_ids = record.condition_id.approver_ids.user_id

View File

@ -8,12 +8,16 @@ class ApprovalConditions(models.Model):
sequence = fields.Integer(default=1) sequence = fields.Integer(default=1)
description = fields.Text(string="Description") description = fields.Text(string="Description")
company_id = fields.Many2one(
'res.company', 'Company', copy=False,
required=True, index=True, default=lambda s: s.env.company)
approver_sequence = fields.Boolean('Approvers Sequence?', help="If checked, the approvers have to approve in sequence.") approver_sequence = fields.Boolean('Approvers Sequence?', help="If checked, the approvers have to approve in sequence.")
approval_minimum = fields.Integer(string="Minimum Approval", default=5, required=True) approval_minimum = fields.Integer(string="Minimum Approval", default=5, required=True)
invalid_minimum = fields.Boolean(compute='_compute_invalid_minimum') invalid_minimum = fields.Boolean(compute='_compute_invalid_minimum')
invalid_minimum_warning = fields.Char(compute='_compute_invalid_minimum') invalid_minimum_warning = fields.Char(compute='_compute_invalid_minimum')
approver_ids = fields.One2many('approval.category.approver', 'category_id', string="Approvers") approver_ids = fields.One2many('approval.category.approver', 'condition_id', string="Approvers")
category_id = fields.Many2one('approval.category', string="Approval Category", required=True,ondelete="cascade") user_ids = fields.Many2many('res.users', compute='_compute_user_ids', string="Approver Users")
category_id = fields.Many2one('approval.category', string="Approval Category")
model_id = fields.Many2one('ir.model', string='Model', related='category_id.model_id', store=True, readonly=True) model_id = fields.Many2one('ir.model', string='Model', related='category_id.model_id', store=True, readonly=True)
model_name = fields.Char( model_name = fields.Char(
string='Model Name', string='Model Name',
@ -21,7 +25,7 @@ class ApprovalConditions(models.Model):
store=True, store=True,
readonly=True readonly=True
) )
domain = fields.Char(string="Conditions", compute='_compute_domain', readonly=False, store=True) domain = fields.Char(string="Conditions", readonly=False, store=True)
# @api.depends('category_id.model_id') # @api.depends('category_id.model_id')
# def _compute_model_id(self): # def _compute_model_id(self):
@ -29,11 +33,10 @@ class ApprovalConditions(models.Model):
# for record in self: # for record in self:
# record.model_id = record.category_id.model_id # record.model_id = record.category_id.model_id
@api.depends('approver_ids')
@api.depends('model_id') def _compute_user_ids(self):
def _compute_domain(self):
for record in self: for record in self:
record.domain = '[]' record.user_ids = record.approver_ids.user_id
@api.depends('approval_minimum', 'approver_ids') @api.depends('approval_minimum', 'approver_ids')
def _compute_invalid_minimum(self): def _compute_invalid_minimum(self):
@ -54,6 +57,15 @@ class ApprovalConditions(models.Model):
# for record in self: # for record in self:
# record.filter_id = False # record.filter_id = False
@api.constrains('approver_ids')
def _constrains_approver_ids(self):
# There seems to be a problem with how the database is updated which doesn't let use to an sql constraint for this
# Issue is: records seem to be created before others are saved, meaning that if you originally have only user a
# change user a to user b and add a new line with user a, the second line will be created and will trigger the constraint
# before the first line will be updated which wouldn't trigger a ValidationError
for record in self:
if len(record.approver_ids) != len(record.approver_ids.user_id):
raise ValidationError(_('An user may not be in the approver list multiple times.'))
@api.constrains('approval_minimum', 'approver_ids') @api.constrains('approval_minimum', 'approver_ids')
def _constrains_approval_minimum(self): def _constrains_approval_minimum(self):
for record in self: for record in self:

View File

@ -21,7 +21,7 @@ class ApprovalPromotionLine(models.Model):
current_job = fields.Char( current_job = fields.Char(
"Current Job", required=True, "Current Job", required=True,
compute="_compute_promotion", store=True, readonly=True, precompute=True) compute="_compute_promotion", store=True, readonly=True, precompute=True)
designation_job = fields.Char( new_designation = fields.Char(
"New Designation", required=True, "New Designation", required=True,
compute="_compute_promotion", store=True, readonly=True, precompute=True) compute="_compute_promotion", store=True, readonly=True, precompute=True)
@api.depends('promote_id') @api.depends('promote_id')
@ -29,5 +29,4 @@ class ApprovalPromotionLine(models.Model):
for line in self: for line in self:
line.description = line.promote_id.description or line.promote_id.display_name line.description = line.promote_id.description or line.promote_id.display_name
line.current_job = line.promote_id.job_id.name or '' line.current_job = line.promote_id.job_id.name or ''
line.designation_job = line.promote_id.designation_id.name or '' line.new_designation = line.promote_id.designation_id.name or ''

View File

@ -1,9 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, Command, fields, models, _ from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError, ValidationError from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
class ApprovalRequest(models.Model): class ApprovalRequest(models.Model):
_name = 'approval.request' _name = 'approval.request'
_description = 'Approval Request' _description = 'Approval Request'
@ -68,7 +68,6 @@ class ApprovalRequest(models.Model):
has_location = fields.Selection(related="category_id.has_location") has_location = fields.Selection(related="category_id.has_location")
has_product = fields.Selection(related="category_id.has_product") has_product = fields.Selection(related="category_id.has_product")
has_promotion = fields.Selection(related="category_id.has_promotion") has_promotion = fields.Selection(related="category_id.has_promotion")
requirer_document = fields.Selection(related="category_id.requirer_document") requirer_document = fields.Selection(related="category_id.requirer_document")
approval_minimum = fields.Integer(related="category_id.approval_minimum") approval_minimum = fields.Integer(related="category_id.approval_minimum")
approval_type = fields.Selection(related="category_id.approval_type") approval_type = fields.Selection(related="category_id.approval_type")
@ -99,30 +98,81 @@ class ApprovalRequest(models.Model):
for request in self: for request in self:
if request.date_start and request.date_end and request.date_start > request.date_end: if request.date_start and request.date_end and request.date_start > request.date_end:
raise ValidationError(_("Start date should precede the end date.")) raise ValidationError(_("Start date should precede the end date."))
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
for vals in vals_list: for vals in vals_list:
category = 'category_id' in vals and self.env['approval.category'].browse(vals['category_id']) category = 'category_id' in vals and self.env['approval.category'].browse(vals['category_id'])
if category and category.automated_sequence: if category and category.automated_sequence:
vals['name'] = category.sequence_id.next_by_id() vals['name'] = category.sequence_id.next_by_id()
self._check_conditions_promotion(vals)
created_requests = super().create(vals_list) created_requests = super().create(vals_list)
self._check_conditions(created_requests)
for request in created_requests: for request in created_requests:
request.message_subscribe(partner_ids=request.request_owner_id.partner_id.ids) request.message_subscribe(partner_ids=request.request_owner_id.partner_id.ids)
return created_requests return created_requests
def _check_conditions(self,request):
def _check_conditions_promotion(self,vals): data = request
if vals.get('has_promotion'): if isinstance(request, bool):
promotion_lines = vals.get('promotion_line_ids', []) _logger.error(f"Expected 'vals' to be a list, got {type(request)}")
approval_conditions_to_add = [] data = self
if promotion_lines: for request in data:
for promotion_line in promotion_lines: if request.has_promotion:
promotion_line_record = self.env['approval.promotion.line'].browse(promotion_line) promotion_lines = request.promotion_line_ids
if promotion_line_record and promotion_line_record.promote_id: if promotion_lines:
conditions = request.category_id.conditions_ids
for condition in conditions:
satisfy_condition = False
domain = condition.domain if condition.domain else []
return # Ensure domain is in list or tuple format
return if isinstance(domain, str):
try:
domain = safe_eval(domain)
except Exception as e:
_logger.error(f"Failed to evaluate domain: {e}")
domain = []
elif not isinstance(domain, (list, tuple)):
domain = []
_logger.debug(f"Evaluating condition: {condition}, Domain: {domain}")
for promotion_line in promotion_lines:
if not promotion_line.promote_id or not promotion_line.promote_id.employee_id:
_logger.warning(f"No promote_id or employee_id found for promotion_line: {promotion_line.id}")
continue
model_name = request.category_id.model_name
# if not hasattr(self.env, model_name):
# _logger.error(f"Invalid model name: {model_name}")
# continue
result = self.env[model_name].search(domain)
# Compare promotion_line with result
for res in result:
if promotion_line.promote_id.employee_id.id == res.id:
satisfy_condition = True
break
if satisfy_condition:
request._add_approvers(condition)
else:
_logger.info('Condition not satisfied')
def _add_approvers(self, condition):
for request in self:
approvers = condition.approver_ids
for approver in approvers:
existing_approver = request.approver_ids.filtered(lambda x: x.user_id == approver.user_id)
if existing_approver:
existing_approver.write({
'status': 'pending',
})
else:
self.env['approval.approver'].create({
'user_id': approver.user_id.id,
'request_id': request.id,
'required':approver.required,
'status': 'pending'
})
@api.ondelete(at_uninstall=False) @api.ondelete(at_uninstall=False)
def unlink_attachments(self): def unlink_attachments(self):
attachment_ids = self.env['ir.attachment'].search([ attachment_ids = self.env['ir.attachment'].search([
@ -168,7 +218,6 @@ class ApprovalRequest(models.Model):
approvers = approvers[0] if approvers and approvers[0].status != 'pending' else self.env['approval.approver'] approvers = approvers[0] if approvers and approvers[0].status != 'pending' else self.env['approval.approver']
else: else:
approvers = approvers.filtered(lambda a: a.status == 'new') approvers = approvers.filtered(lambda a: a.status == 'new')
approvers._create_activity() approvers._create_activity()
approvers.sudo().write({'status': 'pending'}) approvers.sudo().write({'status': 'pending'})
self.sudo().write({'date_confirmed': fields.Datetime.now()}) self.sudo().write({'date_confirmed': fields.Datetime.now()})
@ -230,7 +279,17 @@ class ApprovalRequest(models.Model):
subject=subject, subject=subject,
partner_ids=approval.request_owner_id.partner_id.ids, partner_ids=approval.request_owner_id.partner_id.ids,
) )
if self.has_promotion:
all_confirm = True
for approver in self.approver_ids:
if approver.required and approver.status != 'approved':
all_confirm = False
if all_confirm:
for promote in self.promotion_line_ids:
promote_records = self.env['hr.promote'].search([('id', '=', promote.promote_id.id)],limit=1)
promote_records.write({
'state': 'confirmed'
})
self.sudo()._update_next_approvers('pending', approver, only_next_approver=True) self.sudo()._update_next_approvers('pending', approver, only_next_approver=True)
self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback() self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback()
@ -360,9 +419,8 @@ class ApprovalRequest(models.Model):
if 'request_owner_id' in vals: if 'request_owner_id' in vals:
for approval in self: for approval in self:
approval.message_unsubscribe(partner_ids=approval.request_owner_id.partner_id.ids) approval.message_unsubscribe(partner_ids=approval.request_owner_id.partner_id.ids)
res = super().write(vals) res = super().write(vals)
self._check_conditions(res)
if 'request_owner_id' in vals: if 'request_owner_id' in vals:
for approval in self: for approval in self:
approval.message_subscribe(partner_ids=approval.request_owner_id.partner_id.ids) approval.message_subscribe(partner_ids=approval.request_owner_id.partner_id.ids)
@ -417,7 +475,6 @@ class ApprovalApprover(models.Model):
category_approver = fields.Boolean(compute='_compute_category_approver') category_approver = fields.Boolean(compute='_compute_category_approver')
can_edit = fields.Boolean(compute='_compute_can_edit') can_edit = fields.Boolean(compute='_compute_can_edit')
can_edit_user_id = fields.Boolean(compute='_compute_can_edit', help="Simple users should not be able to remove themselves as approvers because they will lose access to the record if they misclick.") can_edit_user_id = fields.Boolean(compute='_compute_can_edit', help="Simple users should not be able to remove themselves as approvers because they will lose access to the record if they misclick.")
def action_approve(self): def action_approve(self):
self.request_id.action_approve(self) self.request_id.action_approve(self)

View File

@ -79,7 +79,7 @@
<notebook> <notebook>
<page string="Conditions"> <page string="Conditions">
<group> <group>
<field name="conditions_ids" nolabel="1"/> <field name="conditions_ids" widget="one2many_list" nolabel="1"/>
</group> </group>
</page> </page>
<page string="Options"> <page string="Options">

View File

@ -8,7 +8,7 @@
<field name="company_id" column_invisible="True"/> <field name="company_id" column_invisible="True"/>
<field name="promote_id"/> <field name="promote_id"/>
<field name="current_job" /> <field name="current_job" />
<field name="designation_job"/> <field name="new_designation"/>
<field name="description"/> <field name="description"/>
</list> </list>
</field> </field>
@ -25,7 +25,7 @@
<field name="promote_id"/> <field name="promote_id"/>
<field name="description"/> <field name="description"/>
<!-- <field name="current_job" options="{'no_create': True, 'no_open': True}"/> <!-- <field name="current_job" options="{'no_create': True, 'no_open': True}"/>
<field name="designation_job" options="{'no_create': True, 'no_open': True}"/> --> <field name="new_designation" options="{'no_create': True, 'no_open': True}"/> -->
</group> </group>
</sheet> </sheet>
</form> </form>
@ -41,7 +41,7 @@
<templates> <templates>
<t t-name="card" class="d-flex justify-content-between"> <t t-name="card" class="d-flex justify-content-between">
<field name="promote_id" class="fw-bolder fs-5"/> <field name="promote_id" class="fw-bolder fs-5"/>
<field name="designation_job" class="fw-bolder fs-5"/> <field name="new_designation" class="fw-bolder fs-5"/>
</t> </t>
</templates> </templates>
</kanban> </kanban>

View File

@ -103,8 +103,20 @@ class HRPromote(models.Model):
raise exceptions.ValidationError("The employee does not have a current job position.") raise exceptions.ValidationError("The employee does not have a current job position.")
promotion_record = super(HRPromote, self).create(vals) promotion_record = super(HRPromote, self).create(vals)
promotion_records.append(promotion_record) promotion_records.append(promotion_record)
# Create an Approval request category = self.env['approval.category'].search([("has_promotion", "=", "required")], limit=1)
if not category:
category = self.env['approval.category'].search([], limit=1)
# Create the approval request
self.env['approval.request'].create({
'category_id': category.id,
'date': fields.Datetime.now(),
'company_id': self.env.company.id,
'name': 'Automatic',
'promotion_line_ids': [(0, 0, {
'company_id': self.env.company.id,
'promote_id': promotion_record.id,
})],
})
return self.browse([record.id for record in promotion_records]) return self.browse([record.id for record in promotion_records])
def action_save_and_close(self): def action_save_and_close(self):
return { return {