Merge pull request !1 from 李鹏宇/pengyuthon-dev
This commit is contained in:
李鹏宇
2024-05-01 15:07:47 +00:00
committed by Gitee
85 changed files with 0 additions and 50312 deletions
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
-54
View File
@@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
# 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'],
'data': [
'security/approval_security.xml',
'security/ir.model.access.csv',
'data/approval_category_data.xml',
'data/mail_activity_type_data.xml',
'views/approval_category_views.xml',
'views/approval_category_approver_views.xml',
'views/approval_product_line_views.xml',
'views/approval_request_views.xml',
'views/res_users_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.tests_assets': [
'approvals/static/tests/helpers/**/*',
],
'web.qunit_suite_tests': [
'approvals/static/tests/**/*tests.js',
],
},
'license': 'OEEL-1',
}
-139
View File
@@ -1,139 +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>
</data>
</odoo>
-82
View File
@@ -1,82 +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_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>
@@ -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>
File diff suppressed because it is too large Load Diff
-1220
View File
File diff suppressed because it is too large Load Diff
-1173
View File
File diff suppressed because it is too large Load Diff
-1201
View File
File diff suppressed because it is too large Load Diff
-1154
View File
File diff suppressed because it is too large Load Diff
-1231
View File
File diff suppressed because it is too large Load Diff
-1154
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1230
View File
File diff suppressed because it is too large Load Diff
-1218
View File
File diff suppressed because it is too large Load Diff
-1234
View File
File diff suppressed because it is too large Load Diff
-1230
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1207
View File
File diff suppressed because it is too large Load Diff
-1163
View File
File diff suppressed because it is too large Load Diff
-1202
View File
File diff suppressed because it is too large Load Diff
-1226
View File
File diff suppressed because it is too large Load Diff
-1228
View File
File diff suppressed because it is too large Load Diff
-1199
View File
File diff suppressed because it is too large Load Diff
-1202
View File
File diff suppressed because it is too large Load Diff
-1147
View File
File diff suppressed because it is too large Load Diff
-1207
View File
File diff suppressed because it is too large Load Diff
-1199
View File
File diff suppressed because it is too large Load Diff
-1162
View File
File diff suppressed because it is too large Load Diff
-1156
View File
File diff suppressed because it is too large Load Diff
-1229
View File
File diff suppressed because it is too large Load Diff
-1220
View File
File diff suppressed because it is too large Load Diff
-1158
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1162
View File
File diff suppressed because it is too large Load Diff
-1161
View File
File diff suppressed because it is too large Load Diff
-1199
View File
File diff suppressed because it is too large Load Diff
-1163
View File
File diff suppressed because it is too large Load Diff
-1213
View File
File diff suppressed because it is too large Load Diff
-1165
View File
File diff suppressed because it is too large Load Diff
-1228
View File
File diff suppressed because it is too large Load Diff
-1221
View File
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
-9
View File
@@ -1,9 +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
-162
View File
@@ -1,162 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
CATEGORY_SELECTION = [
('required', 'Required'),
('optional', 'Optional'),
('no', 'None')]
class ApprovalCategory(models.Model):
_name = 'approval.category'
_description = 'Approval Category'
_order = 'sequence, id'
_check_company_auto = True
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())
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.")
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.
""")
user_ids = fields.Many2many('res.users', compute='_compute_user_ids', string="Approver Users")
approver_ids = fields.One2many('approval.category.approver', 'category_id', string="Approvers")
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)
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') + ' ' + vals['sequence_code'],
'padding': 5,
'prefix': vals['sequence_code'],
'company_id': vals.get('company_id'),
})
vals['sequence_id'] = sequence.id
return super().create(vals_list)
def write(self, vals):
if 'sequence_code' in vals:
for approval_category in self:
sequence_vals = {
'name': _('Sequence') + ' ' + 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')
return super().write(vals)
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'
},
}
@@ -1,27 +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', ondelete='cascade', required=True)
company_id = fields.Many2one('res.company', related='category_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)]")
required = fields.Boolean(default=False)
existing_user_ids = fields.Many2many('res.users', compute='_compute_existing_user_ids')
@api.depends('category_id')
def _compute_existing_user_ids(self):
for record in self:
record.existing_user_ids = record.category_id.approver_ids.user_id
-37
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.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
-378
View File
@@ -1,378 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError, ValidationError
from collections import defaultdict
class ApprovalRequest(models.Model):
_name = 'approval.request'
_description = 'Approval Request'
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin']
_order = 'name'
_mail_post_access = 'read'
_check_company_auto = True
@api.model
def _read_group_request_status(self, stages, domain, order):
request_status_list = dict(self._fields['request_status'].selection).keys()
return request_status_list
name = fields.Char(string="Approval Subject", tracking=True)
category_id = fields.Many2one('approval.category', string="Category", required=True)
category_image = fields.Binary(related='category_id.image')
approver_ids = fields.One2many('approval.approver', 'request_id', string="Approvers", check_company=True,
compute='_compute_approver_ids', store=True, readonly=False)
company_id = fields.Many2one(
string='Company', related='category_id.company_id',
store=True, readonly=True, index=True)
date = fields.Datetime(string="Date")
date_start = fields.Datetime(string="Date start")
date_end = fields.Datetime(string="Date end")
quantity = fields.Float(string="Quantity")
location = fields.Char(string="Location")
date_confirmed = fields.Datetime(string="Date Confirmed")
partner_id = fields.Many2one('res.partner', string="Contact", check_company=True)
reference = fields.Char(string="Reference")
amount = fields.Float(string="Amount")
reason = fields.Html(string="Description")
request_status = fields.Selection([
('new', 'To Submit'),
('pending', 'Submitted'),
('approved', 'Approved'),
('refused', 'Refused'),
('cancel', 'Cancel'),
], default="new", compute="_compute_request_status",
store=True, index=True, tracking=True,
group_expand='_read_group_request_status')
request_owner_id = fields.Many2one('res.users', string="Request Owner",
check_company=True, domain="[('company_ids', 'in', company_id)]")
user_status = fields.Selection([
('new', 'New'),
('pending', 'To Approve'),
('waiting', 'Waiting'),
('approved', 'Approved'),
('refused', 'Refused'),
('cancel', 'Cancel')], compute="_compute_user_status")
has_access_to_request = fields.Boolean(string="Has Access To Request", compute="_compute_has_access_to_request")
change_request_owner = fields.Boolean(string='Can Change Request Owner', compute='_compute_has_access_to_request')
attachment_number = fields.Integer('Number of Attachments', compute='_compute_attachment_number')
product_line_ids = fields.One2many('approval.product.line', 'approval_request_id', check_company=True)
has_date = fields.Selection(related="category_id.has_date")
has_period = fields.Selection(related="category_id.has_period")
has_quantity = fields.Selection(related="category_id.has_quantity")
has_amount = fields.Selection(related="category_id.has_amount")
has_reference = fields.Selection(related="category_id.has_reference")
has_partner = fields.Selection(related="category_id.has_partner")
has_payment_method = fields.Selection(related="category_id.has_payment_method")
has_location = fields.Selection(related="category_id.has_location")
has_product = fields.Selection(related="category_id.has_product")
requirer_document = fields.Selection(related="category_id.requirer_document")
approval_minimum = fields.Integer(related="category_id.approval_minimum")
approval_type = fields.Selection(related="category_id.approval_type")
approver_sequence = fields.Boolean(related="category_id.approver_sequence")
automated_sequence = fields.Boolean(related="category_id.automated_sequence")
@api.depends('request_owner_id')
@api.depends_context('uid')
def _compute_has_access_to_request(self):
is_approval_user = self.env.user.has_group('approvals.group_approval_user')
self.change_request_owner = is_approval_user
for request in self:
request.has_access_to_request = request.request_owner_id == self.env.user and is_approval_user
def _compute_attachment_number(self):
domain = [('res_model', '=', 'approval.request'), ('res_id', 'in', self.ids)]
attachment_data = self.env['ir.attachment']._read_group(domain, ['res_id'], ['__count'])
attachment = dict(attachment_data)
for request in self:
request.attachment_number = attachment.get(request.id, 0)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
category = 'category_id' in vals and self.env['approval.category'].browse(vals['category_id'])
if category and category.automated_sequence:
vals['name'] = category.sequence_id.next_by_id()
return super().create(vals_list)
@api.ondelete(at_uninstall=False)
def unlink_attachments(self):
attachment_ids = self.env['ir.attachment'].search([
('res_model', '=', 'approval.request'),
('res_id', 'in', self.ids),
])
if attachment_ids:
attachment_ids.unlink()
def action_get_attachment_view(self):
self.ensure_one()
res = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
res['domain'] = [('res_model', '=', 'approval.request'), ('res_id', 'in', self.ids)]
res['context'] = {'default_res_model': 'approval.request', 'default_res_id': self.id}
return res
def action_confirm(self):
# make sure that the manager is present in the list if he is required
self.ensure_one()
if self.category_id.manager_approval == 'required':
employee = self.env['hr.employee'].search([('user_id', '=', self.request_owner_id.id)], limit=1)
if not employee.parent_id:
raise UserError(_('This request needs to be approved by your manager. There is no manager linked to your employee profile.'))
if not employee.parent_id.user_id:
raise UserError(_('This request needs to be approved by your manager. There is no user linked to your manager.'))
if not self.approver_ids.filtered(lambda a: a.user_id.id == employee.parent_id.user_id.id):
raise UserError(_('This request needs to be approved by your manager. Your manager is not in the approvers list.'))
if len(self.approver_ids) < self.approval_minimum:
raise UserError(_("You have to add at least %s approvers to confirm your request.", self.approval_minimum))
if self.requirer_document == 'required' and not self.attachment_number:
raise UserError(_("You have to attach at lease one document."))
approvers = self.approver_ids
if self.approver_sequence:
approvers = approvers.filtered(lambda a: a.status in ['new', 'pending', 'waiting'])
approvers[1:].sudo().write({'status': 'waiting'})
approvers = approvers[0] if approvers and approvers[0].status != 'pending' else self.env['approval.approver']
else:
approvers = approvers.filtered(lambda a: a.status == 'new')
approvers._create_activity()
approvers.sudo().write({'status': 'pending'})
self.sudo().write({'date_confirmed': fields.Datetime.now()})
def _get_user_approval_activities(self, user):
domain = [
('res_model', '=', 'approval.request'),
('res_id', 'in', self.ids),
('activity_type_id', '=', self.env.ref('approvals.mail_activity_data_approval').id),
('user_id', '=', user.id)
]
activities = self.env['mail.activity'].search(domain)
return activities
def _ensure_can_approve(self):
if any(approval.approver_sequence and approval.user_status == 'waiting' for approval in self):
raise ValidationError(_('You cannot approve before the previous approver.'))
def _update_next_approvers(self, new_status, approver, only_next_approver, cancel_activities=False):
approvers_updated = self.env['approval.approver']
for approval in self.filtered('approver_sequence'):
current_approver = approval.approver_ids & approver
approvers_to_update = approval.approver_ids.filtered(lambda a: a.status not in ['approved', 'refused'] and (a.sequence > current_approver.sequence or (a.sequence == current_approver.sequence and a.id > current_approver.id)))
if only_next_approver and approvers_to_update:
approvers_to_update = approvers_to_update[0]
approvers_updated |= approvers_to_update
approvers_updated.sudo().status = new_status
if new_status == 'pending':
approvers_updated._create_activity()
if cancel_activities:
approvers_updated.request_id._cancel_activities()
def _cancel_activities(self):
approval_activity = self.env.ref('approvals.mail_activity_data_approval')
activities = self.activity_ids.filtered(lambda a: a.activity_type_id == approval_activity)
activities.unlink()
def action_approve(self, approver=None):
self._ensure_can_approve()
if not isinstance(approver, models.BaseModel):
approver = self.mapped('approver_ids').filtered(
lambda approver: approver.user_id == self.env.user
)
approver.write({'status': 'approved'})
self.sudo()._update_next_approvers('pending', approver, only_next_approver=True)
self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback()
def action_refuse(self, approver=None):
if not isinstance(approver, models.BaseModel):
approver = self.mapped('approver_ids').filtered(
lambda approver: approver.user_id == self.env.user
)
approver.write({'status': 'refused'})
self.sudo()._update_next_approvers('refused', approver, only_next_approver=False, cancel_activities=True)
self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback()
def action_withdraw(self, approver=None):
if not isinstance(approver, models.BaseModel):
approver = self.mapped('approver_ids').filtered(
lambda approver: approver.user_id == self.env.user
)
self.sudo()._update_next_approvers('waiting', approver, only_next_approver=False, cancel_activities=True)
approver.write({'status': 'pending'})
def action_draft(self):
self.mapped('approver_ids').write({'status': 'new'})
def action_cancel(self):
self.sudo()._get_user_approval_activities(user=self.env.user).unlink()
self.mapped('approver_ids').write({'status': 'cancel'})
@api.depends_context('uid')
@api.depends('approver_ids.status')
def _compute_user_status(self):
for approval in self:
approval.user_status = approval.approver_ids.filtered(lambda approver: approver.user_id == self.env.user).status
@api.depends('approver_ids.status', 'approver_ids.required')
def _compute_request_status(self):
for request in self:
status_lst = request.mapped('approver_ids.status')
required_approved = all(a.status == 'approved' for a in request.approver_ids.filtered('required'))
minimal_approver = request.approval_minimum if len(status_lst) >= request.approval_minimum else len(status_lst)
if status_lst:
if status_lst.count('cancel'):
status = 'cancel'
elif status_lst.count('refused'):
status = 'refused'
elif status_lst.count('new'):
status = 'new'
elif status_lst.count('approved') >= minimal_approver and required_approved:
status = 'approved'
else:
status = 'pending'
else:
status = 'new'
request.request_status = status
self.filtered_domain([('request_status', 'in', ['approved', 'refused', 'cancel'])])._cancel_activities()
@api.model
def _update_approver_vals(self, approver_id_vals, approver, new_required, new_sequence):
if approver.required != new_required or approver.sequence != new_sequence:
approver_id_vals.append(Command.update(approver.id, {'required': new_required, 'sequence': new_sequence}))
@api.model
def _create_or_update_approver(self, user_id, users_to_approver, approver_id_vals, required, sequence):
if user_id not in users_to_approver.keys():
approver_id_vals.append(Command.create({
'user_id': user_id,
'status': 'new',
'required': required,
'sequence': sequence,
}))
else:
current_approver = users_to_approver.pop(user_id)
self._update_approver_vals(approver_id_vals, current_approver, required, sequence)
@api.depends('category_id', 'request_owner_id')
def _compute_approver_ids(self):
for request in self:
users_to_approver = {}
for approver in request.approver_ids:
users_to_approver[approver.user_id.id] = approver
users_to_category_approver = {}
for approver in request.category_id.approver_ids:
users_to_category_approver[approver.user_id.id] = approver
approver_id_vals = []
if request.category_id.manager_approval:
employee = self.env['hr.employee'].search([('user_id', '=', request.request_owner_id.id)], limit=1)
if employee.parent_id.user_id:
manager_user_id = employee.parent_id.user_id.id
manager_required = request.category_id.manager_approval == 'required'
# We set the manager sequence to be lower than all others (9) so they are the first to approve.
self._create_or_update_approver(manager_user_id, users_to_approver, approver_id_vals, manager_required, 9)
if manager_user_id in users_to_category_approver.keys():
users_to_category_approver.pop(manager_user_id)
for user_id in users_to_category_approver:
self._create_or_update_approver(user_id, users_to_approver, approver_id_vals,
users_to_category_approver[user_id].required,
users_to_category_approver[user_id].sequence)
for current_approver in users_to_approver.values():
# Reset sequence and required for the remaining approvers that are no (longer) part of the category approvers or managers.
# Set the sequence of these manually added approvers to 1000, so that they always appear after the category approvers.
self._update_approver_vals(approver_id_vals, current_approver, False, 1000)
request.update({'approver_ids': approver_id_vals})
def write(self, vals):
res = super().write(vals)
if 'approver_ids' in vals:
to_resequence = self.filtered_domain([('approver_sequence', '=', True), ('request_status', '=', 'pending')])
for approval in to_resequence:
if not approval.approver_ids.filtered(lambda a: a.status == 'pending'):
approver = approval.approver_ids.filtered(lambda a: a.status == 'waiting')
if approver:
approver[0].status = 'pending'
approver[0]._create_activity()
return res
@api.constrains('approver_ids')
def _check_approver_ids(self):
for request in self:
# make sure the approver_ids are unique per request
if len(request.approver_ids) != len(request.approver_ids.user_id):
raise UserError(_("You cannot assign the same approver multiple times on the same request."))
class ApprovalApprover(models.Model):
_name = 'approval.approver'
_description = 'Approver'
_order = 'sequence, id'
_check_company_auto = True
sequence = fields.Integer('Sequence', default=10)
user_id = fields.Many2one('res.users', string="User", required=True, check_company=True, domain="[('id', 'not in', existing_request_user_ids)]")
existing_request_user_ids = fields.Many2many('res.users', compute='_compute_existing_request_user_ids')
status = fields.Selection([
('new', 'New'),
('pending', 'To Approve'),
('waiting', 'Waiting'),
('approved', 'Approved'),
('refused', 'Refused'),
('cancel', 'Cancel')], string="Status", default="new", readonly=True)
request_id = fields.Many2one('approval.request', string="Request",
ondelete='cascade', check_company=True)
company_id = fields.Many2one(
string='Company', related='request_id.company_id',
store=True, readonly=True, index=True)
required = fields.Boolean(default=False, readonly=True)
category_approver = fields.Boolean(compute='_compute_category_approver')
can_edit = fields.Boolean(compute='_compute_can_edit')
can_edit_user_id = fields.Boolean(compute='_compute_can_edit', help="Simple users should not be able to remove themselves as approvers because they will lose access to the record if they misclick.")
def action_approve(self):
self.request_id.action_approve(self)
def action_refuse(self):
self.request_id.action_refuse(self)
def _create_activity(self):
for approver in self:
approver.request_id.activity_schedule(
'approvals.mail_activity_data_approval',
user_id=approver.user_id.id)
@api.depends('request_id.request_owner_id', 'request_id.approver_ids.user_id')
def _compute_existing_request_user_ids(self):
for approver in self:
approver.existing_request_user_ids = \
self.mapped('request_id.approver_ids.user_id')._origin \
| self.request_id.request_owner_id._origin
@api.depends('category_approver', 'user_id')
def _compute_category_approver(self):
for approval in self:
approval.category_approver = approval.user_id in approval.request_id.category_id.approver_ids.user_id
@api.depends_context('uid')
@api.depends('user_id', 'category_approver')
def _compute_can_edit(self):
is_user = self.env.user.has_group('approvals.group_approval_user')
for approval in self:
approval.can_edit = not approval.user_id or not approval.category_approver or is_user
approval.can_edit_user_id = is_user or approval.request_id.request_owner_id == self.env.user
-22
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']
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."))
-20
View File
@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class MailActivity(models.Model):
_inherit = 'mail.activity'
def activity_format(self):
result = super(MailActivity, self).activity_format()
activity_type_approval_id = self.env.ref('approvals.mail_activity_data_approval').id
for activity in result:
if activity['activity_type_id'] and activity['activity_type_id'][0] == activity_type_approval_id and \
activity['res_model'] == 'approval.request':
request = self.env['approval.request'].browse(activity['res_id'])
approver = request.approver_ids.filtered(lambda approver: activity['user_id'][0] == approver.user_id.id)
activity['approver_id'] = approver.id
activity['approver_status'] = approver.status
return result
-197
View File
@@ -1,197 +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_owner_rule" model="ir.rule">
<field name="name">Approval Request: 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="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)]</field>
</record>
<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_write_request_owner_rule" model="ir.rule">
<field name="name">Approval Request: Request Owner create/edit To Submit</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="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[('request_owner_id.id','=', user.id), ('request_status', '=', 'new')]</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_request_approvers_rule" model="ir.rule">
<field name="name">Approval Request: approvers 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="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="domain_force">[('approver_ids.user_id','=', user.id)]</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.id', '=', user.id), ('user_id.id', '=', user.id)]</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.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.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>
<!-- 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_request_owner_rule')]"/>
<value eval="{'perm_write': 0, 'perm_create': 0, 'perm_unlink': 0}"/>
</function>
<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>
-15
View File
@@ -1,15 +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_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,0
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
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_approver_user access_approval_category_approver model_approval_category_approver group_approval_user 1 0 0 0
10 access_approval_category_approver_manager access_approval_category_approver model_approval_category_approver group_approval_manager 1 1 1 1
11 access_approval_approver access_approval_approver model_approval_approver base.group_user 1 1 1 0
12 access_approval_approver_user access_approval_approver model_approval_approver group_approval_user 1 1 1 1
13 access_approval_approver_manager access_approval_approver model_approval_approver group_approval_manager 1 1 1 1
14 access_approval_product_line_user access_approval_product_line model_approval_product_line base.group_user 1 1 1 1
15 access_approval_product_line_manager access_approval_product_line model_approval_product_line group_approval_manager 1 1 1 1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

-1
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

@@ -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;
},
});
@@ -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>
@@ -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;
},
});
@@ -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 });
@@ -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.data.approval" activity="props.data" onChange="props.reloadParentView"/>
<t t-else="">$0</t>
</xpath>
</t>
</templates>
@@ -1,40 +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() {
/** @type {import("@mail/core/web/activity_service").ActivityService} */
this.activityService = useService("mail.activity");
this.store = useState(useService("mail.store"));
}
async onClickApprove() {
await this.env.services.orm.call("approval.approver", "action_approve", [
this.props.activity.approval.id,
]);
this.activityService.delete(this.props.activity);
this.props.onChange();
}
async onClickRefuse() {
await this.env.services.orm.call("approval.approver", "action_refuse", [
this.props.activity.approval.id,
]);
this.activityService.delete(this.props.activity);
this.props.onChange();
}
}
@@ -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.user.user.id">
<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>
@@ -1,150 +0,0 @@
/* @odoo-module */
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
import { start } from "@mail/../tests/helpers/test_utils";
import { makeDeferred } from "@web/../tests/helpers/utils";
import { click, contains } from "@web/../tests/utils";
QUnit.module("activity (patch)");
QUnit.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: pyEnv.currentUserId,
});
pyEnv["mail.activity"].create({
can_write: true,
res_id: requestId,
res_model: "approval.request",
user_id: pyEnv.currentUserId,
});
const { openView } = await start();
await openView({
res_model: "approval.request",
res_id: requestId,
views: [[false, "form"]],
});
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" });
});
QUnit.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({});
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,
});
const { openView } = await start();
await openView({
res_model: "approval.request",
res_id: requestId,
views: [[false, "form"]],
});
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" });
});
QUnit.test("approve approval", async (assert) => {
const pyEnv = await startServer();
const requestId = pyEnv["approval.request"].create({});
pyEnv["approval.approver"].create({
request_id: requestId,
status: "pending",
user_id: pyEnv.currentUserId,
});
pyEnv["mail.activity"].create({
can_write: true,
res_id: requestId,
res_model: "approval.request",
user_id: pyEnv.currentUserId,
});
const def = makeDeferred();
const { openView } = await start({
async mockRPC(route, args) {
if (args.method === "action_approve") {
assert.strictEqual(args.args.length, 1);
assert.strictEqual(args.args[0], requestId);
assert.step("action_approve");
def.resolve();
}
},
});
await openView({
res_model: "approval.request",
res_id: requestId,
views: [[false, "form"]],
});
await click(".o-mail-Activity button", { text: "Approve" });
await def;
assert.verifySteps(["action_approve"]);
});
QUnit.test("refuse approval", async (assert) => {
const pyEnv = await startServer();
const requestId = pyEnv["approval.request"].create({});
pyEnv["approval.approver"].create({
request_id: requestId,
status: "pending",
user_id: pyEnv.currentUserId,
});
pyEnv["mail.activity"].create({
can_write: true,
res_id: requestId,
res_model: "approval.request",
user_id: pyEnv.currentUserId,
});
const def = makeDeferred();
const { openView } = await start({
async mockRPC(route, args) {
if (args.method === "action_refuse") {
assert.strictEqual(args.args.length, 1);
assert.strictEqual(args.args[0], requestId);
assert.step("action_refuse");
def.resolve();
}
},
});
await openView({
res_model: "approval.request",
res_id: requestId,
views: [[false, "form"]],
});
await click(".o-mail-Activity button", { text: "Refuse" });
await def;
assert.verifySteps(["action_refuse"]);
});
@@ -1,48 +0,0 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { MockServer } from '@web/../tests/helpers/mock_server';
patch(MockServer.prototype, {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
async _performRPC(route, args) {
if (args.model === 'approval.approver' && args.method === 'action_approve') {
const ids = args.args[0];
return this._mockApprovalApproverActionApprove(ids);
}
if (args.model === 'approval.approver' && args.method === 'action_refuse') {
const ids = args.args[0];
return this._mockApprovalApproverActionApprove(ids);
}
return super._performRPC(...arguments);
},
//--------------------------------------------------------------------------
// Private Mocked Methods
//--------------------------------------------------------------------------
/**
* Simulates `action_approve` on `approval.approver`.
*
* @private
* @param {integer[]} ids
*/
_mockApprovalApproverActionApprove(ids) {
// TODO implement this mock and improve related tests (task-2300537)
},
/**
* Simulates `action_refuse` on `approval.approver`.
*
* @private
* @param {integer[]} ids
*/
_mockApprovalApproverActionRefuse(ids) {
// TODO implement this mock and improve related tests (task-2300537)
},
});
@@ -1,31 +0,0 @@
/** @odoo-module **/
// ensure mail override is applied first.
import '@mail/../tests/helpers/mock_server/models/mail_activity';
import { patch } from "@web/core/utils/patch";
import { MockServer } from '@web/../tests/helpers/mock_server';
patch(MockServer.prototype, {
/**
* @override
*/
_mockMailActivityActivityFormat(ids) {
const activities = super._mockMailActivityActivityFormat(ids);
for (const activity of activities) {
if (activity.res_model === 'approval.request') {
// check on activity type being approval not done here for simplicity
const approver = this.getRecords('approval.approver', [
['request_id', '=', activity.res_id],
['user_id', '=', activity.user_id[0]],
])[0];
if (approver) {
activity.approver_id = approver.id;
activity.approver_status = approver.status;
}
}
}
return activities;
},
});
@@ -1,5 +0,0 @@
/** @odoo-module **/
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch(['approval.request', 'approval.approver']);
@@ -1,14 +0,0 @@
/** @odoo-module **/
import { registry } from '@web/core/registry';
const viewArchsRegistry = registry.category('bus.view.archs');
viewArchsRegistry.category('form').add(
'approval.request',
`<form>
<div class="oe_chatter">
<field name="activity_ids"/>
</div>
</form>`
);
-121
View File
@@ -1,121 +0,0 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("approvals_tour", {
test: true,
url: "/web",
steps: () => [
{
trigger: '.o_app[data-menu-xmlid="approvals.approvals_menu_root"]',
content: "open approvals app",
run: "click",
},
{
trigger: ".oe_kanban_action_button:first",
content: "create new request",
run: "click",
},
{
trigger: '.o_field_widget[name="name"] input',
content: "give name",
run: "text Business Trip To Berlin",
},
{
trigger: '.o_field_widget[name="date_start"] input',
content: "give start date",
run: "text 12/13/2018 13:00:00",
},
{
trigger: '.o_field_widget[name="date_end"] input',
content: "give end date",
run: "text 12/20/2018 13:00:00",
},
{
trigger: '.o_field_widget[name="location"] input',
content: "give location",
run: "text Berlin, Schulz Hotel",
},
{
trigger: 'div[name="reason"]',
content: "give description",
run: "text 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",
run: function (actions) {
actions.text("Marc", this.$anchor.find("input"));
},
},
{
trigger: ".ui-autocomplete > li > a:contains(Marc)",
auto: true,
},
{
trigger: ".o_form_button_save",
content: "save the request",
run: "click",
},
{
trigger: 'button[name="action_confirm"]',
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-label="Current state"][data-value="refused"]',
content: "wait the request status compute",
isCheck: true,
},
{
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"]',
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",
isCheck: true,
},
],
});
-5
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
-151
View File
@@ -1,151 +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
from odoo.exceptions import UserError
class TestRequest(common.TransactionCase):
def test_compute_request_status(self):
category_test = self.env['approval.category'].browse(1)
record = self.env['approval.request'].create({
'name': 'test request',
'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['approval.category'].browse(1)
record = self.env['approval.request'].create({
'name': 'test request',
'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['approval.category'].browse(1)
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
"""
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',
})
approval.unlink()
-15
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("/web", 'approvals_tour', login='admin')
@@ -1,17 +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.tree</field>
<field name="model">approval.category.approver</field>
<field name="arch" type="xml">
<tree 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="required"/>
</tree>
</field>
</record>
</odoo>
-145
View File
@@ -1,145 +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="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">tree,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">tree,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.tree</field>
<field name="model">approval.category</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree>
<field name="sequence" widget="handle" />
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/>
</tree>
</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="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="Options" name="options">
<group>
<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_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}"/>
<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>
</group>
</page>
</notebook>
</sheet>
</form>
</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" class="o_modules_kanban">
<field name="name"/>
<field name="id"/>
<field name="image"/>
<templates>
<t t-name="kanban-box">
<div class="oe_module_vignette">
<t t-if="record.id">
<img t-att-src="kanban_image('approval.category', 'image', record.id.raw_value)" alt="Approvals Types Image"
class="oe_kanban_avatar float-start me-3" width="64" height="64"/>
</t>
<div class="oe_module_desc">
<h4 class="o_kanban_record_title">
<field name="name"/>
</h4>
<p class="oe_module_name">
<field name="description"/>
</p>
<div class="oe_module_action" 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 float-end" name="%(approvals.approval_request_action_to_review_category)d" groups="approvals.group_approval_user">To Review: <field name="request_to_validate_count"/></button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>
@@ -1,81 +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.tree</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">
<tree 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"/>
</tree>
</field>
</record>
<record id="approval_product_line_view_tree" model="ir.ui.view">
<field name="name">approval.product.line.view.tree</field>
<field name="model">approval.product.line</field>
<field name="priority">15</field>
<field name="arch" type="xml">
<tree editable="bottom" string="Products">
<field name="company_id" column_invisible="True"/>
<field name="product_id" required="parent.has_product == 'required'"/>
<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"/>
</tree>
</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">
<field name="product_id"/>
<field name="quantity"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="d-flex justify-content-between">
<strong class="o_kanban_record_title"><t t-esc="record.product_id.value"/></strong>
<span class="o_kanban_record_title"><t t-esc="record.quantity.value"/></span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>
-364
View File
@@ -1,364 +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,tree,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">tree,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">tree,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.tree</field>
<field name="model">approval.request</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree 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="activity_ids" widget="list_activity"/>
<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"/>
</tree>
</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')]"/>
</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"/>
<button name="action_confirm" string="Submit" type="object" invisible="not approver_ids or request_status != 'new'" class="btn-primary" data-hotkey="q"/>
<button name="action_approve" string="Approve" type="object" invisible="request_status in ['approved', 'refused', 'cancel'] or user_status != 'pending'" 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" data-hotkey="x"/>
<widget name="attach_document" string="Attach Document" action="message_post" invisible="attachment_number &lt; 1"/>
<widget name="attach_document" string="Attach Document" action="message_post" highlight="1" invisible="attachment_number &gt;= 1"/>
<field name="request_status" widget="statusbar"/>
</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="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="1"
readonly="automated_sequence"/>
</h1>
</div>
<group>
<group name="request_main">
<field name="request_owner_id" domain="[('share', '=', False)]" 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>
<span>From: </span><field name="date_start" class="oe_inline" readonly="request_status != 'new'" required="has_period == 'required'"/>
</div>
<div>
<span>to: </span><field name="date_end" class="oe_inline ms-4" 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'"/>
</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="{'tree_view_ref': 'approvals.approval_product_line_view_tree', 'kanban_view_ref': 'approvals.approval_product_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="tree,kanban" readonly="request_status != 'new'">
<tree 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"/>
</tree>
<kanban class="o_kanban_mobile">
<field name="company_id"/>
<field name="user_id"/>
<field name="status"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click">
<div class="o_kanban_content">
<t t-esc="record.user_id.value"/>
<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-esc="record.status.raw_value" t-attf-class="float-end rounded-pill {{ classname }}"/>
</t>
</div>
</div>
</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"/>
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</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_owner_id"/>
<field name="request_status"/>
<field name="user_status"/>
<field name="name"/>
<field name="activity_ids"/>
<field name="category_id"/>
<templates>
<t t-name="kanban-menu">
<a t-if="widget.editable" role="menuitem" type="edit" 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="kanban-box">
<div class="oe_kanban_global_click container">
<div class="oe_kanban_content">
<t t-if="record.user_status.raw_value == 'pending'">
<span class="badge rounded-pill float-end text-bg-warning mt4 mr16"><t t-esc="record.user_status.value"/></span>
</t>
<t t-if="record.user_status.raw_value == 'approved'">
<span class="badge rounded-pill float-end text-bg-success mt4 mr16"><t t-esc="record.user_status.value"/></span>
</t>
<t t-if="record.user_status.raw_value == 'refused'">
<span class="badge rounded-pill float-end text-bg-danger mt4 mr16"><t t-esc="record.user_status.value"/></span>
</t>
<div>
<strong class="o_kanban_record_title"><field name="name"/></strong>
</div>
<div class="text-muted o_kanban_record_subtitle">
<field name="category_id"/>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field name="activity_ids" widget="kanban_activity"/>
</div>
<div class="oe_kanban_bottom_right">
<field name="request_owner_id" widget="many2one_avatar_user"/>
</div>
</div>
</div>
</div>
</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_approval_menu"
parent="approvals_menu_root"
name="My Approvals"
sequence="10"/>
<menuitem
id="approvals_category_menu_new"
parent="approvals_approval_menu"
name="New Request"
action="approval_category_action_new_request"
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.product_template_action"
sequence="10"/>
<menuitem
id="approvals_menu_product_variant"
parent="approvals_menu_product"
name="Product Variants"
action="product.product_normal_action"
groups="product.group_product_variant"
sequence="20"/>
</odoo>
-13
View File
@@ -1,13 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="res_users_view_form" model="ir.ui.view">
<field name="name">hr.user.preferences.view.form.approval.inherit</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="hr.res_users_view_form_profile"/>
<field name="arch" type="xml">
<header position="inside">
<button name="%(approvals.approval_category_action_new_request)d" string="Request Approval" type="action" class="btn btn-primary"/>
</header>
</field>
</record>
</odoo>