first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import document
from . import team
+313
View File
@@ -0,0 +1,313 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError, AccessError
from .selection import ApprovalMethods, DocumentState, ApproverState, ApprovalStep, DocumentVisibility
class DocApprovalDocumentPackage(models.Model):
_name = 'xf.doc.approval.document.package'
_inherit = ['mail.thread']
_description = 'Document Package'
active = fields.Boolean(default=True)
name = fields.Char(
string='Name',
required=True,
translate=True,
readonly=True,
tracking=True,
)
description = fields.Text(
string='Description',
translate=True,
)
state = fields.Selection(
string='Status',
selection=DocumentState.list,
required=True,
default=DocumentState.default,
readonly=True,
tracking=True,
)
approval_state = fields.Selection(
string='Approval Status',
selection=ApproverState.list,
compute='_compute_approval_state',
)
approval_step = fields.Selection(
string='Approval Step',
selection=ApprovalStep.list,
compute='_compute_approval_step',
)
method = fields.Selection(
string='Approval Method',
selection=ApprovalMethods.list,
required=True,
default=ApprovalMethods.default,
readonly=True,
)
visibility = fields.Selection(
string='Visibility',
selection=DocumentVisibility.list,
required=True,
default=DocumentVisibility.default,
)
initiator_user_id = fields.Many2one(
string='Initiator',
comodel_name='res.users',
required=True,
default=lambda self: self.env.user,
readonly=True,
)
company_id = fields.Many2one(
string='Company',
comodel_name='res.company',
required=True,
default=lambda self: self.env.company,
readonly=True,
)
approval_team_id = fields.Many2one(
string='Approval Team',
comodel_name='xf.doc.approval.team',
readonly=True,
domain="[('company_id', '=', company_id)]",
)
approver_ids = fields.One2many(
string='Approvers',
comodel_name='xf.doc.approval.document.approver',
inverse_name='document_package_id',
readonly=True,
)
document_ids = fields.One2many(
string='Documents',
comodel_name='xf.doc.approval.document',
inverse_name='document_package_id',
readonly=True,
)
is_initiator = fields.Boolean('Is Initiator', compute='_compute_access')
is_approver = fields.Boolean('Is Approver', compute='_compute_access')
reject_reason = fields.Text('Reject Reason')
# Compute fields
@api.depends('state', 'approval_team_id')
def _compute_access(self):
for record in self:
# Check if the current user is initiator (true for admin)
record.is_initiator = self.env.user == record.initiator_user_id or self.env.user._is_admin()
# Check if the document needs approval from current user (true for admin)
current_approvers = record.get_current_approvers()
responsible = self.env.user in current_approvers.mapped('user_id') or self.env.user._is_admin()
record.is_approver = record.approval_state == 'pending' and responsible
@api.depends('approver_ids.state')
def _compute_approval_state(self):
for record in self:
approvers = record.approver_ids
if len(approvers) == len(approvers.filtered(lambda a: a.state == 'approved')):
record.approval_state = 'approved'
elif approvers.filtered(lambda a: a.state == 'rejected'):
record.approval_state = 'rejected'
elif approvers.filtered(lambda a: a.state == 'pending'):
record.approval_state = 'pending'
else:
record.approval_state = 'to approve'
@api.depends('approver_ids.state', 'approver_ids.step')
def _compute_approval_step(self):
for record in self:
approval_step = None
steps = record.approver_ids.mapped('step')
steps.sort()
for step in steps:
if record.approver_ids.filtered(lambda a: a.step == step and a.state != 'approved'):
approval_step = step
break
record.approval_step = approval_step
# Onchange handlers
@api.onchange('approval_team_id')
def onchange_approval_team(self):
if self.approval_team_id:
team_approvers = []
for team_approver in self.approval_team_id.approver_ids:
team_approvers += [{
'step': team_approver.step,
'user_id': team_approver.user_id.id,
'role': team_approver.role,
}]
approvers = self.approver_ids.browse([])
for a in team_approvers:
approvers += approvers.new(a)
self.approver_ids = approvers
@api.onchange('approver_ids')
def onchange_approvers(self):
if self.approval_team_id:
if self.approval_team_id.approver_ids.mapped('user_id') != self.approver_ids.mapped('user_id'):
self.approval_team_id = None
# Validation
@api.constrains('company_id')
def _validate_company(self):
for record in self:
record.approver_ids.validate_company(record.company_id)
@api.constrains('state', 'approver_ids')
def _check_approvers(self):
for record in self:
if record.state == 'approval' and not record.approver_ids:
raise ValidationError(_('Please add at least one approver!'))
@api.constrains('state', 'document_ids')
def _check_documents(self):
for record in self:
if record.state == 'approval' and not record.document_ids:
raise ValidationError(_('Please add at least one document!'))
# Helpers
def set_state(self, state, vals=None):
if vals is None:
vals = {}
vals.update({'state': state})
return self.write(vals)
def get_next_approvers(self):
self.ensure_one()
next_approvers = self.approver_ids.filtered(lambda a: a.state == 'to approve').sorted('step')
if not next_approvers:
return next_approvers
next_step = next_approvers[0].step
return next_approvers.filtered(lambda a: a.step == next_step)
def get_current_approvers(self):
self.ensure_one()
return self.approver_ids.filtered(lambda a: a.state == 'pending' and a.step == self.approval_step)
def get_current_approver(self):
self.ensure_one()
current_approvers = self.get_current_approvers()
if not current_approvers:
raise UserError(_('There are not approvers for this document package!'))
current_approver = current_approvers.filtered(lambda a: a.user_id == self.env.user)
if not current_approver and self.env.user._is_admin():
current_approver = current_approvers[0]
if not current_approver:
raise AccessError(_('You are not allowed to approve this document package!'))
return current_approver
def send_notification(self, view_ref, partner_ids):
for record in self:
record.message_post_with_source(
view_ref,
subject=_('Document Approval: %s') % record.name,
partner_ids=partner_ids,
subtype_xmlid='mail.mt_note',
)
# User actions
def action_send_for_approval(self):
for record in self:
if record.state == 'draft' and record.approver_ids:
# Subscribe approvers
record.message_subscribe(partner_ids=record.approver_ids.mapped('user_id').mapped('partner_id').ids)
if record.approval_state == 'pending':
raise UserError(_('The document package have already been sent for approval!'))
elif record.approval_state == 'approved':
raise UserError(_('The document package have already been approved!'))
elif record.approval_state == 'rejected':
raise UserError(_('The document package was rejected! To send it for approval again, please update document(s) first.'))
elif record.approval_state == 'to approve':
next_approvers = record.get_next_approvers()
if next_approvers:
if record.state == 'draft':
record.state = 'approval'
next_approvers.write({'state': 'pending'})
partner_ids = next_approvers.mapped('user_id').mapped('partner_id').ids
record.send_notification('xf_doc_approval.request_to_approve', partner_ids)
else:
raise UserError(_('There are not approvers for this document package!'))
def action_approve_wizard(self):
self.ensure_one()
current_approver = self.get_current_approver()
return current_approver.action_wizard('action_approve_wizard', _('Approve'))
def action_reject_wizard(self):
self.ensure_one()
current_approver = self.get_current_approver()
return current_approver.action_wizard('action_reject_wizard', _('Reject'))
def action_draft(self):
for record in self:
record.approver_ids.write({'state': 'to approve', 'notes': None})
record.write({'state': 'draft', 'reject_reason': None})
return True
def action_cancel(self):
if not self.env.user._is_admin() and self.filtered(lambda record: record.state == 'approved'):
raise UserError(_("Cannot cancel a document package that is approved."))
return self.set_state('cancelled')
def action_finish_approval(self):
for record in self:
if record.approval_state == 'approved':
record.state = 'approved'
else:
raise UserError(_('Document Package must be fully approved!'))
# Built-in methods
def unlink(self):
if any(self.filtered(lambda record: record.state not in ('draft', 'cancelled'))):
raise UserError(_('You cannot delete a record which is not draft or cancelled!'))
return super(DocApprovalDocumentPackage, self).unlink()
def _track_subtype(self, init_values):
self.ensure_one()
if 'state' in init_values and self.state == 'approval':
return self.env.ref('xf_doc_approval.mt_document_package_approval')
elif 'state' in init_values and self.state == 'approved':
return self.env.ref('xf_doc_approval.mt_document_package_approved')
elif 'state' in init_values and self.state == 'cancelled':
return self.env.ref('xf_doc_approval.mt_document_package_cancelled')
elif 'state' in init_values and self.state == 'rejected':
return self.env.ref('xf_doc_approval.mt_document_package_rejected')
return super(DocApprovalDocumentPackage, self)._track_subtype(init_values)
class DocApprovalDocument(models.Model):
_name = 'xf.doc.approval.document'
_description = 'Document'
document_package_id = fields.Many2one(
string='Document Package',
comodel_name='xf.doc.approval.document.package',
required=True,
ondelete='cascade',
)
name = fields.Char(
string='Name',
required=True,
translate=True,
)
file = fields.Binary(
string='File',
required=True,
attachment=True,
)
file_name = fields.Char(
string='File Name'
)
@api.onchange('file_name')
def _onchange_file_name(self):
if self.file_name and not self.name:
self.name = self.file_name
+57
View File
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
class Selection(object):
list = []
folded = []
default = None
@classmethod
def name(cls, state):
states_dict = dict(cls.list)
if state in states_dict:
return states_dict[state]
@classmethod
def values(cls):
return list(dict(cls.list))
class ApproverState(Selection):
list = [
('to approve', 'To Approve'),
('pending', 'Pending'),
('approved', 'Approved'),
('rejected', 'Rejected'),
]
default = list[0][0]
class ApprovalMethods(Selection):
list = [
('button', 'Button'),
]
default = list[0][0]
class DocumentState(Selection):
list = [
('draft', 'Draft'),
('approval', 'Approval'),
('approved', 'Approved'),
('cancelled', 'Cancelled'),
('rejected', 'Rejected'),
]
default = list[0][0]
class DocumentVisibility(Selection):
list = [
('all_users', 'All Users'),
('followers', 'Followers'),
('approvers', 'Approvers'),
]
default = list[0][0]
class ApprovalStep(Selection):
step_range = list(range(1, 21))
list = [("{:02d}".format(step), "{:02d}".format(step)) for step in step_range]
default = list[0][0]
+195
View File
@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from .selection import ApproverState, ApprovalMethods, ApprovalStep
class DocApprovalTeam(models.Model):
_name = 'xf.doc.approval.team'
_description = 'Doc Approval Team'
active = fields.Boolean('Active', default=True)
name = fields.Char(
string='Name',
required=True,
)
user_id = fields.Many2one(
string='Team Leader',
comodel_name='res.users',
required=True,
default=lambda self: self.env.user,
index=True
)
company_id = fields.Many2one(
string='Company',
comodel_name='res.company',
required=True,
default=lambda self: self.env.company.id,
index=True,
)
approver_ids = fields.One2many(
string='Approvers',
comodel_name='xf.doc.approval.team.approver',
inverse_name='team_id',
)
# Validation
@api.constrains('company_id')
def _check_team_company(self):
for team in self:
team.approver_ids.validate_company(team.company_id)
class DocApprovalApproverAbstract(models.Model):
_name = 'xf.doc.approval.approver.abstract'
_description = 'Abstract Approver'
_order = 'step'
_rec_name = 'user_id'
step = fields.Selection(
string='Step',
selection=ApprovalStep.list,
required=True,
default=ApprovalStep.default,
)
user_id = fields.Many2one(
string='Approver',
comodel_name='res.users',
required=True
)
role = fields.Char(
string='Role/Position',
required=True,
default="Approver"
)
# Onchange handlers
@api.onchange('user_id')
def _detect_user_role(self):
for approver in self:
# if user related to employee, try to get job title for hr.employee
employee = hasattr(approver.user_id, 'employee_ids') and getattr(approver.user_id, 'employee_ids')
employee_job_id = hasattr(employee, 'job_id') and getattr(employee, 'job_id')
employee_job_title = employee_job_id.name if employee_job_id else False
if employee_job_title:
approver.role = employee_job_title
continue
# if user related partner, try to get job title for res.partner
partner = approver.user_id.partner_id
partner_job_title = hasattr(partner, 'function') and getattr(partner, 'function')
if partner_job_title:
approver.role = partner_job_title
# Validation
@api.constrains('user_id')
def _check_users(self):
for approver in self:
if not approver.user_id.has_group('xf_doc_approval.group_xf_doc_approval_user'):
raise ValidationError(_('%s does not have access to the Doc Approval module.') % (approver.user_id.name,)
+ '\n' +
_('Please ask system administrator to add him/her to the Doc Approval module group first.'))
def validate_company(self, company):
if not company:
return
for approver in self:
if company not in approver.user_id.company_ids:
raise ValidationError(
_('%s does not have access to the company %s') % (approver.user_id.name, company.name))
class DocApprovalTeamApprover(models.Model):
_name = 'xf.doc.approval.team.approver'
_inherit = ['xf.doc.approval.approver.abstract']
_description = 'Approval Team Member'
team_id = fields.Many2one(
string='Team',
comodel_name='xf.doc.approval.team',
required=True,
ondelete='cascade'
)
# Validation
@api.constrains('user_id', 'team_id')
def _check_users(self):
for approver in self:
approver.validate_company(approver.team_id.company_id)
return super(DocApprovalTeamApprover, self)._check_users()
class DocApprovalDocumentApprover(models.Model):
_name = 'xf.doc.approval.document.approver'
_inherit = ['xf.doc.approval.approver.abstract']
_description = 'Doc Approver'
team_approver_id = fields.Many2one(
string='Doc Team Approver',
comodel_name='xf.doc.approval.team.approver',
ondelete='set null'
)
document_package_id = fields.Many2one(
string='Document Package',
comodel_name='xf.doc.approval.document.package',
required=True,
ondelete='cascade',
)
method = fields.Selection(
string='Method',
related='document_package_id.method',
readonly=True,
)
state = fields.Selection(
string='Status',
selection=ApproverState.list,
readonly=True,
required=True,
default=ApproverState.default
)
notes = fields.Text(
string='Notes',
readonly=True,
)
# Validation
@api.constrains('user_id', 'document_package_id')
def _check_users(self):
for approver in self:
approver.validate_company(approver.document_package_id.company_id)
return super(DocApprovalDocumentApprover, self)._check_users()
# User actions
def action_wizard(self, view_ref, window_title):
self.ensure_one()
view = self.env.ref('xf_doc_approval.' + view_ref)
return {
'name': window_title,
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': self._name,
'res_id': self.id,
'views': [(view.id, 'form')],
'view_id': view.id,
'target': 'new'
}
def action_approve(self):
for approver in self:
document_package = approver.document_package_id
approver.state = 'approved'
if document_package.approval_state == 'to approve':
document_package.sudo().action_send_for_approval()
elif document_package.approval_state == 'approved':
document_package.sudo().action_finish_approval()
def action_reject(self):
for approver in self:
approver.state = 'rejected'
approver.document_package_id.sudo().set_state('rejected', {'reject_reason': approver.notes})