remove all extra addons

This commit is contained in:
hoangvv 2025-01-21 10:10:11 +07:00
parent bb344d53fb
commit af02b1f171
3518 changed files with 0 additions and 953680 deletions

View File

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models

View File

@ -1,55 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Approvals',
'version': '1.0',
'category': 'Human Resources/Approvals',
'sequence': 190,
'summary': 'Create and validate approvals requests',
'description': """
This module manages approvals workflow
======================================
This module manages approval requests like business trips,
out of office, overtime, borrow items, general approvals,
procurements, contract approval, etc.
According to the approval type configuration, a request
creates next activities for the related approvers.
""",
'depends': ['mail', 'hr', 'product','hr_promote'],
'data': [
'security/approval_security.xml',
'security/ir.model.access.csv',
'data/approval_category_data.xml',
'data/mail_activity_type_data.xml',
'data/mail_message_subtype_data.xml',
'views/approval_category_views.xml',
'views/approval_category_approver_views.xml',
'views/approval_product_line_views.xml',
'views/approval_products_views.xml',
'views/approval_promotion_line_views.xml',
'views/approval_request_template.xml',
'report/approval_request_report.xml',
'views/approval_request_views.xml',
'views/res_users_views.xml',
'views/approval_condition_views.xml',
],
'demo':[
'data/approval_demo.xml',
],
'application': True,
'installable': True,
'assets': {
'web.assets_backend': [
'approvals/static/src/**',
],
'web.assets_tests': [
'approvals/static/tests/tours/**/*',
],
'web.assets_unit_tests': [
'approvals/static/tests/**/*',
('remove', 'approvals/static/tests/tours/**/*'),
],
},
'license': 'OEEL-1',
}

View File

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="base.user_admin" model="res.users">
<field eval="[(4, ref('approvals.group_approval_manager'))]" name="groups_id"/>
</record>
<record id="approval_category_data_business_trip" model="approval.category">
<field name="name">Business Trip</field>
<field name="image" type="base64" file="approvals/static/src/img/Suitcase.png"/>
<field name="sequence">1</field>
<field name="has_date">no</field>
<field name="has_period">required</field>
<field name="has_product">no</field>
<field name="has_quantity">no</field>
<field name="has_amount">no</field>
<field name="has_reference">no</field>
<field name="has_partner">no</field>
<field name="has_payment_method">no</field>
<field name="has_location">required</field>
<field name="requirer_document">optional</field>
<field name="approval_minimum">1</field>
<field name="manager_approval">approver</field>
</record>
<record id="approval_category_data_borrow_items" model="approval.category">
<field name="name">Borrow Items</field>
<field name="image" type="base64" file="approvals/static/src/img/Books.png"/>
<field name="sequence">10</field>
<field name="has_date">no</field>
<field name="has_period">required</field>
<field name="has_product">required</field>
<field name="has_quantity">optional</field>
<field name="has_amount">no</field>
<field name="has_reference">no</field>
<field name="has_partner">no</field>
<field name="has_payment_method">no</field>
<field name="has_location">no</field>
<field name="requirer_document">optional</field>
<field name="approval_minimum">1</field>
</record>
<record id="approval_category_data_general_approval" model="approval.category">
<field name="name">General Approval</field>
<field name="image" type="base64" file="approvals/static/src/img/Folder.png"/>
<field name="sequence">20</field>
<field name="has_date">optional</field>
<field name="has_period">optional</field>
<field name="has_product">optional</field>
<field name="has_quantity">optional</field>
<field name="has_amount">optional</field>
<field name="has_reference">optional</field>
<field name="has_partner">optional</field>
<field name="has_payment_method">optional</field>
<field name="has_location">optional</field>
<field name="requirer_document">required</field>
<field name="approval_minimum">1</field>
<field name="manager_approval">approver</field>
</record>
<record id="approval_category_data_contract_approval" model="approval.category">
<field name="name">Contract Approval</field>
<field name="image" type="base64" file="approvals/static/src/img/Contract.png"/>
<field name="sequence">30</field>
<field name="has_date">no</field>
<field name="has_period">no</field>
<field name="has_product">no</field>
<field name="has_quantity">no</field>
<field name="has_amount">optional</field>
<field name="has_reference">required</field>
<field name="has_partner">required</field>
<field name="has_payment_method">no</field>
<field name="has_location">no</field>
<field name="requirer_document">optional</field>
<field name="approval_minimum">1</field>
</record>
<record id="approval_category_data_payment_application" model="approval.category">
<field name="name">Payment Application</field>
<field name="image" type="base64" file="approvals/static/src/img/Credit-card.png"/>
<field name="sequence">40</field>
<field name="has_date">required</field>
<field name="has_period">no</field>
<field name="has_product">no</field>
<field name="has_quantity">no</field>
<field name="has_amount">required</field>
<field name="has_reference">no</field>
<field name="has_partner">required</field>
<field name="has_payment_method">required</field>
<field name="has_location">no</field>
<field name="requirer_document">optional</field>
<field name="approval_minimum">1</field>
</record>
<record id="approval_category_data_car_rental_application" model="approval.category">
<field name="name">Car Rental Application</field>
<field name="image" type="base64" file="approvals/static/src/img/Car.png"/>
<field name="sequence">50</field>
<field name="has_date">no</field>
<field name="has_period">required</field>
<field name="has_product">no</field>
<field name="has_quantity">no</field>
<field name="has_amount">no</field>
<field name="has_reference">no</field>
<field name="has_partner">no</field>
<field name="has_payment_method">no</field>
<field name="has_location">no</field>
<field name="requirer_document">optional</field>
<field name="approval_minimum">1</field>
</record>
<record id="approval_category_data_job_referral_award" model="approval.category">
<field name="name">Job Referral Award</field>
<field name="image" type="base64" file="approvals/static/src/img/Award.png"/>
<field name="sequence">60</field>
<field name="has_date">no</field>
<field name="has_period">no</field>
<field name="has_product">no</field>
<field name="has_quantity">no</field>
<field name="has_amount">no</field>
<field name="has_reference">no</field>
<field name="has_partner">required</field>
<field name="has_payment_method">no</field>
<field name="has_location">no</field>
<field name="requirer_document">optional</field>
<field name="approval_minimum">1</field>
</record>
<record id="approval_category_data_procurement" model="approval.category">
<field name="name">Procurement</field>
<field name="image" type="base64" file="approvals/static/src/img/Bag.png"/>
<field name="sequence">70</field>
<field name="has_date">no</field>
<field name="has_period">no</field>
<field name="has_product">no</field>
<field name="has_quantity">required</field>
<field name="has_amount">optional</field>
<field name="has_reference">no</field>
<field name="has_partner">no</field>
<field name="has_payment_method">no</field>
<field name="has_location">no</field>
<field name="requirer_document">optional</field>
<field name="approval_minimum">1</field>
</record>
<record id="approval_category_data_promote" model="approval.category">
<field name="name">Promote</field>
<field name="image" type="base64" file="approvals/static/src/img/Folder.png"/>
<field name="sequence">80</field>
<field name="has_date">no</field>
<field name="has_period">required</field>
<field name="has_product">no</field>
<field name="has_quantity">no</field>
<field name="has_amount">no</field>
<field name="has_reference">optional</field>
<field name="has_partner">no</field>
<field name="has_payment_method">no</field>
<field name="has_location">required</field>
<field name="requirer_document">optional</field>
</record>
</data>
</odoo>

View File

@ -1,85 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[
(3, ref('approvals.group_approval_user')),
(3, ref('approvals.group_approval_manager')),
]"/>
</record>
<record id="approval_request_demo_business_trip" model="approval.request">
<field name="name">Business trip to London</field>
<field name="request_status">pending</field>
<field name="location">London</field>
<field name="category_id" ref="approval_category_data_business_trip"/>
<field name="request_owner_id" ref="base.user_admin"/>
<field name="date_start" eval="time.strftime('%Y-05-01')"/>
<field name="date_end" eval="time.strftime('%Y-05-05')"/>
<field name="reason">Meeting with a potential customer.</field>
</record>
<record id="approval_approver_business_trip_1" model="approval.approver">
<field name="user_id" ref="base.user_demo"/>
<field name="request_id" ref="approval_request_demo_business_trip"/>
</record>
<record id="approval_approver_business_trip_2" model="approval.approver">
<field name="user_id" ref="base.user_admin"/>
<field name="request_id" ref="approval_request_demo_business_trip"/>
</record>
<record id="approval_request_demo_borrow_items" model="approval.request">
<field name="name">Borrow the screwdriver</field>
<field name="request_status">pending</field>
<field name="category_id" ref="approval_category_data_borrow_items"/>
<field name="request_owner_id" ref="base.user_demo"/>
<field name="date_start" eval="time.strftime('%Y-05-01')"/>
<field name="date_end" eval="time.strftime('%Y-05-01')"/>
<field name="product_line_ids" eval="[(0, 0, {'description': 'Screwdriver X15'})]"/>
<field name="reason">Need a screwdriver to fix the main door.</field>
</record>
<record id="approval_approver_borrow_items_1" model="approval.approver">
<field name="user_id" ref="base.user_admin"/>
<field name="request_id" ref="approval_request_demo_borrow_items"/>
</record>
<record id="approval_approver_borrow_items_2" model="approval.approver">
<field name="user_id" ref="base.user_demo"/>
<field name="request_id" ref="approval_request_demo_borrow_items"/>
</record>
<record id="approval_category_data_business_trip" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
<record id="approval_category_data_borrow_items" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
<record id="approval_category_data_general_approval" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
<record id="approval_category_data_contract_approval" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
<record id="approval_category_data_payment_application" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
<record id="approval_category_data_car_rental_application" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
<record id="approval_category_data_job_referral_award" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
<record id="approval_category_data_procurement" model="approval.category">
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
</record>
</data>
</odoo>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_activity_data_approval" model="mail.activity.type">
<field name="name">Approval</field>
<field name="icon">fa-check-circle</field>
<field name="sequence">4</field>
<field name="active">False</field>
<field name="res_model">approval.request</field>
</record>
</data>
</odoo>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="mt_approval_request_status" model="mail.message.subtype">
<field name="name">Approval Status Change</field>
<field name="res_model">Approval.Request</field>
<field name="default" eval="False"/>
<field name="description">Approval Status Change</field>
<field name="sequence">5</field>
</record>
</odoo>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import approval_category
from . import approval_category_approver
from . import approval_product_line
from . import approval_request
from . import mail_activity
from . import ir_attachment
from . import approval_category_condition
from . import approval_promotion_line

View File

@ -1,214 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import logging
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
CATEGORY_SELECTION = [
('required', 'Required'),
('optional', 'Optional'),
('no', 'None')]
class ApprovalCategory(models.Model):
# ----------------------------------- Private Attributes ---------------------------------
_name = 'approval.category'
_description = 'Approval Category'
_order = 'sequence, id'
_check_company_auto = True
DEFAULT_PYTHON_CODE = """# Available variables:
# - env: environment on which the action is triggered
# - model: model of the record on which the action is triggered; is a void recordset
# - record: record on which the action is triggered; may be void
# - records: recordset of all records on which the action is triggered in multi-mode; may be void
# - time, datetime, dateutil, timezone: useful Python libraries
# - float_compare: utility function to compare floats based on specific precision
# - b64encode, b64decode: functions to encode/decode binary data
# - log: log(message, level='info'): logging function to record debug information in ir.logging table
# - _logger: _logger.info(message): logger to emit messages in server logs
# - UserError: exception class for raising user-facing warning messages
# - Command: x2many commands namespace
# To return an action, assign: action = {...}\n\n\n\n"""
def _get_default_image(self):
default_image_path = 'approvals/static/src/img/Folder.png'
return base64.b64encode(tools.misc.file_open(default_image_path, 'rb').read())
# ----------------------------------- Fields Declaration ----------------------------------
name = fields.Char(string="Name", translate=True, required=True)
company_id = fields.Many2one(
'res.company', 'Company', copy=False,
required=True, index=True, default=lambda s: s.env.company)
active = fields.Boolean(default=True)
sequence = fields.Integer(string="Sequence")
description = fields.Char(string="Description", translate=True)
image = fields.Binary(string='Image', default=_get_default_image)
has_date = fields.Selection(CATEGORY_SELECTION, string="Has Date", default="no", required=True)
has_period = fields.Selection(CATEGORY_SELECTION, string="Has Period", default="no", required=True)
has_quantity = fields.Selection(CATEGORY_SELECTION, string="Has Quantity", default="no", required=True)
has_amount = fields.Selection(CATEGORY_SELECTION, string="Has Amount", default="no", required=True)
has_reference = fields.Selection(
CATEGORY_SELECTION, string="Has Reference", default="no", required=True,
help="An additional reference that should be specified on the request.")
has_partner = fields.Selection(CATEGORY_SELECTION, string="Has Contact", default="no", required=True)
has_payment_method = fields.Selection(CATEGORY_SELECTION, string="Has Payment", default="no", required=True)
has_location = fields.Selection(CATEGORY_SELECTION, string="Has Location", default="no", required=True)
has_product = fields.Selection(
CATEGORY_SELECTION, string="Has Product", default="no", required=True,
help="Additional products that should be specified on the request.")
has_promotion = fields.Selection(
CATEGORY_SELECTION, string="Has Promotion", default="no", required=True,
help="Additional promotion that should be specified on the request.")
requirer_document = fields.Selection([('required', 'Required'), ('optional', 'Optional')], string="Documents", default="optional", required=True)
approval_minimum = fields.Integer(string="Minimum Approval", default="1", required=True)
# invalid_minimum = fields.Boolean(compute='_compute_invalid_minimum')
# invalid_minimum_warning = fields.Char(compute='_compute_invalid_minimum')
approval_type = fields.Selection(string="Approval Type", selection=[],
help="Allows you to define which documents you would like to create once the request has been approved")
manager_approval = fields.Selection([('approver', 'Is Approver'), ('required', 'Is Required Approver')],
string="Employee's Manager",
help="""How the employee's manager interacts with this type of approval.
Empty: do nothing
Is Approver: the employee's manager will be in the approver list
Is Required Approver: the employee's manager will be required to approve the request.
""")
# ---------------------------------------- Relational -----------------------------------------
user_ids = fields.Many2many('res.users', compute='_compute_user_ids', string="Approver Users")
approver_ids = fields.One2many('approval.category.approver', 'category_id', string="Approvers")
conditions_ids = fields.One2many('approval.category.condition', 'category_id', string="Approval Condition")
approver_sequence = fields.Boolean('Approvers Sequence?', help="If checked, the approvers have to approve in sequence (one after the other). If Employee's Manager is selected as approver, they will be the first in line.")
request_to_validate_count = fields.Integer("Number of requests to validate", compute="_compute_request_to_validate_count")
automated_sequence = fields.Boolean('Automated Sequence?',
help="If checked, the Approval Requests will have an automated generated name based on the given code.")
sequence_code = fields.Char(string="Code")
sequence_id = fields.Many2one('ir.sequence', 'Reference Sequence',
copy=False, check_company=True)
model_id = fields.Many2one('ir.model', string='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_warning = fields.Boolean("Invalid minimum Warning",compute="_compute_invalid_minimum_warning")
# domain = fields.Char(string='Conditions')
# ---------------------------------------- Methods -----------------------------------------
# @api.depends('conditions_ids')
# def _compute_invalid_minimum(self):
# for record in self:
# # If you expect to compute the invalid_minimum for each condition, loop through them
# invalid_minimums = record.conditions_ids.mapped('invalid_minimum')
# if invalid_minimums:
# # Handle the logic for determining invalid_minimum (example)
# record.invalid_minimum = any(invalid_minimums)
# @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):
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_mapped_data = {category.id: count for category, count in requests_data}
for category in self:
category.request_to_validate_count = requests_mapped_data.get(category.id, 0)
# @api.depends_context('lang')
# @api.depends('approval_minimum', 'approver_ids', 'manager_approval')
# def _compute_invalid_minimum(self):
# for record in self:
# if record.approval_minimum > len(record.approver_ids) + int(bool(record.manager_approval)):
# record.invalid_minimum = True
# else:
# record.invalid_minimum = False
# record.invalid_minimum_warning = record.invalid_minimum and _('Your minimum approval exceeds the total of default approvers.')
@api.depends('approver_ids')
def _compute_user_ids(self):
for record in self:
record.user_ids = record.approver_ids.user_id
# @api.constrains('approval_minimum', 'approver_ids')
# def _constrains_approval_minimum(self):
# for record in self:
# if record.approval_minimum < len(record.approver_ids.filtered('required')):
# raise ValidationError(_('Minimum Approval must be equal or superior to the sum of required Approvers.'))
@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('approver_sequence', 'approval_minimum')
# def _constrains_approver_sequence(self):
# if any(a.approver_sequence and not a.approval_minimum for a in self):
# raise ValidationError(_('Approver Sequence can only be activated with at least 1 minimum approver.'))
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('automated_sequence'):
sequence = self.env['ir.sequence'].create({
'name': _('Sequence %(code)s', code=vals['sequence_code']),
'padding': 5,
'prefix': vals['sequence_code'],
'company_id': vals.get('company_id'),
})
vals['sequence_id'] = sequence.id
res = super().create(vals_list)
return res
def write(self, vals):
if 'sequence_code' in vals:
for approval_category in self:
sequence_vals = {
'name': _('Sequence %(code)s', code=vals['sequence_code']),
'padding': 5,
'prefix': vals['sequence_code'],
}
if approval_category.sequence_id:
approval_category.sequence_id.write(sequence_vals)
else:
sequence_vals['company_id'] = vals.get('company_id', approval_category.company_id.id)
sequence = self.env['ir.sequence'].create(sequence_vals)
approval_category.sequence_id = sequence
if 'company_id' in vals:
for approval_category in self:
if approval_category.sequence_id:
approval_category.sequence_id.company_id = vals.get('company_id')
res = super().write(vals)
return res
def create_request(self):
self.ensure_one()
# If category uses sequence, set next sequence as name
# (if not, set category name as default name).
return {
"type": "ir.actions.act_window",
"res_model": "approval.request",
"views": [[False, "form"]],
"context": {
'default_name': _('New') if self.automated_sequence else self.name,
'default_category_id': self.id,
'default_request_owner_id': self.env.user.id,
'default_request_status': 'new'
},
}

View File

@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ApprovalCategoryApprover(models.Model):
""" Intermediate model between approval.category and res.users
To know whether an approver for this category is required or not
"""
_name = 'approval.category.approver'
_description = 'Approval Type Approver'
_rec_name = 'user_id'
_order = 'sequence'
sequence = fields.Integer('Sequence', default=10)
category_id = fields.Many2one('approval.category', string='Approval Type')
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,
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)
existing_user_ids = fields.Many2many('res.users', compute='_compute_existing_user_ids')
required = fields.Boolean(default=False)
@api.depends('user_id', 'company_id')
def _compute_job_title(self):
for record in self:
if record.user_id and record.company_id:
# Search for the employee linked to the user and company
employee = self.env['hr.employee'].search([
('user_id', '=', record.user_id.id),
('company_id', '=', record.company_id.id)
], limit=1)
record.job_title = employee.job_title if employee else ''
else:
record.job_title = ''
@api.depends('condition_id')
def _compute_existing_user_ids(self):
for record in self:
record.existing_user_ids = record.condition_id.approver_ids.user_id

View File

@ -1,84 +0,0 @@
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class ApprovalConditions(models.Model):
_name = 'approval.category.condition'
_description = 'Approval Condition'
_order = 'sequence, id'
sequence = fields.Integer(default=1)
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.")
approval_minimum = fields.Integer(string="Minimum Approval", default=5, required=True)
invalid_minimum = fields.Boolean(compute='_compute_invalid_minimum')
invalid_minimum_warning = fields.Char(compute='_compute_invalid_minimum')
approver_ids = fields.One2many('approval.category.approver', 'condition_id', string="Approvers")
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_name = fields.Char(
string='Model Name',
related='model_id.model',
store=True,
readonly=True
)
domain = fields.Char(string="Conditions", readonly=False, store=True)
# @api.depends('category_id.model_id')
# def _compute_model_id(self):
# """Compute the model_id from the related approval category."""
# for record in self:
# record.model_id = record.category_id.model_id
@api.depends('approver_ids')
def _compute_user_ids(self):
for record in self:
record.user_ids = record.approver_ids.user_id
@api.depends('approval_minimum', 'approver_ids')
def _compute_invalid_minimum(self):
for record in self:
if record.approval_minimum >= len(record.approver_ids):
record.invalid_minimum = False
else:
record.invalid_minimum = True
record.invalid_minimum_warning = record.invalid_minimum and _('Your minimum approval exceeds the total of default approvers.')
# @api.depends('approver_ids')
# def _compute_user_ids(self):
# for record in self:
# record.user_ids = record.approver_ids.user_id
# @api.depends('model_name')
# def _compute_filter_id(self):
# for record in self:
# 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')
def _constrains_approval_minimum(self):
for record in self:
if record.approval_minimum < len(record.approver_ids):
raise ValidationError(_('Minimum Approval must be equal or superior to the sum of required Approvers.'))
@api.constrains('approver_sequence', 'approval_minimum')
def _constrains_approver_sequence(self):
for record in self:
if record.approver_sequence and record.approval_minimum < 1:
raise ValidationError(_('Approver Sequence can only be activated with at least 1 minimum approver.'))
def delete_condition(self):
for record in self:
record.unlink()

View File

@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class ApprovalProductLine(models.Model):
_name = 'approval.product.line'
_description = 'Product Line'
_check_company_auto = True
approval_request_id = fields.Many2one('approval.request', required=True)
description = fields.Char(
"Description", required=True,
compute="_compute_description", store=True, readonly=False, precompute=True)
company_id = fields.Many2one(
string='Company', related='approval_request_id.company_id',
store=True, readonly=True, index=True)
product_id = fields.Many2one('product.product', string="Products", check_company=True)
product_uom_id = fields.Many2one(
'uom.uom', string="Unit of Measure",
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True,
domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
quantity = fields.Float("Quantity", default=1.0)
@api.depends('product_id')
def _compute_description(self):
for line in self:
line.description = line.product_id.description_purchase or line.product_id.display_name
@api.depends('product_id')
def _compute_product_uom_id(self):
for line in self:
line.product_uom_id = line.product_id.uom_id

View File

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class ApprovalPromotionLine(models.Model):
_name = 'approval.promotion.line'
_description = 'Promote Line'
_check_company_auto = True
approval_request_id = fields.Many2one('approval.request', required=True)
description = fields.Char(
"Description", required=True,
compute="_compute_promotion", store=True, readonly=False, precompute=True)
company_id = fields.Many2one(
string='Company', related='approval_request_id.company_id',
store=True, readonly=True, index=True)
promote_id = fields.Many2one('hr.promote', string="Promotion", check_company=True)
current_job = fields.Char(
"Current Job", required=True,
compute="_compute_promotion", store=True, readonly=True, precompute=True)
new_designation = fields.Char(
"New Designation", required=True,
compute="_compute_promotion", store=True, readonly=True, precompute=True)
@api.depends('promote_id')
def _compute_promotion(self):
for line in self:
line.description = line.promote_id.description or line.promote_id.display_name
line.current_job = line.promote_id.job_id.name or ''
line.new_designation = line.promote_id.designation_id.name or ''

View File

@ -1,508 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
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
name = fields.Char(string="Approval Subject", tracking=True)
category_id = fields.Many2one('approval.category', string="Category", required=True,ondelete='cascade')
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)
user_ids = fields.Many2many('res.users', string="Users",
compute='_compute_user_ids', readonly=True)
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', 'Canceled'),
], default="new", compute="_compute_request_status",
store=True, index=True, tracking=True,
group_expand=True)
request_owner_id = fields.Many2one('res.users', string="Request Owner",
check_company=True, domain="[('company_ids', 'in', company_id)]",
default=lambda self: self.env.user)
user_status = fields.Selection([
('new', 'New'),
('pending', 'To Approve'),
('waiting', 'Waiting'),
('approved', 'Approved'),
('refused', 'Refused'),
('cancel', 'Canceled')], 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_ids = fields.One2many(comodel_name='ir.attachment', inverse_name='res_id', domain=[('res_model', '=', 'approval.request')], string='Attachments')
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)
promotion_line_ids = fields.One2many('approval.promotion.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")
has_promotion = fields.Selection(related="category_id.has_promotion")
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('approver_ids')
def _compute_user_ids(self):
for request in self:
request.user_ids = request.approver_ids.user_id
@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()
created_requests = super().create(vals_list)
self._check_conditions(created_requests)
for request in created_requests:
request.message_subscribe(partner_ids=request.request_owner_id.partner_id.ids)
return created_requests
def _check_conditions(self,request):
data = request
if isinstance(request, bool):
_logger.error(f"Expected 'vals' to be a list, got {type(request)}")
data = self
for request in data:
if request.has_promotion:
promotion_lines = request.promotion_line_ids
if promotion_lines:
conditions = request.category_id.conditions_ids
for condition in conditions:
satisfy_condition = False
domain = condition.domain if condition.domain else []
# Ensure domain is in list or tuple format
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)
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()
self.filtered(lambda a: a.has_promotion).promotion_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), ('company_id', '=', self.company_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'})
# Send approval accepted message
for approval in self:
if approval.request_owner_id.partner_id:
body = _("The request created on %(create_date)s by %(request_owner)s has been accepted.",
create_date=approval.create_date.date(),
request_owner=approval.request_owner_id.name)
subject = _("The request %(request_name)s for %(request_owner)s has been accepted",
request_name=approval.name,
request_owner=approval.request_owner_id.name)
approval.message_notify(
body=body,
subject=subject,
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()._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'})
# Send approval refused message
for approval in self:
if approval.request_owner_id.partner_id:
body = _("The request created on %(create_date)s by %(request_owner)s has been refused.",
create_date=approval.create_date.date(),
request_owner=approval.request_owner_id.name)
subject = _("The request %(request_name)s for %(request_owner)s has been refused",
request_name=approval.name,
request_owner=approval.request_owner_id.name)
approval.message_notify(
body=body,
subject=subject,
partner_ids=approval.request_owner_id.partner_id.ids,
)
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):
if 'request_owner_id' in vals:
for approval in self:
approval.message_unsubscribe(partner_ids=approval.request_owner_id.partner_id.ids)
res = super().write(vals)
self._check_conditions(res)
if 'request_owner_id' in vals:
for approval in self:
approval.message_subscribe(partner_ids=approval.request_owner_id.partner_id.ids)
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
def _track_subtype(self, init_values):
self.ensure_one()
if 'request_status' in init_values:
return self.env.ref('approvals.mt_approval_request_status')
return super()._track_subtype(init_values)
@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), ('id', '=', user_id)]")
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

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, _
from odoo.exceptions import UserError
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
@api.ondelete(at_uninstall=False)
def _unlink_approved_approval_request(self):
"""
Prevent attachment deletion for an approval request
that is in the approved, refused or cancel state.
"""
approval_request_ids = [attachment.res_id for attachment in self if attachment.res_model == 'approval.request' and not attachment.res_field]
if not approval_request_ids:
return
approval_requests = self.env['approval.request'].browse(approval_request_ids)
for approval_request in approval_requests:
if approval_request.request_status in ['approved', 'refused', 'cancel']:
raise UserError(_("You cannot unlink an attachment which is linked to a validated, refused or cancelled approval request."))

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo.addons.mail.tools.discuss import Store
class MailActivity(models.Model):
_inherit = "mail.activity"
def _to_store(self, store: Store):
super()._to_store(store)
activity_type_approval_id = self.env.ref("approvals.mail_activity_data_approval")
for activity in self.filtered(
lambda activity: activity["res_model"] == "approval.request"
and activity.activity_type_id == activity_type_approval_id
):
request = self.env["approval.request"].browse(activity["res_id"])
approver = request.approver_ids.filtered(
lambda approver: activity.user_id == approver.user_id
)
store.add(activity, {"approver_id": approver.id, "approver_status": approver.status})

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_report_approval_request" model="ir.actions.report">
<field name="name">Approval Request</field>
<field name="model">approval.request</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">approvals.report_approval_request</field>
<field name="report_file">approvals.report_approval_request</field>
<field name="print_report_name">'Approval - %s' % object.name</field>
<field name="binding_model_id" ref="model_approval_request"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@ -1,193 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.module.category" id="base.module_category_human_resources_approvals">
<field name="sequence">9</field>
</record>
<record id="group_approval_user" model="res.groups">
<field name="name">Officer: Approve all requests</field>
<field name="category_id" ref="base.module_category_human_resources_approvals"/>
<field name="implied_ids" eval="[(6, 0, [ref('base.group_user')])]"/>
<field name="comment">The user will be able to access all requests and approve/refuse them.</field>
</record>
<record id="group_approval_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="comment">The user will have access to the approvals configuration.</field>
<field name="category_id" ref="base.module_category_human_resources_approvals"/>
<field name="implied_ids" eval="[(4, ref('group_approval_user'))]"/>
</record>
<data noupdate="1">
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4,ref('approvals.group_approval_manager'))]"/>
</record>
<!-- Regular User -->
<record id="approval_request_request_approver_rule" model="ir.rule">
<field name="name">Approval Request: request approver rule</field>
<field name="model_id" ref="model_approval_request"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">['|', '|',
('request_owner_id', '=', user.id),
('approver_ids.user_id','=', user.id),
'&amp;',
('category_id.manager_approval', 'in', ['approver', 'required']),
('request_owner_id.parent_id.user_id', '=', user.id)]</field>
</record>
<record id="approval_request_unlink_request_owner_rule" model="ir.rule">
<field name="name">Approval Request: unlink request owner rule</field>
<field name="model_id" ref="model_approval_request"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">[('request_owner_id', '=', user.id), ('request_status', 'in', ['cancel', 'new'])]</field>
</record>
<record id="approval_approver_user_read_own" model="ir.rule">
<field name="name">Approval Approver: read own request</field>
<field name="model_id" ref="model_approval_approver"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">['|', ('request_id.request_owner_id', '=', user.id), ('user_id', '=', user.id)]</field>
</record>
<record id="approval_approver_user_unlink_own" model="ir.rule">
<field name="name">Approval Approver: unlink own request</field>
<field name="model_id" ref="model_approval_approver"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">['|', ('request_id.request_owner_id', '=', user.id), ('user_id', '=', user.id), ('request_id.request_status', '=', 'new')]</field>
</record>
<record id="approval_approver_user_change_own" model="ir.rule">
<field name="name">Approval Approver: change own</field>
<field name="model_id" ref="model_approval_approver"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
</record>
<record id="approval_approver_user_create" model="ir.rule">
<field name="name">Approval Approver: create</field>
<field name="model_id" ref="model_approval_approver"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">[('request_id.request_owner_id', '=', user.id), ('request_id.request_status', 'in', ('new', 'cancel'))]</field>
</record>
<!-- Approval User -->
<record id="approval_request_user" model="ir.rule">
<field name="name">Approval Request: user</field>
<field name="model_id" ref="model_approval_request"/>
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="approval_request_user_unlink" model="ir.rule">
<field name="name">Approval Request: user unlink</field>
<field name="model_id" ref="model_approval_request"/>
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">[('request_status','=','cancel')]</field>
</record>
<record id="approval_approver_manager" model="ir.rule">
<field name="name">Approval Approver: manage all</field>
<field name="model_id" ref="model_approval_approver"/>
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="approval_approver_manager_unlink" model="ir.rule">
<field name="name">Approval Approver: unlink unapproved</field>
<field name="model_id" ref="model_approval_approver"/>
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">[('request_id.request_status', 'in', ['new', 'pending', 'cancel'])]</field>
</record>
<!-- Approval Administrator -->
<record id="approval_request_manager" model="ir.rule">
<field name="name">Approval Request: manager</field>
<field name="model_id" ref="model_approval_request"/>
<field name="groups" eval="[(4, ref('group_approval_manager'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<record id="approval_approver_manager" model="ir.rule">
<field name="name">Approval Approver: manager</field>
<field name="model_id" ref="model_approval_approver"/>
<field name="groups" eval="[(4, ref('group_approval_manager'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<!-- Multi-company rules -->
<record model="ir.rule" id="approval_request_rule">
<field name="name">approval_request multi-company</field>
<field name="model_id" search="[('model','=','approval.request')]" model="ir.model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="approval_category_rule">
<field name="name">approval_category multi-company</field>
<field name="model_id" search="[('model','=','approval.category')]" model="ir.model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="approval_approver_rule">
<field name="name">approval_approver multi-company</field>
<field name="model_id" search="[('model','=','approval.approver')]" model="ir.model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="approval_product_line_rule">
<field name="name">approval_product_line multi-company</field>
<field name="model_id" search="[('model','=','approval.product.line')]" model="ir.model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
</data>
<function model="ir.rule" name="write">
<value eval="[ref('approvals.approval_request_unlink_request_owner_rule')]"/>
<value eval="{'domain_force': '[(\'request_owner_id.id\', \'=\', user.id), (\'request_status\', \'in\', (\'new\', \'cancel\'))]'}"/>
</function>
</odoo>

View File

@ -1,21 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_approval_request,access_approval_request,model_approval_request,base.group_user,1,1,1,1
access_approval_request_user,access_approval_request,model_approval_request,group_approval_user,1,1,1,1
access_approval_request_manager,access_approval_request,model_approval_request,group_approval_manager,1,1,1,1
access_approval_category,access_approval_category,model_approval_category,base.group_user,1,0,0,0
access_approval_category_user,access_approval_category,model_approval_category,group_approval_user,1,0,0,0
access_approval_category_manager,access_approval_category,model_approval_category,group_approval_manager,1,1,1,1
access_approval_category_approver,access_approval_category_approver,model_approval_category_approver,base.group_user,1,0,0,0
access_approval_category_condition,access_approval_category_condition,model_approval_category_condition,base.group_user,1,1,1,1
access_approval_category_approver_user,access_approval_category_approver,model_approval_category_approver,group_approval_user,1,0,0,0
access_approval_category_approver_manager,access_approval_category_approver,model_approval_category_approver,group_approval_manager,1,1,1,1
access_approval_approver,access_approval_approver,model_approval_approver,base.group_user,1,1,1,1
access_approval_approver_user,access_approval_approver,model_approval_approver,group_approval_user,1,1,1,1
access_approval_approver_manager,access_approval_approver,model_approval_approver,group_approval_manager,1,1,1,1
access_approval_product_line_user,access_approval_product_line,model_approval_product_line,base.group_user,1,1,1,1
access_approval_product_line_manager,access_approval_product_line,model_approval_product_line,group_approval_manager,1,1,1,1
access_approval_promotion_line_user,access_approval_promotion_line,model_approval_promotion_line,base.group_user,1,1,1,1
access_approval_promotion_line,access_approval_promotion_line,model_approval_promotion_line,base.group_user,1,1,1,1
access_approval_promotion_line_user,access_approval_promotion_line,model_approval_promotion_line,base.group_user,1,1,1,1
access_approval_promotion_line_manager,access_approval_promotion_line,model_approval_promotion_line,group_approval_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_approval_request access_approval_request model_approval_request base.group_user 1 1 1 1
3 access_approval_request_user access_approval_request model_approval_request group_approval_user 1 1 1 1
4 access_approval_request_manager access_approval_request model_approval_request group_approval_manager 1 1 1 1
5 access_approval_category access_approval_category model_approval_category base.group_user 1 0 0 0
6 access_approval_category_user access_approval_category model_approval_category group_approval_user 1 0 0 0
7 access_approval_category_manager access_approval_category model_approval_category group_approval_manager 1 1 1 1
8 access_approval_category_approver access_approval_category_approver model_approval_category_approver base.group_user 1 0 0 0
9 access_approval_category_condition access_approval_category_condition model_approval_category_condition base.group_user 1 1 1 1
10 access_approval_category_approver_user access_approval_category_approver model_approval_category_approver group_approval_user 1 0 0 0
11 access_approval_category_approver_manager access_approval_category_approver model_approval_category_approver group_approval_manager 1 1 1 1
12 access_approval_approver access_approval_approver model_approval_approver base.group_user 1 1 1 1
13 access_approval_approver_user access_approval_approver model_approval_approver group_approval_user 1 1 1 1
14 access_approval_approver_manager access_approval_approver model_approval_approver group_approval_manager 1 1 1 1
15 access_approval_product_line_user access_approval_product_line model_approval_product_line base.group_user 1 1 1 1
16 access_approval_product_line_manager access_approval_product_line model_approval_product_line group_approval_manager 1 1 1 1
17 access_approval_promotion_line_user access_approval_promotion_line model_approval_promotion_line base.group_user 1 1 1 1
18 access_approval_promotion_line access_approval_promotion_line model_approval_promotion_line base.group_user 1 1 1 1
19 access_approval_promotion_line_user access_approval_promotion_line model_approval_promotion_line base.group_user 1 1 1 1
20 access_approval_promotion_line_manager access_approval_promotion_line model_approval_promotion_line group_approval_manager 1 1 1 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1 +0,0 @@
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M28 16c0 5.523-4.477 10-10 10S8 21.523 8 16 12.477 6 18 6s10 4.477 10 10Z" fill="#1AD3BB"/><path d="M43 24a5 5 0 1 1-10 0 5 5 0 0 1 10 0Zm-27 7h26a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4H16V31Z" fill="#005E7A"/><path d="M4 31h16c7.18 0 13 5.82 13 13H17C9.82 44 4 38.18 4 31Z" fill="#1AD3BB"/><path d="M23.549 12.128 22.425 11l-5.532 5.556-3.319-3.335-1.123 1.129a1.552 1.552 0 0 0 0 2.188L16.892 21l6.657-6.685a1.55 1.55 0 0 0 0-2.187Z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,52 +0,0 @@
// .o_approvals_category_actions_field,
// .o_approvals_category_kanban_view {
// .o_kanban_ungrouped {
// padding: 0;
// .o_kanban_record {
// width: 30%;
// margin: 0;
// &.o_kanban_ghost {
// display: none;
// }
// }
// }
// }
// .o_approvals_category_actions_field .o_kanban_ghost {
// display: none;
// }
// .o_approvals_category_kanban_view {
// .o_kanban_grouped .row {
// flex-direction: column !important;
// gap: 0.5rem !important;
// > * {
// width: 100% !important;
// > * {
// margin: 0 0.5rem !important;
// }
// }
// }
// .o_kanban_ungrouped .o_kanban_record .oe_kanban_global_click {
// border-top: 0;
// display: flex;
// align-items: center;
// .o_widget_web_ribbon {
// align-self: flex-start;
// }
// }
// }
// .o_form_view .o_group .o_field_widget, .o_form_view .o_inner_group .o_field_widget {
// width: 50%;
// }
// .o_approvals_category_error {
// pre {
// max-height: 25vh !important;
// }
// }

View File

@ -1,41 +0,0 @@
import { _t } from "@web/core/l10n/translation";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { useService } from "@web/core/utils/hooks";
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
export class ApprovalCategoryKanbanController extends KanbanController {
async setup() {
super.setup();
this.action = useService("action");
}
OpenNewApprovalRequest() {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "approval.request",
views: [[false, "form"]],
target: "current",
});
}
}
export class ApprovalKanbanRenderer extends KanbanRenderer {
async archiveRecord(record, active) {
if (active) {
this.dialog.add(ConfirmationDialog, {
body: _t("Are you sure that you want to archive this record?"),
confirmLabel: _t("Archive"),
confirm: () => {
record.archive();
this.props.list.load();
},
cancel: () => {},
});
} else {
record.unarchive();
this.props.list.load();
}
}
}

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="approvals.ApprovalsCategoryKanbanView.Buttons">
<button type="button" class="btn btn-primary" t-on-click="OpenNewApprovalRequest">
New Request
</button>
</t>
</templates>

View File

@ -1,13 +0,0 @@
import { registry } from "@web/core/registry";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { ApprovalKanbanRenderer, ApprovalCategoryKanbanController } from "./approvals_category_kanban_controller";
export const approvalsCategoryKanbanView = {
...kanbanView,
Controller: ApprovalCategoryKanbanController,
Renderer: ApprovalKanbanRenderer,
buttonTemplate: "approvals.ApprovalsCategoryKanbanView.Buttons",
};
registry.category("views").add("approvals_category_kanban", approvalsCategoryKanbanView);

View File

@ -1,24 +0,0 @@
/** @odoo-module */
import { Approval } from "@approvals/web/activity/approval";
import { ActivityListPopoverItem } from "@mail/core/web/activity_list_popover_item";
import { patch } from "@web/core/utils/patch";
Object.assign(ActivityListPopoverItem.components, { Approval });
patch(ActivityListPopoverItem.prototype, {
get hasEditButton() {
if (this.props.activity.approval) {
return false;
}
return super.hasEditButton;
},
get hasMarkDoneButton() {
if (this.props.activity.approval) {
return false;
}
return super.hasMarkDoneButton;
},
});

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ActivityListPopoverItem" t-inherit-mode="extension">
<xpath expr="//button[hasclass('o-mail-ActivityListPopoverItem-markAsDone')]" position="after">
<Approval t-if="props.activity.approval" activity="this.props.activity" onChange="this.props.onActivityChanged"/>
</xpath>
</t>
</templates>

View File

@ -1,19 +0,0 @@
/** @odoo-module */
import { Activity } from "@mail/core/web/activity_model";
import { patch } from "@web/core/utils/patch";
patch(Activity, {
_insert(data) {
const activity = super._insert(...arguments);
if ("approver_id" in data && "approver_status" in data) {
if (!data.approver_id) {
delete activity.approval;
} else {
activity.approval = { id: data.approver_id, status: data.approver_status };
}
}
return activity;
},
});

View File

@ -1,7 +0,0 @@
/** @odoo-module */
import { Activity } from "@mail/core/web/activity";
import { Approval } from "@approvals/web/activity/approval";
Object.assign(Activity.components, { Approval });

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.Activity" t-inherit-mode="extension">
<xpath expr="//t[@name='tools']" position="replace">
<Approval t-if="props.activity.approval" activity="props.activity" onChange="props.reloadParentView"/>
<t t-else="">$0</t>
</xpath>
</t>
</templates>

View File

@ -1,38 +0,0 @@
/* @odoo-module */
import { Component, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
/**
* @typedef {Object} Props
* @property {import("@mail/core/web/activity_model").Activity} activity
* @extends {Component<Props, Env>}
*/
export class Approval extends Component {
static template = "approvals.Approval";
static props = {
activity: Object,
onChange: Function,
};
setup() {
this.store = useState(useService("mail.store"));
}
async onClickApprove() {
await this.env.services.orm.call("approval.approver", "action_approve", [
this.props.activity.approval.id,
]);
this.props.activity.remove();
this.props.onChange();
}
async onClickRefuse() {
await this.env.services.orm.call("approval.approver", "action_refuse", [
this.props.activity.approval.id,
]);
this.props.activity.remove();
this.props.onChange();
}
}

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="approvals.Approval">
<div t-if="props.activity.approval.status === 'pending'">
<t t-if="props.activity.user_id[0] === store.self.userId">
<button class="btn btn-success btn-link ps-0 pt-0" t-on-click="onClickApprove">
<i class="fa fa-check"/> Approve
</button>
<button class="btn btn-danger btn-link pt-0" t-on-click="onClickRefuse">
<i class="fa fa-times"/> Refuse
</button>
</t>
<span t-else="" class="text-warning ps-0 pt-0">
<i class="fa fa-pencil"/> To Approve
</span>
</div>
</t>
</templates>

View File

@ -1,137 +0,0 @@
import { describe, expect, test } from "@odoo/hoot";
import { Deferred } from "@odoo/hoot-mock";
import {
assertSteps,
click,
contains,
start,
startServer,
step,
openFormView,
} from "@mail/../tests/mail_test_helpers";
import { serverState, onRpc } from "@web/../tests/web_test_helpers";
import { defineApprovalsModels } from "@approvals/../tests/approvals_test_helpers";
describe.current.tags("desktop");
defineApprovalsModels();
test("activity with approval to be made by logged user", async () => {
const pyEnv = await startServer();
const requestId = pyEnv["approval.request"].create({});
pyEnv["approval.approver"].create({
request_id: requestId,
status: "pending",
user_id: serverState.userId,
});
pyEnv["mail.activity"].create({
can_write: true,
res_id: requestId,
res_model: "approval.request",
user_id: serverState.userId,
});
await start();
await openFormView("approval.request", requestId);
await contains(".o-mail-Activity");
await contains(".o-mail-Activity-sidebar");
await contains(".o-mail-Activity-user");
await contains(".o-mail-Activity-note", { count: 0 });
await contains(".o-mail-Activity-details", { count: 0 });
await contains(".o-mail-Activity-mailTemplates", { count: 0 });
await contains(".o-mail-Activity .btn", { count: 0, text: "Edit" });
await contains(".o-mail-Activity .btn", { count: 0, text: "Cancel" });
await contains(".o-mail-Activity .btn", { count: 0, text: "Mark Done" });
await contains(".o-mail-Activity .btn", { count: 0, text: "Upload Document" });
await contains(".o-mail-Activity button", { text: "Approve" });
await contains(".o-mail-Activity button", { text: "Refuse" });
});
test("activity with approval to be made by another user", async () => {
const pyEnv = await startServer();
const requestId = pyEnv["approval.request"].create({});
const userId = pyEnv["res.users"].create({
partner_id: pyEnv["res.partner"].create({}),
});
pyEnv["approval.approver"].create({
request_id: requestId,
status: "pending",
user_id: userId,
});
pyEnv["mail.activity"].create({
can_write: true,
res_id: requestId,
res_model: "approval.request",
user_id: userId,
});
await start();
await openFormView("approval.request", requestId);
await contains(".o-mail-Activity");
await contains(".o-mail-Activity-sidebar");
await contains(".o-mail-Activity-user");
await contains(".o-mail-Activity-note", { count: 0 });
await contains(".o-mail-Activity-details", { count: 0 });
await contains(".o-mail-Activity-mailTemplates", { count: 0 });
await contains(".o-mail-Activity .btn", { count: 0, text: "Edit" });
await contains(".o-mail-Activity .btn", { count: 0, text: "Cancel" });
await contains(".o-mail-Activity .btn", { count: 0, text: "Mark Done" });
await contains(".o-mail-Activity .btn", { count: 0, text: "Upload Document" });
await contains(".o-mail-Activity button", { count: 0, text: "Approve" });
await contains(".o-mail-Activity button", { count: 0, text: "Refuse" });
await contains(".o-mail-Activity span", { text: "To Approve" });
});
test("approve approval", async () => {
const pyEnv = await startServer();
const requestId = pyEnv["approval.request"].create({});
pyEnv["approval.approver"].create({
request_id: requestId,
status: "pending",
user_id: serverState.userId,
});
pyEnv["mail.activity"].create({
can_write: true,
res_id: requestId,
res_model: "approval.request",
user_id: serverState.userId,
});
const def = new Deferred();
onRpc("approval.approver", "action_approve", (args) => {
expect(args.args.length).toBe(1);
expect(args.args[0]).toBe(requestId);
step("action_approve");
def.resolve();
});
await start();
await openFormView("approval.request", requestId);
await click(".o-mail-Activity button", { text: "Approve" });
await def;
assertSteps(["action_approve"]);
});
test("refuse approval", async () => {
const pyEnv = await startServer();
const requestId = pyEnv["approval.request"].create({});
pyEnv["approval.approver"].create({
request_id: requestId,
status: "pending",
user_id: serverState.userId,
});
pyEnv["mail.activity"].create({
can_write: true,
res_id: requestId,
res_model: "approval.request",
user_id: serverState.userId,
});
const def = new Deferred();
onRpc("approval.approver", "action_refuse", (args) => {
expect(args.args.length).toBe(1);
expect(args.args[0]).toBe(requestId);
step("action_refuse");
def.resolve();
});
await start();
await openFormView("approval.request", requestId);
await click(".o-mail-Activity button", { text: "Refuse" });
await def;
assertSteps(["action_refuse"]);
});

View File

@ -1,16 +0,0 @@
import { defineModels } from "@web/../tests/web_test_helpers";
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { ApprovalRequest } from "@approvals/../tests/mock_server/mock_models/approval_request";
import { ApprovalApprover } from "@approvals/../tests/mock_server/mock_models/approval_approver";
import { MailActivity } from "@approvals/../tests/mock_server/mock_models/mail_activity";
export function defineApprovalsModels() {
return defineModels(approvalsModels);
}
export const approvalsModels = {
...mailModels,
ApprovalRequest,
ApprovalApprover,
MailActivity
};

View File

@ -1,13 +0,0 @@
import { models } from "@web/../tests/web_test_helpers";
export class ApprovalApprover extends models.ServerModel {
_name = "approval.approver";
action_approve() {
return true;
}
action_refuse() {
return true;
}
}

View File

@ -1,14 +0,0 @@
import { models } from "@web/../tests/web_test_helpers";
import { DEFAULT_MAIL_SEARCH_ID, DEFAULT_MAIL_VIEW_ID } from "@mail/../tests/mock_server/mock_models/constants";
export class ApprovalRequest extends models.ServerModel {
_name = "approval.request";
_views = {
[`search, ${DEFAULT_MAIL_SEARCH_ID}`]: /* xml */ `<search/>`,
[`form,${DEFAULT_MAIL_VIEW_ID}`]: /* xml */ `
<form>
<chatter/>
</form>`,
};
}

View File

@ -1,24 +0,0 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
export class MailActivity extends mailModels.MailActivity {
/** @param {number[]} ids */
_to_store(ids, store) {
super._to_store(...arguments);
for (const activity of this._filter([
["id", "in", ids],
["res_model", "=", "approval.request"],
])) {
// check on activity type being approval not done here for simplicity
const [approver] = this.env["approval.approver"]._filter([
["request_id", "=", activity.res_id],
["user_id", "=", activity.user_id],
]);
if (approver) {
store.add(this.browse(activity.id), {
approver_id: approver.id,
approver_status: approver.status,
});
}
}
}
}

View File

@ -1,117 +0,0 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("approvals_tour", {
url: "/odoo",
steps: () => [
{
trigger: '.o_app[data-menu-xmlid="approvals.approvals_menu_root"]',
content: "open approvals app",
run: "click",
},
{
trigger: "button.oe_kanban_action:first",
content: "create new request",
run: "click",
},
{
trigger: '.o_field_widget[name="name"] input',
content: "give name",
run: "edit Business Trip To Berlin",
},
{
trigger: '.o_field_widget[name="date_start"] input',
content: "give start date",
run: "edit 12/13/2018 13:00:00",
},
{
trigger: '.o_field_widget[name="date_end"] input',
content: "give end date",
run: "edit 12/20/2018 13:00:00",
},
{
trigger: '.o_field_widget[name="location"] input',
content: "give location",
run: "edit Berlin, Schulz Hotel",
},
{
trigger: 'div[name="reason"] .odoo-editor-editable',
content: "give description",
run: "editor (We need to go, because reason (and also for beer)))",
},
{
trigger: 'a:contains("Approver(s)"):first',
content: "open approvers page",
run: "click",
},
{
trigger: ".o_field_x2many_list_row_add > a",
content: "add an approver",
run: "click",
},
{
content: "select an approver",
trigger: ".o_selected_row .o_input_dropdown input",
run: "edit Marc",
},
{
isActive: ["auto"],
trigger: ".ui-autocomplete > li > a:contains(Marc)",
run: "click",
},
{
trigger: ".o_form_button_save",
content: "save the request",
run: "click",
},
{
trigger: "button[name=action_confirm]:enabled",
content: "confirm the request",
run: "click",
},
{
trigger: ".o-mail-Activity button:contains('Approve')",
content: "approve the request via activity",
run: "click",
},
{
trigger: 'button[name="action_withdraw"]',
content: "withdraw approver",
run: "click",
},
{
trigger: 'button[name="action_refuse"]',
content: "refuse request",
run: "click",
},
{
trigger: 'button[aria-checked="true"][data-value="refused"]',
content: "wait the request status compute",
},
{
trigger: 'button[name="action_cancel"]',
content: "cancel request",
run: "click",
},
{
trigger: 'button[name="action_draft"]',
content: "back the request to draft",
run: "click",
},
{
trigger: "button[name=action_confirm]:enabled",
content: "confirm the request again",
run: "click",
},
{
trigger: 'button[name="action_approve"]',
content: "approve request",
run: "click",
},
{
trigger: 'button[name="action_withdraw"]',
content: "wait the the request to be approved",
},
],
});

View File

@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_approvals
from . import test_ui

View File

@ -1,202 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command, fields
from odoo.tests import common, Form
from odoo.exceptions import UserError
class TestRequest(common.TransactionCase):
def test_compute_request_status(self):
category_test = self.env.ref('approvals.approval_category_data_business_trip')
requester_user = self.env.ref('base.user_admin')
record = self.env['approval.request'].create({
'name': 'test request',
'request_owner_id': requester_user.id,
'category_id': category_test.id,
'date_start': fields.Datetime.now(),
'date_end': fields.Datetime.now(),
'location': 'testland'
})
first_approver = self.env['approval.approver'].create({
'user_id': 1,
'request_id': record.id,
'status': 'new'})
second_approver = self.env['approval.approver'].create({
'user_id': 2,
'request_id': record.id,
'status': 'new'})
record.approver_ids = (first_approver | second_approver)
self.assertEqual(record.request_status, 'new')
record.action_confirm()
# Test case 1: Min approval = 1
self.assertEqual(record.request_status, 'pending')
record.action_approve(first_approver)
self.assertEqual(record.request_status, 'approved')
record.action_approve(second_approver)
self.assertEqual(record.request_status, 'approved')
record.action_withdraw(first_approver)
self.assertEqual(record.request_status, 'approved')
record.action_refuse(first_approver)
self.assertEqual(record.request_status, 'refused')
# Test case 2: Min approval = 1
category_test.approval_minimum = 2
record.action_withdraw(first_approver)
record.action_withdraw(second_approver)
self.assertEqual(record.request_status, 'pending')
record.action_approve(first_approver)
self.assertEqual(record.request_status, 'pending')
record.action_approve(second_approver)
self.assertEqual(record.request_status, 'approved')
record.action_withdraw(second_approver)
self.assertEqual(record.request_status, 'pending')
record.action_refuse(second_approver)
self.assertEqual(record.request_status, 'refused')
# Test case 3: Check that cancel is erasing the old validations
record.action_cancel()
self.assertEqual(first_approver.status, 'cancel')
self.assertEqual(second_approver.status, 'cancel')
self.assertEqual(record.request_status, 'cancel')
# Test case 4: Set the approval request to draft
record.action_draft()
self.assertEqual(first_approver.status, 'new')
self.assertEqual(second_approver.status, 'new')
self.assertEqual(record.request_status, 'new')
# Test case 5: Set min approval to an impossible value to reach
category_test.approval_minimum = 3
with self.assertRaises(UserError):
record.action_confirm()
self.assertEqual(record.request_status, 'new')
def test_compute_request_status_with_required(self):
category_test = self.env.ref('approvals.approval_category_data_business_trip')
requester_user = self.env.ref('base.user_admin')
record = self.env['approval.request'].create({
'name': 'test request',
'request_owner_id': requester_user.id,
'category_id': category_test.id,
'date_start': fields.Datetime.now(),
'date_end': fields.Datetime.now(),
'location': 'testland'
})
first_approver = self.env['approval.approver'].create({
'user_id': 1,
'request_id': record.id,
'status': 'new',
'required': True})
second_approver = self.env['approval.approver'].create({
'user_id': 2,
'request_id': record.id,
'status': 'new'})
record.approver_ids = (first_approver | second_approver)
self.assertEqual(record.request_status, 'new')
record.action_confirm()
# Min approval = 1 but first approver IS required
self.assertEqual(record.request_status, 'pending')
record.action_approve(second_approver)
# Min approval is met but required approvals are not
self.assertEqual(record.request_status, 'pending')
record.action_approve(first_approver)
self.assertEqual(record.request_status, 'approved')
# Min approval = 2
category_test.approval_minimum = 2
record.action_withdraw(first_approver)
record.action_withdraw(second_approver)
self.assertEqual(record.request_status, 'pending')
record.action_approve(first_approver)
# All required approvals are met but not the minimal approval count
self.assertEqual(record.request_status, 'pending')
record.action_approve(second_approver)
self.assertEqual(record.request_status, 'approved')
def test_product_line_compute_uom(self):
category_test = self.env.ref('approvals.approval_category_data_business_trip')
uom = self.env.ref('uom.product_uom_dozen')
product = self.env['product.product'].create({
'name': 'foo',
'uom_id': uom.id,
})
approval = self.env['approval.request'].create({
'category_id': category_test.id,
'product_line_ids': [
Command.create({'product_id': product.id})
],
})
self.assertEqual(approval.product_line_ids.description, 'foo')
self.assertEqual(approval.product_line_ids.product_uom_id, uom)
def test_unlink_approval(self):
"""
There is no error when unlinking a draft request with a document attached
or a binary field filled.
"""
approval = self.env['approval.request'].create({
'name': 'test request',
'category_id': self.env.ref('approvals.approval_category_data_business_trip').id,
'date_start': fields.Datetime.now(),
'date_end': fields.Datetime.now(),
'location': 'testland'
})
self.env['ir.attachment'].create({
'name': 'test.file',
'res_id': approval.id,
'res_model': 'approval.request',
})
self.env['ir.model.fields'].create({
'name': 'x_test_field',
'model_id': self.env.ref('approvals.model_approval_request').id,
'ttype': 'binary',
})
approval.x_test_field = 'test'
approval.unlink()
def test_unlink_multiple_approvals_with_product_line(self):
"""
There is no error when unlinking a multiple approval requests with a
product line.
"""
approvals = self.env['approval.request'].create([{
'name': 'Approval Request 1',
'category_id': self.env.ref('approvals.approval_category_data_borrow_items').id,
'date_start': fields.Datetime.now(),
'date_end': fields.Datetime.now(),
'location': 'testland',
}, {
'name': 'Approval Request 1',
'category_id': self.env.ref('approvals.approval_category_data_borrow_items').id,
'date_start': fields.Datetime.now(),
'date_end': fields.Datetime.now(),
'location': 'testitems',
}])
product_line = self.env['approval.product.line'].create({
'approval_request_id': approvals[0].id,
'description': "Description",
})
approvals.unlink()
self.assertFalse(product_line.exists())
self.assertFalse(approvals.exists())
def test_request_with_automated_sequence(self):
approval_category = self.env['approval.category'].create({
'name': 'Test Category',
'automated_sequence': True,
'sequence_code': '1234',
})
request_form = Form(self.env['approval.request'])
request_form.category_id = approval_category
approval_request = request_form.save()
self.assertEqual( approval_request.name, '123400001')

View File

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
@tagged('-at_install', 'post_install')
class TestUi(HttpCaseWithUserDemo):
def test_ui(self):
self.env.ref('approvals.approval_category_data_business_trip').write({
'approver_ids': [(5, 0, 0), (0, 0, {'user_id': self.env.ref('base.user_admin').id})],
})
self.start_tour("/odoo", 'approvals_tour', login='admin')

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Approval Category Approver -->
<record id="approval_category_approver_view_tree" model="ir.ui.view">
<field name="name">approval.category.approver.view.list</field>
<field name="model">approval.category.approver</field>
<field name="arch" type="xml">
<list editable="bottom">
<field name="existing_user_ids" column_invisible="True"/>
<field name="company_id" column_invisible="True"/>
<field name="sequence" widget="handle" />
<field name="user_id" options="{'no_create': True}"/>
<field name="job_title"/>
<field name="required"/>
</list>
</field>
</record>
</odoo>

View File

@ -1,175 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="approval_category_action_new_request" model="ir.actions.act_window">
<field name="name">Dashboard</field>
<field name="path">approvals</field>
<field name="res_model">approval.category</field>
<field name="view_mode">kanban</field>
</record>
<record id="approval_category_action" model="ir.actions.act_window">
<field name="name">Approvals Types</field>
<field name="res_model">approval.category</field>
<field name="view_mode">list,form</field>
</record>
<record id="approval_request_action_to_review_category" model="ir.actions.act_window">
<field name="name">Approvals to review</field>
<field name="res_model">approval.request</field>
<field name="view_mode">list,form,kanban</field>
<field name="domain">[('approver_ids.user_id', '=', uid), ('request_status', '=', 'pending'), ('category_id', '=', active_id)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No new approvals to review
</p>
</field>
</record>
<record id="approval_category_view_tree" model="ir.ui.view">
<field name="name">approval.category.view.list</field>
<field name="model">approval.category</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<list>
<field name="sequence" widget="handle" />
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
</list>
</field>
</record>
<record id="approval_category_view_search" model="ir.ui.view">
<field name="name">approval.category.search</field>
<field name="model">approval.category</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<separator/>
<filter name="archived" string="Archived" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<record id="approval_category_view_form" model="ir.ui.view">
<field name="name">approval.category.view.form</field>
<field name="model">approval.category</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form>
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<field name="image" widget="image" class="oe_avatar" options='{"preview_image": "image", "size": [80, 80]}'/>
<div class="oe_title">
<label for="name" string="Approval Type"/>
<h1>
<field name="name" placeholder="e.g. Procurement"/>
</h1>
</div>
<group>
<field name="description"/>
<field name="model_id"/>
<field name="model_name" invisible="1"/>
<field name="approval_type" invisible="1"/>
<field name="automated_sequence"/>
<field name="sequence_code" invisible="not automated_sequence" required="automated_sequence"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
<notebook>
<page string="Conditions">
<group>
<field name="conditions_ids" widget="one2many_list" nolabel="1"/>
</group>
</page>
<page string="Options">
<group string="Fields" name="option_settings">
<field name="active" invisible="1"/>
<field name="requirer_document" string="Document" widget="radio" options="{'horizontal': true}"/>
<field name="has_partner" string="Contact" widget="radio" options="{'horizontal': true}"/>
<field name="has_date" string="Date" widget="radio" options="{'horizontal': true}"/>
<field name="has_period" string="Period" widget="radio" options="{'horizontal': true}"/>
<field name="has_product" string="Product" force_save="1" widget="radio" options="{'horizontal': true}"/>
<field name="has_promotion" string="Promotion" force_save="1" widget="radio" options="{'horizontal': true}"/>
<field name="has_quantity" string="Quantity" widget="radio" options="{'horizontal': true}"/>
<field name="has_amount" string="Amount" widget="radio" options="{'horizontal': true}"/>
<field name="has_reference" string="Reference" widget="radio" options="{'horizontal': true}"/>
<field name="has_payment_method" string="Payment" widget="radio" options="{'horizontal': true}" invisible="1"/>
<field name="has_location" string="Location" widget="radio" options="{'horizontal': true}"/>
</group>
<!-- <group string="Approvers" name="approvers">
<field name="manager_approval"/>
<separator colspan="2"/>
<field name="approver_ids"/>
<field name="approver_sequence" invisible="approval_minimum == 0"/>
<field name="approval_minimum"/>
<field name="invalid_minimum" invisible="1"/>
<div class="text-warning" colspan="2" invisible="not invalid_minimum">
<span class="fa fa-warning" title="Invalid minimum approvals"/><field name="invalid_minimum_warning" nolabel="1"/>
</div>
</group> -->
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_open_approval_category" model="ir.actions.server">
<field name="name">Open Approval Category</field>
<field name="model_id" ref="model_approval_category"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"res_model": "approval.category",
"views": [[False, "form"]],
"target": "new",
"res_id": records.id,
}
</field>
</record>
<record id="approval_category_view_kanban" model="ir.ui.view">
<field name="name">approval.category.views.kanban</field>
<field name="model">approval.category</field>
<field name="arch" type="xml">
<kanban create="false" can_open="0" class="o_modules_kanban" js_class="approvals_category_kanban">
<field name="active"/>
<templates>
<t t-name="menu" groups="approvals.group_approval_user">
<a t-if="widget.editable"
role="menuitem"
type="action"
name="%(approvals.action_open_approval_category)d"
class="dropdown-item">Edit</a>
<a t-if="record.active.raw_value"
role="menuitem"
type="archive"
class="dropdown-item">Archive</a>
<a t-if="!record.active.raw_value"
role="menuitem"
type="unarchive"
class="dropdown-item">Unarchive</a>
</t>
<t t-name="card" class="flex-row">
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<aside>
<field name="image" widget="image" class="mt-2"/>
</aside>
<main class="ms-4">
<field name="name" class="fw-bold fs-5"/>
<field name="description" class="text-muted lh-1 mb-1 mt-1"/>
<footer t-if="!selection_mode">
<button type="object" class="btn btn-primary btn-sm" name="create_request" context="{'category_id':id}">New Request</button>
<button type="action" class="btn btn-sm btn-secondary ms-auto" name="%(approvals.approval_request_action_to_review_category)d" groups="approvals.group_approval_user">To Review: <field name="request_to_validate_count"/></button>
</footer>
</main>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="approval_condition_view_form" model="ir.ui.view">
<field name="name">approval.category.condition.form</field>
<field name="model">approval.category.condition</field>
<field name="arch" type="xml">
<form>
<sheet>
<group string="Conditions" name="conditions">
<field name="model_id"/>
<field name="model_name" invisible="1"/>
<field name="domain" widget="domain" options="{'model': 'model_name'}"/>
</group>
<group string="Approvers" name="approvers">
<field name="approver_ids" domain="domain"/>
<field name="sequence"/>
<field name="approver_sequence" invisible="approval_minimum == 0"/>
<field name="approval_minimum"/>
<field name="invalid_minimum" invisible = "1"/>
<div class="text-warning" colspan="2" invisible="not invalid_minimum">
<span class="fa fa-warning" title="Invalid minimum approvals"/>
<field name="invalid_minimum_warning" nolabel="1"/>
</div>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="approval_condition_view_list" model="ir.ui.view">
<field name="name">approval.category.condition.list</field>
<field name="model">approval.category.condition</field>
<field name="arch" type="xml">
<list>
<field name="sequence" widget="handle"/>
<field name="description" colspan="2"/>
<field name="domain" colspan="3"/>
<!-- <field name="approver_sequence" colspan="3"/>
<field name="approval_minimum" colspan="3"/> -->
<!-- <button name="delete_condition" type="object" class="btn fa fa-trash fa-xl px-3 ms-auto" title="Delete Condition" /> -->
</list>
</field>
</record>
<record id="approval_condition_action" model="ir.actions.act_window">
<field name="name">Approvals Conditions</field>
<field name="res_model">approval.category.condition</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -1,75 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="approval_product_line_view_tree_independent" model="ir.ui.view">
<field name="name">approval.product.line.view.list</field>
<field name="model">approval.product.line</field>
<!-- This priority should be lower than the regular list view,
so studio takes it instead of the regular view,
which is embedded in a form view by default -->
<field name="priority">10</field>
<field name="arch" type="xml">
<list editable="bottom" string="Products">
<field name="approval_request_id"/>
<field name="company_id" column_invisible="True"/>
<field name="product_id"/>
<field name="description"/>
<field name="quantity"/>
<field name="product_uom_id" groups="uom.group_uom"
options="{'no_create': True, 'no_open': True}"/>
<field name="product_uom_category_id" column_invisible="True"/>
</list>
</field>
</record>
<record id="approval_product_line_view_tree" model="ir.ui.view">
<field name="name">approval.product.line.view.list</field>
<field name="model">approval.product.line</field>
<field name="priority">15</field>
<field name="arch" type="xml">
<list editable="bottom" string="Products">
<field name="company_id" column_invisible="True"/>
<field name="product_id" required="parent.has_product == 'required'" context="{'search_default_filter_to_purchase': 1, 'search_view_ref': 'product.product_template_search_view'}"/>
<field name="description"/>
<field name="quantity" column_invisible="parent.has_quantity == 'no'" required="parent.has_quantity == 'required' and parent.request_status != 'new'"/>
<field name="product_uom_id" groups="uom.group_uom"
options="{'no_create': True, 'no_open': True}"/>
<field name="product_uom_category_id" column_invisible="True"/>
</list>
</field>
</record>
<record id="approval_product_line_view_form" model="ir.ui.view">
<field name="name">approval.product.line.view.form</field>
<field name="model">approval.product.line</field>
<field name="arch" type="xml">
<form string="Products">
<sheet>
<group>
<field name="company_id" invisible="1"/>
<field name="product_id"/>
<field name="description"/>
<field name="quantity"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name="product_uom_category_id" invisible="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="approval_product_kanban_mobile_view" model="ir.ui.view">
<field name="name">approval.product.kanban.mobile</field>
<field name="model">approval.product.line</field>
<field name="priority">15</field>
<field name="arch" type="xml">
<kanban editable="bottom" string="Products">
<templates>
<t t-name="card" class="d-flex justify-content-between">
<field name="product_id" class="fw-bolder fs-5"/>
<field name="quantity" class="fw-bolder fs-5"/>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="product_template_action" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product.template</field>
<field name="search_view_id" ref="product.product_template_search_view"/>
<field name="view_mode">kanban,list,form</field>
<field name="context">{'search_default_filter_to_purchase': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No product found. Let's create one!
</p><p>
Define the components and finished products you wish to use in
bill of materials and manufacturing orders.
</p>
</field>
</record>
<record id="product_product_action" model="ir.actions.act_window">
<field name="name">Product Variants</field>
<field name="res_model">product.product</field>
<field name="view_mode">list,form,kanban,activity</field>
<field name="search_view_id" ref="product.product_template_search_view"/>
<field name="context">{'search_default_filter_to_purchase': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new product variant
</p><p>
You must define a product for everything you sell or purchase,
whether it's a storable product, a consumable or a service.
</p>
</field>
</record>
</odoo>

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="approval_promotion_line_view_tree" model="ir.ui.view">
<field name="name">approval.promotion.line.list</field>
<field name="model">approval.promotion.line</field>
<field name="arch" type="xml">
<list editable="bottom" string="Promotions">
<field name="company_id" column_invisible="True"/>
<field name="promote_id"/>
<field name="current_job" />
<field name="new_designation"/>
<field name="description"/>
</list>
</field>
</record>
<record id="approval_promotion_line_view_form" model="ir.ui.view">
<field name="name">approval.promotion.line.form</field>
<field name="model">approval.promotion.line</field>
<field name="arch" type="xml">
<form string="Promotions">
<sheet>
<group>
<field name="company_id" invisible="1"/>
<field name="promote_id"/>
<field name="description"/>
<!-- <field name="current_job" options="{'no_create': True, 'no_open': True}"/>
<field name="new_designation" options="{'no_create': True, 'no_open': True}"/> -->
</group>
</sheet>
</form>
</field>
</record>
<record id="approval_promotion_kanban_mobile_view" model="ir.ui.view">
<field name="name">approval.promotion.kanban.mobile</field>
<field name="model">approval.promotion.line</field>
<field name="priority">15</field>
<field name="arch" type="xml">
<kanban editable="bottom" string="Promotion">
<templates>
<t t-name="card" class="d-flex justify-content-between">
<field name="promote_id" class="fw-bolder fs-5"/>
<field name="new_designation" class="fw-bolder fs-5"/>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="promotion_action" model="ir.actions.act_window">
<field name="name">Promotions</field>
<field name="res_model">hr.promote</field>
<field name="search_view_id" ref="action_hr_promote"/>
<field name="view_mode">kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No promotion found. Let's create one!
</p>
</field>
</record>
</odoo>

View File

@ -1,100 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_aprroval_request_document">
<t t-call="web.external_layout">
<t t-set="o" t-value="o.with_context(lang=o.partner_id.lang)"/>
<h5 class="mt-2" t-field="o.category_id.name">Test Category</h5>
<div class="row mt-4">
<div class="col-6">
<div class="row">
<h6 class="col-3 fw-bold">Request By:</h6>
<h6 t-field="o.request_owner_id">Marc Demo</h6>
</div>
<div class="row">
<h6 class="col-3 fw-bold">Approvers:</h6>
<div>
<h6 t-foreach="o.approver_ids" t-as="approver_id" class="mb-2">
<span t-field="approver_id.user_id.name">Mitchell Admin</span>
<span t-if="approver_id.status == 'approved'">(&#10004;)</span>
</h6>
</div>
</div>
<div class="row">
<h6 class="col-3 fw-bold">Status:</h6>
<h6 t-field="o.request_status">Approved</h6>
</div>
</div>
<div class="col-6">
<div class="row mt-1" t-if="o.date">
<h6 class="col-2 fw-bold">Date:</h6>
<h6 t-field="o.date">07/07/2023 10:00:00</h6>
</div>
<div class="row mt-1" t-if="o.date_start">
<h6 class="col-2 fw-bold">Period:</h6>
<h6>
<span t-field="o.date_start">07/07/2023 10:00:00</span> <span class="fw-bold">To</span> <span t-field="o.date_end">07/08/2023 10:00:00</span>
</h6>
</div>
<div class="row mt-1" t-if="o.location">
<h6 class="col-2 fw-bold">Location:</h6>
<h6 t-field="o.location">Brussels</h6>
</div>
<div class="row mt-1" t-if="o.partner_id">
<h6 class="col-2 fw-bold">Contact:</h6>
<h6 t-field="o.partner_id">Mitchell Admin</h6>
</div>
<div class="row mt-1" t-if="o.amount">
<h6 class="col-2 fw-bold">Amount:</h6>
<h6 t-field="o.amount">1200</h6>
</div>
</div>
</div>
<div class="page mt-4" t-if="o.product_line_ids">
<div class="oe_structure"/>
<table class="table table-sm mt-4">
<thead class="o_black_border">
<tr>
<th name="th_description" class="text-start fw-bold">Product Description</th>
<th name="th_expected_date" class="text-center fw-bold">Quantity</th>
<th name="th_uom" class="text-center fw-bold">UoM</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.product_line_ids" t-as="product_line">
<td><span t-field="product_line.product_id">Test Product</span></td>
<td class="text-center"><span t-field="product_line.quantity">2</span></td>
<td class="text-center"><span t-field="product_line.product_uom_id">Units</span></td>
</tr>
</tbody>
</table>
<div class="oe_structure"/>
</div>
<div t-if="o.reason" class="mt-4">
<h6>
<span class="fw-bold">Description:</span>
<span t-field="o.reason">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span>
</h6>
</div>
<div class="row mt-4" t-if="o.reference">
<div class="col-6" >
<h5>Reference:</h5>
<h6 t-field="o.reference">Test Reference</h6>
</div>
</div>
<div class="row mt-4" t-if="o.attachment_ids.ids">
<div class="col-6" >
<h5>Documents:</h5>
<h6 t-foreach="o.attachment_ids" t-as="attachment" t-out="attachment.name">Test Document</h6>
</div>
</div>
</t>
</template>
<template id="report_approval_request">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="approvals.report_aprroval_request_document" t-lang="o.partner_id.lang"/>
</t>
</t>
</template>
</odoo>

View File

@ -1,378 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--Request-->
<record id="approval_request_action" model="ir.actions.act_window">
<field name="name">My Requests</field>
<field name="res_model">approval.request</field>
<field name="view_mode">kanban,list,form</field>
<field name="domain">[('request_owner_id','=',uid)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No Approvals Requests
</p>
<p>
Let's go to the <a type="action" class="text-primary" name="%(approvals.approval_category_action_new_request)d">new request</a> menu
</p>
</field>
</record>
<record id="approval_request_action_to_review" model="ir.actions.act_window">
<field name="name">Approvals to Review</field>
<field name="res_model">approval.request</field>
<field name="view_mode">list,form,kanban</field>
<field name="domain">['&amp;',('approver_ids.user_id','=',uid),('request_status','=','pending')]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No new approvals to review
</p>
</field>
</record>
<record id="approval_request_action_all" model="ir.actions.act_window">
<field name="name">All Approvals</field>
<field name="res_model">approval.request</field>
<field name="view_mode">list,form,kanban</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No Approvals
</p>
<p>
Let's go to the <a type="action" class="text-primary" name="%(approvals.approval_category_action_new_request)d">new request</a> menu
</p>
</field>
</record>
<record id="approval_request_view_tree" model="ir.ui.view">
<field name="name">approval.request.view.list</field>
<field name="model">approval.request</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<list create="false" sample="1" decoration-info="request_status == 'new'">
<field name="name"/>
<field name="request_owner_id" widget="many2one_avatar_user"/>
<field name="category_id"/>
<field name="date_start" string="Request Start" optional="show"/>
<field name="date_end" string="Request To" optional="show"/>
<field name="date_confirmed" string="Confirm Date" optional="hide"/>
<field name="location" string="Location" optional="hide"/>
<field name="activity_ids" widget="list_activity" optional="hide"/>
<field name="user_ids" string="Approvers" widget="many2many_avatar_user" optional="hide"/>
<field name="request_status" decoration-info="request_status == 'new'" decoration-warning="request_status == 'pending'" decoration-success="request_status == 'approved'" decoration-danger="request_status == 'refused'" widget="badge"/>
<field name="company_id" readonly="True" optional="hide" groups="base.group_multi_company"/>
</list>
</field>
</record>
<record id="approval_search_view_search" model="ir.ui.view">
<field name="name">approval.request.search</field>
<field name="model">approval.request</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<filter string="My Request" name="filter_my_request" domain="[('request_owner_id', '=', uid)]"/>
<filter string="My Approvals to Review" name="filter_approvals_to_review"
domain="[('approver_ids.user_id', '=', uid), ('request_status', '=', 'pending')]"/>
<separator/>
<filter string="Approved" name="filter_approved_requests" domain="[('request_status', '=', 'approved')]"/>
<filter string="Refused" name="filter_refused_requests" domain="[('request_status', '=', 'refused')]"/>
<separator/>
<filter string="Canceled" name="filter_canceled_requests" domain="[('request_status', '=', 'cancel')]"/>
<group string="Group By">
<filter string="Request Owner" name="groupby_request_owner"
context="{'group_by': 'request_owner_id'}"/>
<filter string="Category" name="groupby_category_id"
context="{'group_by': 'category_id'}"/>
</group>
</search>
</field>
</record>
<record id="approval_request_view_form" model="ir.ui.view">
<field name="name">approval.request.view.form</field>
<field name="model">approval.request</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form string="Request" create="false">
<header>
<field name="user_status" invisible="1"/>
<field name="has_access_to_request" invisible="1"/>
<widget name="attach_document" string="Attach Document" action="message_post" highlight="1" invisible="(attachment_number &gt;= 1 and request_owner_id == uid) or request_owner_id != uid"/>
<button name="action_confirm" string="Submit" type="object" invisible="attachment_number &lt; 1 or not approver_ids or request_status != 'new'" class="btn-primary" data-hotkey="q"/>
<button name="action_confirm" string="Submit" type="object" invisible="attachment_number &gt;= 1 or not approver_ids or request_status != 'new'" data-hotkey="q"/>
<button name="action_approve" string="Approve" type="object" invisible="(attachment_number &lt; 1 and request_owner_id == uid) or request_status in ['approved', 'refused', 'cancel'] or user_status != 'pending'" class="btn-primary" data-hotkey="v"/>
<button name="action_approve" string="Approve" type="object" invisible="attachment_number &gt;= 1 or request_owner_id != uid or request_status in ['approved', 'refused', 'cancel'] or user_status != 'pending'" class="btn-primary" data-hotkey="v"/>
<button name="action_refuse" string="Refuse" type="object" invisible="request_status in ['approved', 'refused', 'cancel'] or user_status != 'pending'" data-hotkey="z"/>
<button name="action_withdraw" string="Withdraw" type="object" invisible="request_status == 'new' or user_status in ['waiting', 'pending', 'cancel'] or not user_status" data-hotkey="y"/>
<button name="action_draft" string="Back To Draft" type="object" invisible="request_status != 'cancel' or not user_status and not has_access_to_request" data-hotkey="w"/>
<button name="action_cancel" string="Cancel" type="object" invisible="request_status in ['new', 'cancel'] or not user_status and not has_access_to_request or request_owner_id != uid" data-hotkey="x"/>
<widget name="attach_document" string="Attach Document" action="message_post" invisible="(attachment_number &lt; 1 and request_owner_id == uid)"/>
<button string="Print" invisible="request_status != 'approved' or approval_type !='purchase'" type="action" name="%(action_report_approval_request)d"/>
<field name="request_status" widget="statusbar" statusbar_visible="new,pending,approved,refused" />
</header>
<sheet>
<field name="has_date" invisible="1"/>
<field name="has_period" invisible="1"/>
<field name="has_quantity" invisible="1"/>
<field name="has_amount" invisible="1"/>
<field name="has_reference" invisible="1"/>
<field name="has_partner" invisible="1"/>
<field name="has_payment_method" invisible="1"/>
<field name="has_location" invisible="1"/>
<field name="has_product" invisible="1"/>
<field name="has_promotion" invisible="1"/>
<field name="requirer_document" invisible="1"/>
<field name="approval_minimum" invisible="1"/>
<field name="approval_type" invisible="1"/>
<field name="change_request_owner" invisible="1"/>
<div class="o_not_full oe_button_box" name="button_box">
<button name="action_get_attachment_view"
class="oe_stat_button"
icon="fa-book"
type="object"
invisible="attachment_number == 0">
<field name="attachment_number" widget="statinfo" string="Documents" options="{'reload_on_button': true}"/>
</button>
</div>
<div class="oe_avatar">
<field name="category_image" widget="image" options="{'preview_image': 'category_image', 'size': [80, 80]}" class="bg-view" />
</div>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="automated_sequence" invisible="1"/>
<field name="name" placeholder="E.g: Expenses Paris business trip"
required="not automated_sequence"
readonly="automated_sequence"/>
</h1>
</div>
<group>
<group name="request_main">
<field name="request_owner_id" domain="[('share', '=', False), ('company_ids', 'in', company_id)]" readonly="not change_request_owner or request_status != 'new'" widget="many2one_avatar_user"/>
<field name="category_id" readonly="request_status != 'new'" force_save="1"/>
<field name="date_confirmed" groups="base.group_no_one" readonly="1"/>
<label for="date" string="Date" invisible="has_date == 'no'"/>
<div invisible="has_date == 'no'">
<div>
<field name="date" class="oe_inline" readonly="request_status != 'new'" required="has_date == 'required'"/>
</div>
</div>
<label for="date_start" string="Period" invisible="has_period == 'no'"/>
<div invisible="has_period == 'no'">
<div>
<label for="date_start" class="col-lg-2" string="From:"/>
<field name="date_start" class="oe_inline" readonly="request_status != 'new'" required="has_period == 'required'"/>
</div>
<div>
<label for="date_end" class="col-lg-2" string="to:"/>
<field name="date_end" class="oe_inline" readonly="request_status != 'new'" required="has_period == 'required'"/>
</div>
</div>
<field name="location" invisible="has_location == 'no'" readonly="request_status != 'new'" required="has_location == 'required'" placeholder="e.g. Brussels" />
<field name="partner_id" invisible="has_partner == 'no'" readonly="request_status != 'new'" required="has_partner == 'required'"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group name="request_details">
<field name="quantity" invisible="has_quantity == 'no' or has_product != 'no'" readonly="request_status != 'new'" required="has_quantity == 'required' and has_product == 'no'"/>
<field name="amount" invisible="has_amount == 'no'" readonly="request_status != 'new'" required="has_amount == 'required'"/>
<field name="reference" invisible="has_reference == 'no'" required="has_reference == 'required'"/>
<field name="company_id" invisible="1"/>
</group>
</group>
<notebook>
<page string="Products" name="products"
invisible="has_product == 'no'">
<field name="product_line_ids"
context="{'list_view_ref': 'approvals.approval_product_line_view_tree', 'kanban_view_ref': 'approvals.approval_product_kanban_mobile_view'}"
readonly="request_status != 'new'"/>
</page>
<page string="Promotions" name="promotions"
invisible="has_promotion == 'no'">
<field name="promotion_line_ids"
context="{'list_view_ref': 'approvals.approval_promotion_line_view_tree', 'kanban_view_ref': 'approvals.approval_promotion_kanban_mobile_view'}"
readonly="request_status != 'new'"/>
</page>
<page string="Description" name="description">
<field name="reason" readonly="request_status != 'new'"/>
</page>
<page string="Approver(s)" name="approvers">
<field name="approver_ids" mode="list,kanban" readonly="request_status != 'new'">
<list editable="bottom" decoration-success="status=='approved'" decoration-warning="status in ['pending', 'waiting']" decoration-danger="status=='refused'" no_open="1">
<field name="existing_request_user_ids" column_invisible="True"/>
<field name="sequence" column_invisible="True"/>
<field name="can_edit" column_invisible="True"/>
<field name="can_edit_user_id" column_invisible="True"/>
<field name="user_id" string="Approver" readonly="not can_edit_user_id or status != 'new'" force_save="1" />
<field name="required" readonly="not can_edit" force_save="1"/>
<field name="status"/>
<field name="company_id" column_invisible="True"/>
</list>
<kanban class="o_kanban_mobile">
<templates>
<t t-name="card" class="flex-row">
<field name="user_id"/>
<t t-if="record.status.raw_value">
<t t-set="classname" t-value="{'approved': 'text-bg-success', 'pending': 'text-bg-warning', 'refused': 'text-bg-danger'}[record.status.raw_value] || 'text-bg-light'"/>
<span t-attf-class="ms-auto rounded-pill {{ classname }}">
<field name="status"/>
</span>
</t>
</t>
</templates>
</kanban>
<form>
<group>
<field name="company_id" invisible="1"/>
<field name="existing_request_user_ids" invisible="1"/>
<field name="user_id"/>
</group>
</form>
</field>
</page>
</notebook>
</sheet>
<div class="o_attachment_preview"/>
<chatter/>
</form>
</field>
</record>
<record id="approval_request_view_kanban" model="ir.ui.view">
<field name="name">approval.request.view.kanban</field>
<field name="model">approval.request</field>
<field name="arch" type="xml">
<kanban create="false" class="o_modules_kanban" default_group_by="request_status" default_order="create_date desc"
group_create="false" group_edit="false" group_delete="false" sample="1">
<field name="request_status"/>
<field name="user_status"/>
<templates>
<t t-name="menu">
<a t-if="widget.editable" role="menuitem" type="open" class="dropdown-item">Edit Request</a>
<a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a>
<a name="action_approve" type="object" invisible="user_status != 'pending'"
role="menuitem" class="dropdown-item">Approve</a>
<a name="action_refuse" type="object" invisible="user_status != 'pending'"
role="menuitem" class="dropdown-item">Refuse</a>
<a name="action_withdraw" type="object" invisible="request_status == 'new' or user_status in ['waiting', 'pending', 'cancel']"
role="menuitem" class="dropdown-item">Withdraw</a>
</t>
<t t-name="card">
<div class="ribbon ribbon-top-right" invisible="user_status not in ['pending', 'approved', 'refused', 'cancel']">
<field name="user_status" t-if="record.user_status.raw_value == 'pending'" class="text-bg-info"/>
<field name="user_status" t-if="record.user_status.raw_value == 'approved'" class="text-bg-success"/>
<field name="user_status" t-if="record.user_status.raw_value == 'refused'" class="text-bg-danger"/>
<field name="user_status" t-if="record.user_status.raw_value == 'cancel'" class="text-bg-warning"/>
</div>
<field name="name" class="fw-bolder fs-5"/>
<field name="category_id" class="text-muted"/>
<div class="d-flex text-muted">
<field name="date_start" widget="date" class="me-1"/>-<field name="date_end" widget="date" class="ms-1"/>
</div>
<footer class="pt-0">
<field name="activity_ids" widget="kanban_activity"/>
<field name="request_owner_id" widget="many2one_avatar_user" class="ms-auto"/>
</footer>
</t>
</templates>
</kanban>
</field>
</record>
<!--MenuItem-->
<menuitem
id="approvals_menu_root"
name="Approvals"
web_icon="approvals,static/description/icon.png"
action="approval_category_action_new_request"
sequence="255"/>
<menuitem
id="approvals_category_menu_new"
parent="approvals_menu_root"
name="Dashboard"
action="approval_category_action_new_request"
sequence="1"/>
<menuitem
id="approvals_approval_menu"
parent="approvals_menu_root"
name="My Approvals"
sequence="10"/>
<menuitem
id="approvals_request_menu_my"
parent="approvals_approval_menu"
name="My Requests"
action="approval_request_action"
sequence="20"/>
<menuitem
id="approvals_menu_manager"
parent="approvals_menu_root"
name="Manager"
sequence="20"/>
<menuitem
id="approvals_approval_menu_to_review"
parent="approvals_menu_manager"
name="Approvals to Review"
action="approval_request_action_to_review"
sequence="10"/>
<menuitem
id="approvals_approval_menu_all"
parent="approvals_menu_manager"
name="All Approvals"
action="approval_request_action_all"
sequence="20"/>
<menuitem
id="approvals_menu_config"
parent="approvals_menu_root"
name="Configuration"
groups="group_approval_manager"
sequence="40"/>
<menuitem
id="approvals_category_menu_config"
parent="approvals_menu_config"
name="Approvals Types"
action="approval_category_action"
groups="group_approval_manager"
sequence="10"/>
<menuitem
id="approvals_menu_product"
parent="approvals_menu_config"
name="Products"
sequence="30"/>
<menuitem
id="approvals_menu_product_template"
parent="approvals_menu_product"
name="Products"
action="product_template_action"
sequence="10"/>
<menuitem
id="approvals_menu_product_variant"
parent="approvals_menu_product"
name="Product Variants"
action="product_product_action"
groups="product.group_product_variant"
sequence="20"/>
<!-- <menuitem
id="approvals_menu_employee"
parent="approvals_menu_config"
name="Employees"
sequence="40"/>
<menuitem
id="approvals_menu_promotion_template"
parent="approvals_menu_employee"
name="promotion"
action="promotion_action"
sequence="10"/> -->
</odoo>

Some files were not shown because too many files have changed in this diff Show More