forked from Mapan/odoo17e
389 lines
19 KiB
Python
389 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import api, Command, fields, models, _
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
from collections import defaultdict
|
|
|
|
|
|
class ApprovalRequest(models.Model):
|
|
_name = 'approval.request'
|
|
_description = 'Approval Request'
|
|
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin']
|
|
_order = 'name'
|
|
_mail_post_access = 'read'
|
|
|
|
_check_company_auto = True
|
|
|
|
@api.model
|
|
def _read_group_request_status(self, stages, domain, order):
|
|
request_status_list = dict(self._fields['request_status'].selection).keys()
|
|
return request_status_list
|
|
|
|
name = fields.Char(string="Approval Subject", tracking=True)
|
|
category_id = fields.Many2one('approval.category', string="Category", required=True)
|
|
category_image = fields.Binary(related='category_id.image')
|
|
approver_ids = fields.One2many('approval.approver', 'request_id', string="Approvers", check_company=True,
|
|
compute='_compute_approver_ids', store=True, readonly=False)
|
|
company_id = fields.Many2one(
|
|
string='Company', related='category_id.company_id',
|
|
store=True, readonly=True, index=True)
|
|
date = fields.Datetime(string="Date")
|
|
date_start = fields.Datetime(string="Date start")
|
|
date_end = fields.Datetime(string="Date end")
|
|
quantity = fields.Float(string="Quantity")
|
|
location = fields.Char(string="Location")
|
|
date_confirmed = fields.Datetime(string="Date Confirmed")
|
|
partner_id = fields.Many2one('res.partner', string="Contact", check_company=True)
|
|
reference = fields.Char(string="Reference")
|
|
amount = fields.Float(string="Amount")
|
|
reason = fields.Html(string="Description")
|
|
request_status = fields.Selection([
|
|
('new', 'To Submit'),
|
|
('pending', 'Submitted'),
|
|
('approved', 'Approved'),
|
|
('refused', 'Refused'),
|
|
('cancel', 'Cancel'),
|
|
], default="new", compute="_compute_request_status",
|
|
store=True, index=True, tracking=True,
|
|
group_expand='_read_group_request_status')
|
|
request_owner_id = fields.Many2one('res.users', string="Request Owner",
|
|
check_company=True, domain="[('company_ids', 'in', company_id)]")
|
|
user_status = fields.Selection([
|
|
('new', 'New'),
|
|
('pending', 'To Approve'),
|
|
('waiting', 'Waiting'),
|
|
('approved', 'Approved'),
|
|
('refused', 'Refused'),
|
|
('cancel', 'Cancel')], compute="_compute_user_status")
|
|
has_access_to_request = fields.Boolean(string="Has Access To Request", compute="_compute_has_access_to_request")
|
|
change_request_owner = fields.Boolean(string='Can Change Request Owner', compute='_compute_has_access_to_request')
|
|
attachment_number = fields.Integer('Number of Attachments', compute='_compute_attachment_number')
|
|
product_line_ids = fields.One2many('approval.product.line', 'approval_request_id', check_company=True)
|
|
|
|
has_date = fields.Selection(related="category_id.has_date")
|
|
has_period = fields.Selection(related="category_id.has_period")
|
|
has_quantity = fields.Selection(related="category_id.has_quantity")
|
|
has_amount = fields.Selection(related="category_id.has_amount")
|
|
has_reference = fields.Selection(related="category_id.has_reference")
|
|
has_partner = fields.Selection(related="category_id.has_partner")
|
|
has_payment_method = fields.Selection(related="category_id.has_payment_method")
|
|
has_location = fields.Selection(related="category_id.has_location")
|
|
has_product = fields.Selection(related="category_id.has_product")
|
|
requirer_document = fields.Selection(related="category_id.requirer_document")
|
|
approval_minimum = fields.Integer(related="category_id.approval_minimum")
|
|
approval_type = fields.Selection(related="category_id.approval_type")
|
|
approver_sequence = fields.Boolean(related="category_id.approver_sequence")
|
|
automated_sequence = fields.Boolean(related="category_id.automated_sequence")
|
|
|
|
@api.depends('request_owner_id')
|
|
@api.depends_context('uid')
|
|
def _compute_has_access_to_request(self):
|
|
is_approval_user = self.env.user.has_group('approvals.group_approval_user')
|
|
self.change_request_owner = is_approval_user
|
|
for request in self:
|
|
request.has_access_to_request = request.request_owner_id == self.env.user and is_approval_user
|
|
|
|
def _compute_attachment_number(self):
|
|
domain = [('res_model', '=', 'approval.request'), ('res_id', 'in', self.ids)]
|
|
attachment_data = self.env['ir.attachment']._read_group(domain, ['res_id'], ['__count'])
|
|
attachment = dict(attachment_data)
|
|
for request in self:
|
|
request.attachment_number = attachment.get(request.id, 0)
|
|
|
|
@api.constrains('date_start', 'date_end')
|
|
def _check_dates(self):
|
|
for request in self:
|
|
if request.date_start and request.date_end and request.date_start > request.date_end:
|
|
raise ValidationError(_("Start date should precede the end date."))
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
category = 'category_id' in vals and self.env['approval.category'].browse(vals['category_id'])
|
|
if category and category.automated_sequence:
|
|
vals['name'] = category.sequence_id.next_by_id()
|
|
return super().create(vals_list)
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def unlink_attachments(self):
|
|
attachment_ids = self.env['ir.attachment'].search([
|
|
('res_model', '=', 'approval.request'),
|
|
('res_id', 'in', self.ids),
|
|
])
|
|
if attachment_ids:
|
|
attachment_ids.unlink()
|
|
|
|
def unlink(self):
|
|
self.filtered(lambda a: a.has_product).product_line_ids.unlink()
|
|
return super().unlink()
|
|
|
|
def action_get_attachment_view(self):
|
|
self.ensure_one()
|
|
res = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
|
|
res['domain'] = [('res_model', '=', 'approval.request'), ('res_id', 'in', self.ids)]
|
|
res['context'] = {'default_res_model': 'approval.request', 'default_res_id': self.id}
|
|
return res
|
|
|
|
def action_confirm(self):
|
|
# make sure that the manager is present in the list if he is required
|
|
self.ensure_one()
|
|
if self.category_id.manager_approval == 'required':
|
|
employee = self.env['hr.employee'].search([('user_id', '=', self.request_owner_id.id)], limit=1)
|
|
if not employee.parent_id:
|
|
raise UserError(_('This request needs to be approved by your manager. There is no manager linked to your employee profile.'))
|
|
if not employee.parent_id.user_id:
|
|
raise UserError(_('This request needs to be approved by your manager. There is no user linked to your manager.'))
|
|
if not self.approver_ids.filtered(lambda a: a.user_id.id == employee.parent_id.user_id.id):
|
|
raise UserError(_('This request needs to be approved by your manager. Your manager is not in the approvers list.'))
|
|
if len(self.approver_ids) < self.approval_minimum:
|
|
raise UserError(_("You have to add at least %s approvers to confirm your request.", self.approval_minimum))
|
|
if self.requirer_document == 'required' and not self.attachment_number:
|
|
raise UserError(_("You have to attach at least one document."))
|
|
|
|
approvers = self.approver_ids
|
|
if self.approver_sequence:
|
|
approvers = approvers.filtered(lambda a: a.status in ['new', 'pending', 'waiting'])
|
|
|
|
approvers[1:].sudo().write({'status': 'waiting'})
|
|
approvers = approvers[0] if approvers and approvers[0].status != 'pending' else self.env['approval.approver']
|
|
else:
|
|
approvers = approvers.filtered(lambda a: a.status == 'new')
|
|
|
|
approvers._create_activity()
|
|
approvers.sudo().write({'status': 'pending'})
|
|
self.sudo().write({'date_confirmed': fields.Datetime.now()})
|
|
|
|
def _get_user_approval_activities(self, user):
|
|
domain = [
|
|
('res_model', '=', 'approval.request'),
|
|
('res_id', 'in', self.ids),
|
|
('activity_type_id', '=', self.env.ref('approvals.mail_activity_data_approval').id),
|
|
('user_id', '=', user.id)
|
|
]
|
|
activities = self.env['mail.activity'].search(domain)
|
|
return activities
|
|
|
|
def _ensure_can_approve(self):
|
|
if any(approval.approver_sequence and approval.user_status == 'waiting' for approval in self):
|
|
raise ValidationError(_('You cannot approve before the previous approver.'))
|
|
|
|
def _update_next_approvers(self, new_status, approver, only_next_approver, cancel_activities=False):
|
|
approvers_updated = self.env['approval.approver']
|
|
for approval in self.filtered('approver_sequence'):
|
|
current_approver = approval.approver_ids & approver
|
|
approvers_to_update = approval.approver_ids.filtered(lambda a: a.status not in ['approved', 'refused'] and (a.sequence > current_approver.sequence or (a.sequence == current_approver.sequence and a.id > current_approver.id)))
|
|
|
|
if only_next_approver and approvers_to_update:
|
|
approvers_to_update = approvers_to_update[0]
|
|
approvers_updated |= approvers_to_update
|
|
|
|
approvers_updated.sudo().status = new_status
|
|
if new_status == 'pending':
|
|
approvers_updated._create_activity()
|
|
if cancel_activities:
|
|
approvers_updated.request_id._cancel_activities()
|
|
|
|
def _cancel_activities(self):
|
|
approval_activity = self.env.ref('approvals.mail_activity_data_approval')
|
|
activities = self.activity_ids.filtered(lambda a: a.activity_type_id == approval_activity)
|
|
activities.unlink()
|
|
|
|
def action_approve(self, approver=None):
|
|
self._ensure_can_approve()
|
|
|
|
if not isinstance(approver, models.BaseModel):
|
|
approver = self.mapped('approver_ids').filtered(
|
|
lambda approver: approver.user_id == self.env.user
|
|
)
|
|
approver.write({'status': 'approved'})
|
|
self.sudo()._update_next_approvers('pending', approver, only_next_approver=True)
|
|
self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback()
|
|
|
|
def action_refuse(self, approver=None):
|
|
if not isinstance(approver, models.BaseModel):
|
|
approver = self.mapped('approver_ids').filtered(
|
|
lambda approver: approver.user_id == self.env.user
|
|
)
|
|
approver.write({'status': 'refused'})
|
|
self.sudo()._update_next_approvers('refused', approver, only_next_approver=False, cancel_activities=True)
|
|
self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback()
|
|
|
|
def action_withdraw(self, approver=None):
|
|
if not isinstance(approver, models.BaseModel):
|
|
approver = self.mapped('approver_ids').filtered(
|
|
lambda approver: approver.user_id == self.env.user
|
|
)
|
|
self.sudo()._update_next_approvers('waiting', approver, only_next_approver=False, cancel_activities=True)
|
|
approver.write({'status': 'pending'})
|
|
|
|
def action_draft(self):
|
|
self.mapped('approver_ids').write({'status': 'new'})
|
|
|
|
def action_cancel(self):
|
|
self.sudo()._get_user_approval_activities(user=self.env.user).unlink()
|
|
self.mapped('approver_ids').write({'status': 'cancel'})
|
|
|
|
@api.depends_context('uid')
|
|
@api.depends('approver_ids.status')
|
|
def _compute_user_status(self):
|
|
for approval in self:
|
|
approval.user_status = approval.approver_ids.filtered(lambda approver: approver.user_id == self.env.user).status
|
|
|
|
@api.depends('approver_ids.status', 'approver_ids.required')
|
|
def _compute_request_status(self):
|
|
for request in self:
|
|
status_lst = request.mapped('approver_ids.status')
|
|
required_approved = all(a.status == 'approved' for a in request.approver_ids.filtered('required'))
|
|
minimal_approver = request.approval_minimum if len(status_lst) >= request.approval_minimum else len(status_lst)
|
|
if status_lst:
|
|
if status_lst.count('cancel'):
|
|
status = 'cancel'
|
|
elif status_lst.count('refused'):
|
|
status = 'refused'
|
|
elif status_lst.count('new'):
|
|
status = 'new'
|
|
elif status_lst.count('approved') >= minimal_approver and required_approved:
|
|
status = 'approved'
|
|
else:
|
|
status = 'pending'
|
|
else:
|
|
status = 'new'
|
|
request.request_status = status
|
|
|
|
self.filtered_domain([('request_status', 'in', ['approved', 'refused', 'cancel'])])._cancel_activities()
|
|
|
|
@api.model
|
|
def _update_approver_vals(self, approver_id_vals, approver, new_required, new_sequence):
|
|
if approver.required != new_required or approver.sequence != new_sequence:
|
|
approver_id_vals.append(Command.update(approver.id, {'required': new_required, 'sequence': new_sequence}))
|
|
|
|
@api.model
|
|
def _create_or_update_approver(self, user_id, users_to_approver, approver_id_vals, required, sequence):
|
|
if user_id not in users_to_approver.keys():
|
|
approver_id_vals.append(Command.create({
|
|
'user_id': user_id,
|
|
'status': 'new',
|
|
'required': required,
|
|
'sequence': sequence,
|
|
}))
|
|
else:
|
|
current_approver = users_to_approver.pop(user_id)
|
|
self._update_approver_vals(approver_id_vals, current_approver, required, sequence)
|
|
|
|
@api.depends('category_id', 'request_owner_id')
|
|
def _compute_approver_ids(self):
|
|
for request in self:
|
|
users_to_approver = {}
|
|
for approver in request.approver_ids:
|
|
users_to_approver[approver.user_id.id] = approver
|
|
|
|
users_to_category_approver = {}
|
|
for approver in request.category_id.approver_ids:
|
|
users_to_category_approver[approver.user_id.id] = approver
|
|
|
|
approver_id_vals = []
|
|
|
|
if request.category_id.manager_approval:
|
|
employee = self.env['hr.employee'].search([('user_id', '=', request.request_owner_id.id)], limit=1)
|
|
if employee.parent_id.user_id:
|
|
manager_user_id = employee.parent_id.user_id.id
|
|
manager_required = request.category_id.manager_approval == 'required'
|
|
# We set the manager sequence to be lower than all others (9) so they are the first to approve.
|
|
self._create_or_update_approver(manager_user_id, users_to_approver, approver_id_vals, manager_required, 9)
|
|
if manager_user_id in users_to_category_approver.keys():
|
|
users_to_category_approver.pop(manager_user_id)
|
|
|
|
for user_id in users_to_category_approver:
|
|
self._create_or_update_approver(user_id, users_to_approver, approver_id_vals,
|
|
users_to_category_approver[user_id].required,
|
|
users_to_category_approver[user_id].sequence)
|
|
|
|
for current_approver in users_to_approver.values():
|
|
# Reset sequence and required for the remaining approvers that are no (longer) part of the category approvers or managers.
|
|
# Set the sequence of these manually added approvers to 1000, so that they always appear after the category approvers.
|
|
self._update_approver_vals(approver_id_vals, current_approver, False, 1000)
|
|
|
|
request.update({'approver_ids': approver_id_vals})
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
|
|
if 'approver_ids' in vals:
|
|
to_resequence = self.filtered_domain([('approver_sequence', '=', True), ('request_status', '=', 'pending')])
|
|
for approval in to_resequence:
|
|
if not approval.approver_ids.filtered(lambda a: a.status == 'pending'):
|
|
approver = approval.approver_ids.filtered(lambda a: a.status == 'waiting')
|
|
if approver:
|
|
approver[0].status = 'pending'
|
|
approver[0]._create_activity()
|
|
|
|
return res
|
|
|
|
@api.constrains('approver_ids')
|
|
def _check_approver_ids(self):
|
|
for request in self:
|
|
# make sure the approver_ids are unique per request
|
|
if len(request.approver_ids) != len(request.approver_ids.user_id):
|
|
raise UserError(_("You cannot assign the same approver multiple times on the same request."))
|
|
|
|
class ApprovalApprover(models.Model):
|
|
_name = 'approval.approver'
|
|
_description = 'Approver'
|
|
_order = 'sequence, id'
|
|
|
|
_check_company_auto = True
|
|
|
|
sequence = fields.Integer('Sequence', default=10)
|
|
user_id = fields.Many2one('res.users', string="User", required=True, check_company=True, domain="[('id', 'not in', existing_request_user_ids)]")
|
|
existing_request_user_ids = fields.Many2many('res.users', compute='_compute_existing_request_user_ids')
|
|
status = fields.Selection([
|
|
('new', 'New'),
|
|
('pending', 'To Approve'),
|
|
('waiting', 'Waiting'),
|
|
('approved', 'Approved'),
|
|
('refused', 'Refused'),
|
|
('cancel', 'Cancel')], string="Status", default="new", readonly=True)
|
|
request_id = fields.Many2one('approval.request', string="Request",
|
|
ondelete='cascade', check_company=True)
|
|
company_id = fields.Many2one(
|
|
string='Company', related='request_id.company_id',
|
|
store=True, readonly=True, index=True)
|
|
required = fields.Boolean(default=False, readonly=True)
|
|
category_approver = fields.Boolean(compute='_compute_category_approver')
|
|
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.")
|
|
|
|
def action_approve(self):
|
|
self.request_id.action_approve(self)
|
|
|
|
def action_refuse(self):
|
|
self.request_id.action_refuse(self)
|
|
|
|
def _create_activity(self):
|
|
for approver in self:
|
|
approver.request_id.activity_schedule(
|
|
'approvals.mail_activity_data_approval',
|
|
user_id=approver.user_id.id)
|
|
|
|
@api.depends('request_id.request_owner_id', 'request_id.approver_ids.user_id')
|
|
def _compute_existing_request_user_ids(self):
|
|
for approver in self:
|
|
approver.existing_request_user_ids = \
|
|
self.mapped('request_id.approver_ids.user_id')._origin \
|
|
| self.request_id.request_owner_id._origin
|
|
|
|
@api.depends('category_approver', 'user_id')
|
|
def _compute_category_approver(self):
|
|
for approval in self:
|
|
approval.category_approver = approval.user_id in approval.request_id.category_id.approver_ids.user_id
|
|
|
|
@api.depends_context('uid')
|
|
@api.depends('user_id', 'category_approver')
|
|
def _compute_can_edit(self):
|
|
is_user = self.env.user.has_group('approvals.group_approval_user')
|
|
for approval in self:
|
|
approval.can_edit = not approval.user_id or not approval.category_approver or is_user
|
|
approval.can_edit_user_id = is_user or approval.request_id.request_owner_id == self.env.user or not approval.user_id
|