clean repo from feature/approval
remove feature/approval module
@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
@ -1,55 +0,0 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Approvals',
|
||||
'version': '1.0',
|
||||
'category': 'Human Resources/Approvals',
|
||||
'sequence': 190,
|
||||
'summary': 'Create and validate approvals requests',
|
||||
'description': """
|
||||
This module manages approvals workflow
|
||||
======================================
|
||||
|
||||
This module manages approval requests like business trips,
|
||||
out of office, overtime, borrow items, general approvals,
|
||||
procurements, contract approval, etc.
|
||||
|
||||
According to the approval type configuration, a request
|
||||
creates next activities for the related approvers.
|
||||
""",
|
||||
'depends': ['mail', 'hr', 'product','hr_promote'],
|
||||
'data': [
|
||||
'security/approval_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/approval_category_data.xml',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'views/approval_category_views.xml',
|
||||
'views/approval_category_approver_views.xml',
|
||||
'views/approval_product_line_views.xml',
|
||||
'views/approval_products_views.xml',
|
||||
'views/approval_promotion_line_views.xml',
|
||||
'views/approval_request_template.xml',
|
||||
'report/approval_request_report.xml',
|
||||
'views/approval_request_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/approval_condition_views.xml',
|
||||
],
|
||||
'demo':[
|
||||
'data/approval_demo.xml',
|
||||
],
|
||||
'application': True,
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'approvals/static/src/**',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'approvals/static/tests/tours/**/*',
|
||||
],
|
||||
'web.assets_unit_tests': [
|
||||
'approvals/static/tests/**/*',
|
||||
('remove', 'approvals/static/tests/tours/**/*'),
|
||||
],
|
||||
},
|
||||
'license': 'OEEL-1',
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="base.user_admin" model="res.users">
|
||||
<field eval="[(4, ref('approvals.group_approval_manager'))]" name="groups_id"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_business_trip" model="approval.category">
|
||||
<field name="name">Business Trip</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Suitcase.png"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="has_date">no</field>
|
||||
<field name="has_period">required</field>
|
||||
<field name="has_product">no</field>
|
||||
<field name="has_quantity">no</field>
|
||||
<field name="has_amount">no</field>
|
||||
<field name="has_reference">no</field>
|
||||
<field name="has_partner">no</field>
|
||||
<field name="has_payment_method">no</field>
|
||||
<field name="has_location">required</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
<field name="manager_approval">approver</field>
|
||||
</record>
|
||||
<record id="approval_category_data_borrow_items" model="approval.category">
|
||||
<field name="name">Borrow Items</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Books.png"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="has_date">no</field>
|
||||
<field name="has_period">required</field>
|
||||
<field name="has_product">required</field>
|
||||
<field name="has_quantity">optional</field>
|
||||
<field name="has_amount">no</field>
|
||||
<field name="has_reference">no</field>
|
||||
<field name="has_partner">no</field>
|
||||
<field name="has_payment_method">no</field>
|
||||
<field name="has_location">no</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
</record>
|
||||
<record id="approval_category_data_general_approval" model="approval.category">
|
||||
<field name="name">General Approval</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Folder.png"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="has_date">optional</field>
|
||||
<field name="has_period">optional</field>
|
||||
<field name="has_product">optional</field>
|
||||
<field name="has_quantity">optional</field>
|
||||
<field name="has_amount">optional</field>
|
||||
<field name="has_reference">optional</field>
|
||||
<field name="has_partner">optional</field>
|
||||
<field name="has_payment_method">optional</field>
|
||||
<field name="has_location">optional</field>
|
||||
<field name="requirer_document">required</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
<field name="manager_approval">approver</field>
|
||||
</record>
|
||||
<record id="approval_category_data_contract_approval" model="approval.category">
|
||||
<field name="name">Contract Approval</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Contract.png"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="has_date">no</field>
|
||||
<field name="has_period">no</field>
|
||||
<field name="has_product">no</field>
|
||||
<field name="has_quantity">no</field>
|
||||
<field name="has_amount">optional</field>
|
||||
<field name="has_reference">required</field>
|
||||
<field name="has_partner">required</field>
|
||||
<field name="has_payment_method">no</field>
|
||||
<field name="has_location">no</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
</record>
|
||||
<record id="approval_category_data_payment_application" model="approval.category">
|
||||
<field name="name">Payment Application</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Credit-card.png"/>
|
||||
<field name="sequence">40</field>
|
||||
<field name="has_date">required</field>
|
||||
<field name="has_period">no</field>
|
||||
<field name="has_product">no</field>
|
||||
<field name="has_quantity">no</field>
|
||||
<field name="has_amount">required</field>
|
||||
<field name="has_reference">no</field>
|
||||
<field name="has_partner">required</field>
|
||||
<field name="has_payment_method">required</field>
|
||||
<field name="has_location">no</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
</record>
|
||||
<record id="approval_category_data_car_rental_application" model="approval.category">
|
||||
<field name="name">Car Rental Application</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Car.png"/>
|
||||
<field name="sequence">50</field>
|
||||
<field name="has_date">no</field>
|
||||
<field name="has_period">required</field>
|
||||
<field name="has_product">no</field>
|
||||
<field name="has_quantity">no</field>
|
||||
<field name="has_amount">no</field>
|
||||
<field name="has_reference">no</field>
|
||||
<field name="has_partner">no</field>
|
||||
<field name="has_payment_method">no</field>
|
||||
<field name="has_location">no</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
</record>
|
||||
<record id="approval_category_data_job_referral_award" model="approval.category">
|
||||
<field name="name">Job Referral Award</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Award.png"/>
|
||||
<field name="sequence">60</field>
|
||||
<field name="has_date">no</field>
|
||||
<field name="has_period">no</field>
|
||||
<field name="has_product">no</field>
|
||||
<field name="has_quantity">no</field>
|
||||
<field name="has_amount">no</field>
|
||||
<field name="has_reference">no</field>
|
||||
<field name="has_partner">required</field>
|
||||
<field name="has_payment_method">no</field>
|
||||
<field name="has_location">no</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
</record>
|
||||
<record id="approval_category_data_procurement" model="approval.category">
|
||||
<field name="name">Procurement</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Bag.png"/>
|
||||
<field name="sequence">70</field>
|
||||
<field name="has_date">no</field>
|
||||
<field name="has_period">no</field>
|
||||
<field name="has_product">no</field>
|
||||
<field name="has_quantity">required</field>
|
||||
<field name="has_amount">optional</field>
|
||||
<field name="has_reference">no</field>
|
||||
<field name="has_partner">no</field>
|
||||
<field name="has_payment_method">no</field>
|
||||
<field name="has_location">no</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
<field name="approval_minimum">1</field>
|
||||
</record>
|
||||
<record id="approval_category_data_promote" model="approval.category">
|
||||
<field name="name">Promote</field>
|
||||
<field name="image" type="base64" file="approvals/static/src/img/Folder.png"/>
|
||||
<field name="sequence">80</field>
|
||||
<field name="has_date">no</field>
|
||||
<field name="has_period">required</field>
|
||||
<field name="has_product">no</field>
|
||||
<field name="has_quantity">no</field>
|
||||
<field name="has_amount">no</field>
|
||||
<field name="has_reference">optional</field>
|
||||
<field name="has_partner">no</field>
|
||||
<field name="has_payment_method">no</field>
|
||||
<field name="has_location">required</field>
|
||||
<field name="requirer_document">optional</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
@ -1,85 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[
|
||||
(3, ref('approvals.group_approval_user')),
|
||||
(3, ref('approvals.group_approval_manager')),
|
||||
]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_demo_business_trip" model="approval.request">
|
||||
<field name="name">Business trip to London</field>
|
||||
<field name="request_status">pending</field>
|
||||
<field name="location">London</field>
|
||||
<field name="category_id" ref="approval_category_data_business_trip"/>
|
||||
<field name="request_owner_id" ref="base.user_admin"/>
|
||||
<field name="date_start" eval="time.strftime('%Y-05-01')"/>
|
||||
<field name="date_end" eval="time.strftime('%Y-05-05')"/>
|
||||
<field name="reason">Meeting with a potential customer.</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_business_trip_1" model="approval.approver">
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="request_id" ref="approval_request_demo_business_trip"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_business_trip_2" model="approval.approver">
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="request_id" ref="approval_request_demo_business_trip"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_demo_borrow_items" model="approval.request">
|
||||
<field name="name">Borrow the screwdriver</field>
|
||||
<field name="request_status">pending</field>
|
||||
<field name="category_id" ref="approval_category_data_borrow_items"/>
|
||||
<field name="request_owner_id" ref="base.user_demo"/>
|
||||
<field name="date_start" eval="time.strftime('%Y-05-01')"/>
|
||||
<field name="date_end" eval="time.strftime('%Y-05-01')"/>
|
||||
<field name="product_line_ids" eval="[(0, 0, {'description': 'Screwdriver X15'})]"/>
|
||||
<field name="reason">Need a screwdriver to fix the main door.</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_borrow_items_1" model="approval.approver">
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="request_id" ref="approval_request_demo_borrow_items"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_borrow_items_2" model="approval.approver">
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="request_id" ref="approval_request_demo_borrow_items"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_business_trip" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_borrow_items" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_general_approval" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_contract_approval" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_payment_application" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_car_rental_application" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_job_referral_award" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_data_procurement" model="approval.category">
|
||||
<field name="approver_ids" eval="[(0, 0, {'user_id': ref('base.user_admin')})]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
@ -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>
|
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="mt_approval_request_status" model="mail.message.subtype">
|
||||
<field name="name">Approval Status Change</field>
|
||||
<field name="res_model">Approval.Request</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Approval Status Change</field>
|
||||
<field name="sequence">5</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,11 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import approval_category
|
||||
from . import approval_category_approver
|
||||
from . import approval_product_line
|
||||
from . import approval_request
|
||||
from . import mail_activity
|
||||
from . import ir_attachment
|
||||
from . import approval_category_condition
|
||||
from . import approval_promotion_line
|
@ -1,214 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
CATEGORY_SELECTION = [
|
||||
('required', 'Required'),
|
||||
('optional', 'Optional'),
|
||||
('no', 'None')]
|
||||
|
||||
|
||||
class ApprovalCategory(models.Model):
|
||||
# ----------------------------------- Private Attributes ---------------------------------
|
||||
_name = 'approval.category'
|
||||
_description = 'Approval Category'
|
||||
_order = 'sequence, id'
|
||||
|
||||
_check_company_auto = True
|
||||
DEFAULT_PYTHON_CODE = """# Available variables:
|
||||
# - env: environment on which the action is triggered
|
||||
# - model: model of the record on which the action is triggered; is a void recordset
|
||||
# - record: record on which the action is triggered; may be void
|
||||
# - records: recordset of all records on which the action is triggered in multi-mode; may be void
|
||||
# - time, datetime, dateutil, timezone: useful Python libraries
|
||||
# - float_compare: utility function to compare floats based on specific precision
|
||||
# - b64encode, b64decode: functions to encode/decode binary data
|
||||
# - log: log(message, level='info'): logging function to record debug information in ir.logging table
|
||||
# - _logger: _logger.info(message): logger to emit messages in server logs
|
||||
# - UserError: exception class for raising user-facing warning messages
|
||||
# - Command: x2many commands namespace
|
||||
# To return an action, assign: action = {...}\n\n\n\n"""
|
||||
def _get_default_image(self):
|
||||
default_image_path = 'approvals/static/src/img/Folder.png'
|
||||
return base64.b64encode(tools.misc.file_open(default_image_path, 'rb').read())
|
||||
|
||||
# ----------------------------------- Fields Declaration ----------------------------------
|
||||
name = fields.Char(string="Name", translate=True, required=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', copy=False,
|
||||
required=True, index=True, default=lambda s: s.env.company)
|
||||
active = fields.Boolean(default=True)
|
||||
sequence = fields.Integer(string="Sequence")
|
||||
description = fields.Char(string="Description", translate=True)
|
||||
image = fields.Binary(string='Image', default=_get_default_image)
|
||||
has_date = fields.Selection(CATEGORY_SELECTION, string="Has Date", default="no", required=True)
|
||||
has_period = fields.Selection(CATEGORY_SELECTION, string="Has Period", default="no", required=True)
|
||||
has_quantity = fields.Selection(CATEGORY_SELECTION, string="Has Quantity", default="no", required=True)
|
||||
has_amount = fields.Selection(CATEGORY_SELECTION, string="Has Amount", default="no", required=True)
|
||||
has_reference = fields.Selection(
|
||||
CATEGORY_SELECTION, string="Has Reference", default="no", required=True,
|
||||
help="An additional reference that should be specified on the request.")
|
||||
has_partner = fields.Selection(CATEGORY_SELECTION, string="Has Contact", default="no", required=True)
|
||||
has_payment_method = fields.Selection(CATEGORY_SELECTION, string="Has Payment", default="no", required=True)
|
||||
has_location = fields.Selection(CATEGORY_SELECTION, string="Has Location", default="no", required=True)
|
||||
has_product = fields.Selection(
|
||||
CATEGORY_SELECTION, string="Has Product", default="no", required=True,
|
||||
help="Additional products that should be specified on the request.")
|
||||
has_promotion = fields.Selection(
|
||||
CATEGORY_SELECTION, string="Has Promotion", default="no", required=True,
|
||||
help="Additional promotion that should be specified on the request.")
|
||||
|
||||
|
||||
requirer_document = fields.Selection([('required', 'Required'), ('optional', 'Optional')], string="Documents", default="optional", required=True)
|
||||
approval_minimum = fields.Integer(string="Minimum Approval", default="1", required=True)
|
||||
# invalid_minimum = fields.Boolean(compute='_compute_invalid_minimum')
|
||||
# invalid_minimum_warning = fields.Char(compute='_compute_invalid_minimum')
|
||||
approval_type = fields.Selection(string="Approval Type", selection=[],
|
||||
help="Allows you to define which documents you would like to create once the request has been approved")
|
||||
manager_approval = fields.Selection([('approver', 'Is Approver'), ('required', 'Is Required Approver')],
|
||||
string="Employee's Manager",
|
||||
help="""How the employee's manager interacts with this type of approval.
|
||||
|
||||
Empty: do nothing
|
||||
Is Approver: the employee's manager will be in the approver list
|
||||
Is Required Approver: the employee's manager will be required to approve the request.
|
||||
""")
|
||||
|
||||
|
||||
# ---------------------------------------- Relational -----------------------------------------
|
||||
user_ids = fields.Many2many('res.users', compute='_compute_user_ids', string="Approver Users")
|
||||
|
||||
approver_ids = fields.One2many('approval.category.approver', 'category_id', string="Approvers")
|
||||
conditions_ids = fields.One2many('approval.category.condition', 'category_id', string="Approval Condition")
|
||||
approver_sequence = fields.Boolean('Approvers Sequence?', help="If checked, the approvers have to approve in sequence (one after the other). If Employee's Manager is selected as approver, they will be the first in line.")
|
||||
|
||||
request_to_validate_count = fields.Integer("Number of requests to validate", compute="_compute_request_to_validate_count")
|
||||
automated_sequence = fields.Boolean('Automated Sequence?',
|
||||
help="If checked, the Approval Requests will have an automated generated name based on the given code.")
|
||||
|
||||
sequence_code = fields.Char(string="Code")
|
||||
sequence_id = fields.Many2one('ir.sequence', 'Reference Sequence',
|
||||
copy=False, check_company=True)
|
||||
model_id = fields.Many2one('ir.model', string='Model', store=True)
|
||||
model_name = fields.Char(string='Model Name', related='model_id.model', store=True)
|
||||
|
||||
# invalid_minimum = fields.Boolean("Invalid Minimum", compute="_compute_invalid_minimum", store=True, readonly=False)
|
||||
# invalid_minimum_warning = fields.Boolean("Invalid minimum Warning",compute="_compute_invalid_minimum_warning")
|
||||
# domain = fields.Char(string='Conditions')
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------- Methods -----------------------------------------
|
||||
|
||||
|
||||
|
||||
# @api.depends('conditions_ids')
|
||||
# def _compute_invalid_minimum(self):
|
||||
# for record in self:
|
||||
# # If you expect to compute the invalid_minimum for each condition, loop through them
|
||||
# invalid_minimums = record.conditions_ids.mapped('invalid_minimum')
|
||||
# if invalid_minimums:
|
||||
# # Handle the logic for determining invalid_minimum (example)
|
||||
# record.invalid_minimum = any(invalid_minimums)
|
||||
# @api.depends('conditions_ids')
|
||||
# def _compute_invalid_minimum_warning(self):
|
||||
# for record in self.filtered('conditions_ids'):
|
||||
# record.invalid_minimum_warning = record.conditions_ids.invalid_minimum_warning
|
||||
def _compute_request_to_validate_count(self):
|
||||
domain = [('request_status', '=', 'pending'), ('approver_ids.user_id', '=', self.env.user.id)]
|
||||
requests_data = self.env['approval.request']._read_group(domain, ['category_id'], ['__count'])
|
||||
requests_mapped_data = {category.id: count for category, count in requests_data}
|
||||
for category in self:
|
||||
category.request_to_validate_count = requests_mapped_data.get(category.id, 0)
|
||||
|
||||
# @api.depends_context('lang')
|
||||
# @api.depends('approval_minimum', 'approver_ids', 'manager_approval')
|
||||
# def _compute_invalid_minimum(self):
|
||||
# for record in self:
|
||||
# if record.approval_minimum > len(record.approver_ids) + int(bool(record.manager_approval)):
|
||||
# record.invalid_minimum = True
|
||||
# else:
|
||||
# record.invalid_minimum = False
|
||||
# record.invalid_minimum_warning = record.invalid_minimum and _('Your minimum approval exceeds the total of default approvers.')
|
||||
@api.depends('approver_ids')
|
||||
def _compute_user_ids(self):
|
||||
for record in self:
|
||||
record.user_ids = record.approver_ids.user_id
|
||||
|
||||
# @api.constrains('approval_minimum', 'approver_ids')
|
||||
# def _constrains_approval_minimum(self):
|
||||
# for record in self:
|
||||
# if record.approval_minimum < len(record.approver_ids.filtered('required')):
|
||||
# raise ValidationError(_('Minimum Approval must be equal or superior to the sum of required Approvers.'))
|
||||
|
||||
@api.constrains('approver_ids')
|
||||
def _constrains_approver_ids(self):
|
||||
# There seems to be a problem with how the database is updated which doesn't let use to an sql constraint for this
|
||||
# Issue is: records seem to be created before others are saved, meaning that if you originally have only user a
|
||||
# change user a to user b and add a new line with user a, the second line will be created and will trigger the constraint
|
||||
# before the first line will be updated which wouldn't trigger a ValidationError
|
||||
for record in self:
|
||||
if len(record.approver_ids) != len(record.approver_ids.user_id):
|
||||
raise ValidationError(_('An user may not be in the approver list multiple times.'))
|
||||
# @api.constrains('approver_sequence', 'approval_minimum')
|
||||
# def _constrains_approver_sequence(self):
|
||||
# if any(a.approver_sequence and not a.approval_minimum for a in self):
|
||||
# raise ValidationError(_('Approver Sequence can only be activated with at least 1 minimum approver.'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('automated_sequence'):
|
||||
sequence = self.env['ir.sequence'].create({
|
||||
'name': _('Sequence %(code)s', code=vals['sequence_code']),
|
||||
'padding': 5,
|
||||
'prefix': vals['sequence_code'],
|
||||
'company_id': vals.get('company_id'),
|
||||
})
|
||||
vals['sequence_id'] = sequence.id
|
||||
res = super().create(vals_list)
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
if 'sequence_code' in vals:
|
||||
for approval_category in self:
|
||||
sequence_vals = {
|
||||
'name': _('Sequence %(code)s', code=vals['sequence_code']),
|
||||
'padding': 5,
|
||||
'prefix': vals['sequence_code'],
|
||||
}
|
||||
if approval_category.sequence_id:
|
||||
approval_category.sequence_id.write(sequence_vals)
|
||||
else:
|
||||
sequence_vals['company_id'] = vals.get('company_id', approval_category.company_id.id)
|
||||
sequence = self.env['ir.sequence'].create(sequence_vals)
|
||||
approval_category.sequence_id = sequence
|
||||
if 'company_id' in vals:
|
||||
for approval_category in self:
|
||||
if approval_category.sequence_id:
|
||||
approval_category.sequence_id.company_id = vals.get('company_id')
|
||||
|
||||
res = super().write(vals)
|
||||
return res
|
||||
|
||||
def create_request(self):
|
||||
self.ensure_one()
|
||||
# If category uses sequence, set next sequence as name
|
||||
# (if not, set category name as default name).
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "approval.request",
|
||||
"views": [[False, "form"]],
|
||||
"context": {
|
||||
'default_name': _('New') if self.automated_sequence else self.name,
|
||||
'default_category_id': self.id,
|
||||
'default_request_owner_id': self.env.user.id,
|
||||
'default_request_status': 'new'
|
||||
},
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class ApprovalCategoryApprover(models.Model):
|
||||
""" Intermediate model between approval.category and res.users
|
||||
To know whether an approver for this category is required or not
|
||||
"""
|
||||
_name = 'approval.category.approver'
|
||||
_description = 'Approval Type Approver'
|
||||
_rec_name = 'user_id'
|
||||
_order = 'sequence'
|
||||
sequence = fields.Integer('Sequence', default=10)
|
||||
category_id = fields.Many2one('approval.category', string='Approval Type')
|
||||
condition_id = fields.Many2one('approval.category.condition', string='Approval Condition')
|
||||
company_id = fields.Many2one('res.company', related='condition_id.company_id')
|
||||
user_id = fields.Many2one('res.users', string='User', ondelete='cascade', required=True,
|
||||
check_company=True, domain="[('company_ids', 'in', company_id), ('id', 'not in', existing_user_ids)]")
|
||||
job_title = fields.Char(string='Job Position', compute='_compute_job_title', store=True)
|
||||
existing_user_ids = fields.Many2many('res.users', compute='_compute_existing_user_ids')
|
||||
required = fields.Boolean(default=False)
|
||||
|
||||
|
||||
@api.depends('user_id', 'company_id')
|
||||
def _compute_job_title(self):
|
||||
for record in self:
|
||||
if record.user_id and record.company_id:
|
||||
# Search for the employee linked to the user and company
|
||||
employee = self.env['hr.employee'].search([
|
||||
('user_id', '=', record.user_id.id),
|
||||
('company_id', '=', record.company_id.id)
|
||||
], limit=1)
|
||||
record.job_title = employee.job_title if employee else ''
|
||||
else:
|
||||
record.job_title = ''
|
||||
@api.depends('condition_id')
|
||||
def _compute_existing_user_ids(self):
|
||||
for record in self:
|
||||
record.existing_user_ids = record.condition_id.approver_ids.user_id
|
@ -1,84 +0,0 @@
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
class ApprovalConditions(models.Model):
|
||||
_name = 'approval.category.condition'
|
||||
_description = 'Approval Condition'
|
||||
_order = 'sequence, id'
|
||||
|
||||
sequence = fields.Integer(default=1)
|
||||
description = fields.Text(string="Description")
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', copy=False,
|
||||
required=True, index=True, default=lambda s: s.env.company)
|
||||
approver_sequence = fields.Boolean('Approvers Sequence?', help="If checked, the approvers have to approve in sequence.")
|
||||
approval_minimum = fields.Integer(string="Minimum Approval", default=5, required=True)
|
||||
invalid_minimum = fields.Boolean(compute='_compute_invalid_minimum')
|
||||
invalid_minimum_warning = fields.Char(compute='_compute_invalid_minimum')
|
||||
approver_ids = fields.One2many('approval.category.approver', 'condition_id', string="Approvers")
|
||||
user_ids = fields.Many2many('res.users', compute='_compute_user_ids', string="Approver Users")
|
||||
category_id = fields.Many2one('approval.category', string="Approval Category")
|
||||
model_id = fields.Many2one('ir.model', string='Model', related='category_id.model_id', store=True, readonly=True)
|
||||
model_name = fields.Char(
|
||||
string='Model Name',
|
||||
related='model_id.model',
|
||||
store=True,
|
||||
readonly=True
|
||||
)
|
||||
domain = fields.Char(string="Conditions", readonly=False, store=True)
|
||||
|
||||
# @api.depends('category_id.model_id')
|
||||
# def _compute_model_id(self):
|
||||
# """Compute the model_id from the related approval category."""
|
||||
# for record in self:
|
||||
# record.model_id = record.category_id.model_id
|
||||
|
||||
@api.depends('approver_ids')
|
||||
def _compute_user_ids(self):
|
||||
for record in self:
|
||||
record.user_ids = record.approver_ids.user_id
|
||||
|
||||
@api.depends('approval_minimum', 'approver_ids')
|
||||
def _compute_invalid_minimum(self):
|
||||
for record in self:
|
||||
if record.approval_minimum >= len(record.approver_ids):
|
||||
record.invalid_minimum = False
|
||||
else:
|
||||
record.invalid_minimum = True
|
||||
record.invalid_minimum_warning = record.invalid_minimum and _('Your minimum approval exceeds the total of default approvers.')
|
||||
|
||||
# @api.depends('approver_ids')
|
||||
# def _compute_user_ids(self):
|
||||
# for record in self:
|
||||
# record.user_ids = record.approver_ids.user_id
|
||||
|
||||
# @api.depends('model_name')
|
||||
# def _compute_filter_id(self):
|
||||
# for record in self:
|
||||
# record.filter_id = False
|
||||
|
||||
@api.constrains('approver_ids')
|
||||
def _constrains_approver_ids(self):
|
||||
# There seems to be a problem with how the database is updated which doesn't let use to an sql constraint for this
|
||||
# Issue is: records seem to be created before others are saved, meaning that if you originally have only user a
|
||||
# change user a to user b and add a new line with user a, the second line will be created and will trigger the constraint
|
||||
# before the first line will be updated which wouldn't trigger a ValidationError
|
||||
for record in self:
|
||||
if len(record.approver_ids) != len(record.approver_ids.user_id):
|
||||
raise ValidationError(_('An user may not be in the approver list multiple times.'))
|
||||
@api.constrains('approval_minimum', 'approver_ids')
|
||||
def _constrains_approval_minimum(self):
|
||||
for record in self:
|
||||
if record.approval_minimum < len(record.approver_ids):
|
||||
raise ValidationError(_('Minimum Approval must be equal or superior to the sum of required Approvers.'))
|
||||
|
||||
@api.constrains('approver_sequence', 'approval_minimum')
|
||||
def _constrains_approver_sequence(self):
|
||||
for record in self:
|
||||
if record.approver_sequence and record.approval_minimum < 1:
|
||||
raise ValidationError(_('Approver Sequence can only be activated with at least 1 minimum approver.'))
|
||||
|
||||
def delete_condition(self):
|
||||
for record in self:
|
||||
record.unlink()
|
||||
|
@ -1,37 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ApprovalProductLine(models.Model):
|
||||
_name = 'approval.product.line'
|
||||
_description = 'Product Line'
|
||||
|
||||
_check_company_auto = True
|
||||
|
||||
approval_request_id = fields.Many2one('approval.request', required=True)
|
||||
description = fields.Char(
|
||||
"Description", required=True,
|
||||
compute="_compute_description", store=True, readonly=False, precompute=True)
|
||||
company_id = fields.Many2one(
|
||||
string='Company', related='approval_request_id.company_id',
|
||||
store=True, readonly=True, index=True)
|
||||
product_id = fields.Many2one('product.product', string="Products", check_company=True)
|
||||
product_uom_id = fields.Many2one(
|
||||
'uom.uom', string="Unit of Measure",
|
||||
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True,
|
||||
domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
quantity = fields.Float("Quantity", default=1.0)
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_description(self):
|
||||
for line in self:
|
||||
line.description = line.product_id.description_purchase or line.product_id.display_name
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_product_uom_id(self):
|
||||
for line in self:
|
||||
line.product_uom_id = line.product_id.uom_id
|
@ -1,32 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class ApprovalPromotionLine(models.Model):
|
||||
_name = 'approval.promotion.line'
|
||||
_description = 'Promote Line'
|
||||
|
||||
_check_company_auto = True
|
||||
|
||||
approval_request_id = fields.Many2one('approval.request', required=True)
|
||||
description = fields.Char(
|
||||
"Description", required=True,
|
||||
compute="_compute_promotion", store=True, readonly=False, precompute=True)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
string='Company', related='approval_request_id.company_id',
|
||||
store=True, readonly=True, index=True)
|
||||
promote_id = fields.Many2one('hr.promote', string="Promotion", check_company=True)
|
||||
current_job = fields.Char(
|
||||
"Current Job", required=True,
|
||||
compute="_compute_promotion", store=True, readonly=True, precompute=True)
|
||||
new_designation = fields.Char(
|
||||
"New Designation", required=True,
|
||||
compute="_compute_promotion", store=True, readonly=True, precompute=True)
|
||||
@api.depends('promote_id')
|
||||
def _compute_promotion(self):
|
||||
for line in self:
|
||||
line.description = line.promote_id.description or line.promote_id.display_name
|
||||
line.current_job = line.promote_id.job_id.name or ''
|
||||
line.new_designation = line.promote_id.designation_id.name or ''
|
@ -1,508 +0,0 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
from odoo import api, Command, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
_logger = logging.getLogger(__name__)
|
||||
class ApprovalRequest(models.Model):
|
||||
_name = 'approval.request'
|
||||
_description = 'Approval Request'
|
||||
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin']
|
||||
_order = 'name'
|
||||
_mail_post_access = 'read'
|
||||
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char(string="Approval Subject", tracking=True)
|
||||
category_id = fields.Many2one('approval.category', string="Category", required=True,ondelete='cascade')
|
||||
category_image = fields.Binary(related='category_id.image')
|
||||
approver_ids = fields.One2many('approval.approver', 'request_id', string="Approvers", check_company=True,
|
||||
compute='_compute_approver_ids', store=True, readonly=False)
|
||||
user_ids = fields.Many2many('res.users', string="Users",
|
||||
compute='_compute_user_ids', readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
string='Company', related='category_id.company_id',
|
||||
store=True, readonly=True, index=True)
|
||||
date = fields.Datetime(string="Date")
|
||||
date_start = fields.Datetime(string="Date start")
|
||||
date_end = fields.Datetime(string="Date end")
|
||||
quantity = fields.Float(string="Quantity")
|
||||
location = fields.Char(string="Location")
|
||||
date_confirmed = fields.Datetime(string="Date Confirmed")
|
||||
partner_id = fields.Many2one('res.partner', string="Contact", check_company=True)
|
||||
reference = fields.Char(string="Reference")
|
||||
amount = fields.Float(string="Amount")
|
||||
reason = fields.Html(string="Description")
|
||||
request_status = fields.Selection([
|
||||
('new', 'To Submit'),
|
||||
('pending', 'Submitted'),
|
||||
('approved', 'Approved'),
|
||||
('refused', 'Refused'),
|
||||
('cancel', 'Canceled'),
|
||||
], default="new", compute="_compute_request_status",
|
||||
store=True, index=True, tracking=True,
|
||||
group_expand=True)
|
||||
request_owner_id = fields.Many2one('res.users', string="Request Owner",
|
||||
check_company=True, domain="[('company_ids', 'in', company_id)]",
|
||||
default=lambda self: self.env.user)
|
||||
user_status = fields.Selection([
|
||||
('new', 'New'),
|
||||
('pending', 'To Approve'),
|
||||
('waiting', 'Waiting'),
|
||||
('approved', 'Approved'),
|
||||
('refused', 'Refused'),
|
||||
('cancel', 'Canceled')], compute="_compute_user_status")
|
||||
has_access_to_request = fields.Boolean(string="Has Access To Request", compute="_compute_has_access_to_request")
|
||||
change_request_owner = fields.Boolean(string='Can Change Request Owner', compute='_compute_has_access_to_request')
|
||||
attachment_ids = fields.One2many(comodel_name='ir.attachment', inverse_name='res_id', domain=[('res_model', '=', 'approval.request')], string='Attachments')
|
||||
attachment_number = fields.Integer('Number of Attachments', compute='_compute_attachment_number')
|
||||
product_line_ids = fields.One2many('approval.product.line', 'approval_request_id', check_company=True)
|
||||
promotion_line_ids = fields.One2many('approval.promotion.line', 'approval_request_id', check_company=True)
|
||||
has_date = fields.Selection(related="category_id.has_date")
|
||||
has_period = fields.Selection(related="category_id.has_period")
|
||||
has_quantity = fields.Selection(related="category_id.has_quantity")
|
||||
has_amount = fields.Selection(related="category_id.has_amount")
|
||||
has_reference = fields.Selection(related="category_id.has_reference")
|
||||
has_partner = fields.Selection(related="category_id.has_partner")
|
||||
has_payment_method = fields.Selection(related="category_id.has_payment_method")
|
||||
has_location = fields.Selection(related="category_id.has_location")
|
||||
has_product = fields.Selection(related="category_id.has_product")
|
||||
has_promotion = fields.Selection(related="category_id.has_promotion")
|
||||
requirer_document = fields.Selection(related="category_id.requirer_document")
|
||||
approval_minimum = fields.Integer(related="category_id.approval_minimum")
|
||||
approval_type = fields.Selection(related="category_id.approval_type")
|
||||
approver_sequence = fields.Boolean(related="category_id.approver_sequence")
|
||||
automated_sequence = fields.Boolean(related="category_id.automated_sequence")
|
||||
@api.depends('approver_ids')
|
||||
def _compute_user_ids(self):
|
||||
for request in self:
|
||||
request.user_ids = request.approver_ids.user_id
|
||||
|
||||
@api.depends('request_owner_id')
|
||||
@api.depends_context('uid')
|
||||
def _compute_has_access_to_request(self):
|
||||
is_approval_user = self.env.user.has_group('approvals.group_approval_user')
|
||||
self.change_request_owner = is_approval_user
|
||||
for request in self:
|
||||
request.has_access_to_request = request.request_owner_id == self.env.user and is_approval_user
|
||||
|
||||
def _compute_attachment_number(self):
|
||||
domain = [('res_model', '=', 'approval.request'), ('res_id', 'in', self.ids)]
|
||||
attachment_data = self.env['ir.attachment']._read_group(domain, ['res_id'], ['__count'])
|
||||
attachment = dict(attachment_data)
|
||||
for request in self:
|
||||
request.attachment_number = attachment.get(request.id, 0)
|
||||
|
||||
@api.constrains('date_start', 'date_end')
|
||||
def _check_dates(self):
|
||||
for request in self:
|
||||
if request.date_start and request.date_end and request.date_start > request.date_end:
|
||||
raise ValidationError(_("Start date should precede the end date."))
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
category = 'category_id' in vals and self.env['approval.category'].browse(vals['category_id'])
|
||||
if category and category.automated_sequence:
|
||||
vals['name'] = category.sequence_id.next_by_id()
|
||||
created_requests = super().create(vals_list)
|
||||
self._check_conditions(created_requests)
|
||||
for request in created_requests:
|
||||
request.message_subscribe(partner_ids=request.request_owner_id.partner_id.ids)
|
||||
return created_requests
|
||||
def _check_conditions(self,request):
|
||||
data = request
|
||||
if isinstance(request, bool):
|
||||
_logger.error(f"Expected 'vals' to be a list, got {type(request)}")
|
||||
data = self
|
||||
for request in data:
|
||||
if request.has_promotion:
|
||||
promotion_lines = request.promotion_line_ids
|
||||
if promotion_lines:
|
||||
conditions = request.category_id.conditions_ids
|
||||
for condition in conditions:
|
||||
satisfy_condition = False
|
||||
domain = condition.domain if condition.domain else []
|
||||
|
||||
# Ensure domain is in list or tuple format
|
||||
if isinstance(domain, str):
|
||||
try:
|
||||
domain = safe_eval(domain)
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to evaluate domain: {e}")
|
||||
domain = []
|
||||
elif not isinstance(domain, (list, tuple)):
|
||||
domain = []
|
||||
|
||||
_logger.debug(f"Evaluating condition: {condition}, Domain: {domain}")
|
||||
|
||||
for promotion_line in promotion_lines:
|
||||
if not promotion_line.promote_id or not promotion_line.promote_id.employee_id:
|
||||
_logger.warning(f"No promote_id or employee_id found for promotion_line: {promotion_line.id}")
|
||||
continue
|
||||
|
||||
model_name = request.category_id.model_name
|
||||
# if not hasattr(self.env, model_name):
|
||||
# _logger.error(f"Invalid model name: {model_name}")
|
||||
# continue
|
||||
|
||||
result = self.env[model_name].search(domain)
|
||||
# Compare promotion_line with result
|
||||
for res in result:
|
||||
if promotion_line.promote_id.employee_id.id == res.id:
|
||||
satisfy_condition = True
|
||||
break
|
||||
if satisfy_condition:
|
||||
request._add_approvers(condition)
|
||||
else:
|
||||
_logger.info('Condition not satisfied')
|
||||
|
||||
def _add_approvers(self, condition):
|
||||
for request in self:
|
||||
approvers = condition.approver_ids
|
||||
for approver in approvers:
|
||||
existing_approver = request.approver_ids.filtered(lambda x: x.user_id == approver.user_id)
|
||||
if existing_approver:
|
||||
existing_approver.write({
|
||||
'status': 'pending',
|
||||
})
|
||||
else:
|
||||
self.env['approval.approver'].create({
|
||||
'user_id': approver.user_id.id,
|
||||
'request_id': request.id,
|
||||
'required':approver.required,
|
||||
'status': 'pending'
|
||||
})
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def unlink_attachments(self):
|
||||
attachment_ids = self.env['ir.attachment'].search([
|
||||
('res_model', '=', 'approval.request'),
|
||||
('res_id', 'in', self.ids),
|
||||
])
|
||||
if attachment_ids:
|
||||
attachment_ids.unlink()
|
||||
|
||||
def unlink(self):
|
||||
self.filtered(lambda a: a.has_product).product_line_ids.unlink()
|
||||
self.filtered(lambda a: a.has_promotion).promotion_line_ids.unlink()
|
||||
return super().unlink()
|
||||
|
||||
def action_get_attachment_view(self):
|
||||
self.ensure_one()
|
||||
res = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
|
||||
res['domain'] = [('res_model', '=', 'approval.request'), ('res_id', 'in', self.ids)]
|
||||
res['context'] = {'default_res_model': 'approval.request', 'default_res_id': self.id}
|
||||
return res
|
||||
|
||||
def action_confirm(self):
|
||||
# make sure that the manager is present in the list if he is required
|
||||
self.ensure_one()
|
||||
if self.category_id.manager_approval == 'required':
|
||||
employee = self.env['hr.employee'].search([('user_id', '=', self.request_owner_id.id), ('company_id', '=', self.company_id.id)], limit=1)
|
||||
if not employee.parent_id:
|
||||
raise UserError(_('This request needs to be approved by your manager. There is no manager linked to your employee profile.'))
|
||||
if not employee.parent_id.user_id:
|
||||
raise UserError(_('This request needs to be approved by your manager. There is no user linked to your manager.'))
|
||||
if not self.approver_ids.filtered(lambda a: a.user_id.id == employee.parent_id.user_id.id):
|
||||
raise UserError(_('This request needs to be approved by your manager. Your manager is not in the approvers list.'))
|
||||
if len(self.approver_ids) < self.approval_minimum:
|
||||
raise UserError(_("You have to add at least %s approvers to confirm your request.", self.approval_minimum))
|
||||
if self.requirer_document == 'required' and not self.attachment_number:
|
||||
raise UserError(_("You have to attach at least one document."))
|
||||
|
||||
approvers = self.approver_ids
|
||||
if self.approver_sequence:
|
||||
approvers = approvers.filtered(lambda a: a.status in ['new', 'pending', 'waiting'])
|
||||
|
||||
approvers[1:].sudo().write({'status': 'waiting'})
|
||||
approvers = approvers[0] if approvers and approvers[0].status != 'pending' else self.env['approval.approver']
|
||||
else:
|
||||
approvers = approvers.filtered(lambda a: a.status == 'new')
|
||||
approvers._create_activity()
|
||||
approvers.sudo().write({'status': 'pending'})
|
||||
self.sudo().write({'date_confirmed': fields.Datetime.now()})
|
||||
|
||||
def _get_user_approval_activities(self, user):
|
||||
domain = [
|
||||
('res_model', '=', 'approval.request'),
|
||||
('res_id', 'in', self.ids),
|
||||
('activity_type_id', '=', self.env.ref('approvals.mail_activity_data_approval').id),
|
||||
('user_id', '=', user.id)
|
||||
]
|
||||
activities = self.env['mail.activity'].search(domain)
|
||||
return activities
|
||||
|
||||
def _ensure_can_approve(self):
|
||||
if any(approval.approver_sequence and approval.user_status == 'waiting' for approval in self):
|
||||
raise ValidationError(_('You cannot approve before the previous approver.'))
|
||||
|
||||
def _update_next_approvers(self, new_status, approver, only_next_approver, cancel_activities=False):
|
||||
approvers_updated = self.env['approval.approver']
|
||||
for approval in self.filtered('approver_sequence'):
|
||||
current_approver = approval.approver_ids & approver
|
||||
approvers_to_update = approval.approver_ids.filtered(lambda a: a.status not in ['approved', 'refused'] and (a.sequence > current_approver.sequence or (a.sequence == current_approver.sequence and a.id > current_approver.id)))
|
||||
|
||||
if only_next_approver and approvers_to_update:
|
||||
approvers_to_update = approvers_to_update[0]
|
||||
approvers_updated |= approvers_to_update
|
||||
|
||||
approvers_updated.sudo().status = new_status
|
||||
if new_status == 'pending':
|
||||
approvers_updated._create_activity()
|
||||
if cancel_activities:
|
||||
approvers_updated.request_id._cancel_activities()
|
||||
|
||||
def _cancel_activities(self):
|
||||
approval_activity = self.env.ref('approvals.mail_activity_data_approval')
|
||||
activities = self.activity_ids.filtered(lambda a: a.activity_type_id == approval_activity)
|
||||
activities.unlink()
|
||||
|
||||
def action_approve(self, approver=None):
|
||||
self._ensure_can_approve()
|
||||
|
||||
if not isinstance(approver, models.BaseModel):
|
||||
approver = self.mapped('approver_ids').filtered(
|
||||
lambda approver: approver.user_id == self.env.user
|
||||
)
|
||||
approver.write({'status': 'approved'})
|
||||
# Send approval accepted message
|
||||
for approval in self:
|
||||
if approval.request_owner_id.partner_id:
|
||||
body = _("The request created on %(create_date)s by %(request_owner)s has been accepted.",
|
||||
create_date=approval.create_date.date(),
|
||||
request_owner=approval.request_owner_id.name)
|
||||
subject = _("The request %(request_name)s for %(request_owner)s has been accepted",
|
||||
request_name=approval.name,
|
||||
request_owner=approval.request_owner_id.name)
|
||||
approval.message_notify(
|
||||
body=body,
|
||||
subject=subject,
|
||||
partner_ids=approval.request_owner_id.partner_id.ids,
|
||||
)
|
||||
if self.has_promotion:
|
||||
all_confirm = True
|
||||
for approver in self.approver_ids:
|
||||
if approver.required and approver.status != 'approved':
|
||||
all_confirm = False
|
||||
if all_confirm:
|
||||
for promote in self.promotion_line_ids:
|
||||
promote_records = self.env['hr.promote'].search([('id', '=', promote.promote_id.id)],limit=1)
|
||||
promote_records.write({
|
||||
'state': 'confirmed'
|
||||
})
|
||||
self.sudo()._update_next_approvers('pending', approver, only_next_approver=True)
|
||||
self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback()
|
||||
|
||||
def action_refuse(self, approver=None):
|
||||
if not isinstance(approver, models.BaseModel):
|
||||
approver = self.mapped('approver_ids').filtered(
|
||||
lambda approver: approver.user_id == self.env.user
|
||||
)
|
||||
approver.write({'status': 'refused'})
|
||||
|
||||
# Send approval refused message
|
||||
for approval in self:
|
||||
if approval.request_owner_id.partner_id:
|
||||
body = _("The request created on %(create_date)s by %(request_owner)s has been refused.",
|
||||
create_date=approval.create_date.date(),
|
||||
request_owner=approval.request_owner_id.name)
|
||||
subject = _("The request %(request_name)s for %(request_owner)s has been refused",
|
||||
request_name=approval.name,
|
||||
request_owner=approval.request_owner_id.name)
|
||||
approval.message_notify(
|
||||
body=body,
|
||||
subject=subject,
|
||||
partner_ids=approval.request_owner_id.partner_id.ids,
|
||||
)
|
||||
|
||||
self.sudo()._update_next_approvers('refused', approver, only_next_approver=False, cancel_activities=True)
|
||||
self.sudo()._get_user_approval_activities(user=self.env.user).action_feedback()
|
||||
|
||||
def action_withdraw(self, approver=None):
|
||||
if not isinstance(approver, models.BaseModel):
|
||||
approver = self.mapped('approver_ids').filtered(
|
||||
lambda approver: approver.user_id == self.env.user
|
||||
)
|
||||
self.sudo()._update_next_approvers('waiting', approver, only_next_approver=False, cancel_activities=True)
|
||||
approver.write({'status': 'pending'})
|
||||
|
||||
def action_draft(self):
|
||||
self.mapped('approver_ids').write({'status': 'new'})
|
||||
|
||||
def action_cancel(self):
|
||||
self.sudo()._get_user_approval_activities(user=self.env.user).unlink()
|
||||
self.mapped('approver_ids').write({'status': 'cancel'})
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('approver_ids.status')
|
||||
def _compute_user_status(self):
|
||||
for approval in self:
|
||||
approval.user_status = approval.approver_ids.filtered(lambda approver: approver.user_id == self.env.user).status
|
||||
|
||||
@api.depends('approver_ids.status', 'approver_ids.required')
|
||||
def _compute_request_status(self):
|
||||
for request in self:
|
||||
status_lst = request.mapped('approver_ids.status')
|
||||
required_approved = all(a.status == 'approved' for a in request.approver_ids.filtered('required'))
|
||||
minimal_approver = request.approval_minimum if len(status_lst) >= request.approval_minimum else len(status_lst)
|
||||
if status_lst:
|
||||
if status_lst.count('cancel'):
|
||||
status = 'cancel'
|
||||
elif status_lst.count('refused'):
|
||||
status = 'refused'
|
||||
elif status_lst.count('new'):
|
||||
status = 'new'
|
||||
elif status_lst.count('approved') >= minimal_approver and required_approved:
|
||||
status = 'approved'
|
||||
else:
|
||||
status = 'pending'
|
||||
else:
|
||||
status = 'new'
|
||||
request.request_status = status
|
||||
|
||||
self.filtered_domain([('request_status', 'in', ['approved', 'refused', 'cancel'])])._cancel_activities()
|
||||
|
||||
@api.model
|
||||
def _update_approver_vals(self, approver_id_vals, approver, new_required, new_sequence):
|
||||
if approver.required != new_required or approver.sequence != new_sequence:
|
||||
approver_id_vals.append(Command.update(approver.id, {'required': new_required, 'sequence': new_sequence}))
|
||||
|
||||
@api.model
|
||||
def _create_or_update_approver(self, user_id, users_to_approver, approver_id_vals, required, sequence):
|
||||
if user_id not in users_to_approver.keys():
|
||||
approver_id_vals.append(Command.create({
|
||||
'user_id': user_id,
|
||||
'status': 'new',
|
||||
'required': required,
|
||||
'sequence': sequence,
|
||||
}))
|
||||
else:
|
||||
current_approver = users_to_approver.pop(user_id)
|
||||
self._update_approver_vals(approver_id_vals, current_approver, required, sequence)
|
||||
|
||||
@api.depends('category_id', 'request_owner_id')
|
||||
def _compute_approver_ids(self):
|
||||
for request in self:
|
||||
users_to_approver = {}
|
||||
for approver in request.approver_ids:
|
||||
users_to_approver[approver.user_id.id] = approver
|
||||
|
||||
users_to_category_approver = {}
|
||||
for approver in request.category_id.approver_ids:
|
||||
users_to_category_approver[approver.user_id.id] = approver
|
||||
|
||||
approver_id_vals = []
|
||||
|
||||
if request.category_id.manager_approval:
|
||||
employee = self.env['hr.employee'].search([('user_id', '=', request.request_owner_id.id)], limit=1)
|
||||
if employee.parent_id.user_id:
|
||||
manager_user_id = employee.parent_id.user_id.id
|
||||
manager_required = request.category_id.manager_approval == 'required'
|
||||
# We set the manager sequence to be lower than all others (9) so they are the first to approve.
|
||||
self._create_or_update_approver(manager_user_id, users_to_approver, approver_id_vals, manager_required, 9)
|
||||
if manager_user_id in users_to_category_approver.keys():
|
||||
users_to_category_approver.pop(manager_user_id)
|
||||
|
||||
for user_id in users_to_category_approver:
|
||||
self._create_or_update_approver(user_id, users_to_approver, approver_id_vals,
|
||||
users_to_category_approver[user_id].required,
|
||||
users_to_category_approver[user_id].sequence)
|
||||
|
||||
for current_approver in users_to_approver.values():
|
||||
# Reset sequence and required for the remaining approvers that are no (longer) part of the category approvers or managers.
|
||||
# Set the sequence of these manually added approvers to 1000, so that they always appear after the category approvers.
|
||||
self._update_approver_vals(approver_id_vals, current_approver, False, 1000)
|
||||
|
||||
request.update({'approver_ids': approver_id_vals})
|
||||
|
||||
def write(self, vals):
|
||||
if 'request_owner_id' in vals:
|
||||
for approval in self:
|
||||
approval.message_unsubscribe(partner_ids=approval.request_owner_id.partner_id.ids)
|
||||
res = super().write(vals)
|
||||
self._check_conditions(res)
|
||||
if 'request_owner_id' in vals:
|
||||
for approval in self:
|
||||
approval.message_subscribe(partner_ids=approval.request_owner_id.partner_id.ids)
|
||||
|
||||
if 'approver_ids' in vals:
|
||||
to_resequence = self.filtered_domain([('approver_sequence', '=', True), ('request_status', '=', 'pending')])
|
||||
for approval in to_resequence:
|
||||
if not approval.approver_ids.filtered(lambda a: a.status == 'pending'):
|
||||
approver = approval.approver_ids.filtered(lambda a: a.status == 'waiting')
|
||||
if approver:
|
||||
approver[0].status = 'pending'
|
||||
approver[0]._create_activity()
|
||||
|
||||
return res
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'request_status' in init_values:
|
||||
return self.env.ref('approvals.mt_approval_request_status')
|
||||
return super()._track_subtype(init_values)
|
||||
|
||||
@api.constrains('approver_ids')
|
||||
def _check_approver_ids(self):
|
||||
for request in self:
|
||||
# make sure the approver_ids are unique per request
|
||||
if len(request.approver_ids) != len(request.approver_ids.user_id):
|
||||
raise UserError(_("You cannot assign the same approver multiple times on the same request."))
|
||||
|
||||
class ApprovalApprover(models.Model):
|
||||
_name = 'approval.approver'
|
||||
_description = 'Approver'
|
||||
_order = 'sequence, id'
|
||||
|
||||
_check_company_auto = True
|
||||
|
||||
sequence = fields.Integer('Sequence', default=10)
|
||||
user_id = fields.Many2one('res.users', string="User", required=True, check_company=True, domain="['|', ('id', 'not in', existing_request_user_ids), ('id', '=', user_id)]")
|
||||
existing_request_user_ids = fields.Many2many('res.users', compute='_compute_existing_request_user_ids')
|
||||
status = fields.Selection([
|
||||
('new', 'New'),
|
||||
('pending', 'To Approve'),
|
||||
('waiting', 'Waiting'),
|
||||
('approved', 'Approved'),
|
||||
('refused', 'Refused'),
|
||||
('cancel', 'Cancel')], string="Status", default="new", readonly=True)
|
||||
request_id = fields.Many2one('approval.request', string="Request",
|
||||
ondelete='cascade', check_company=True)
|
||||
company_id = fields.Many2one(
|
||||
string='Company', related='request_id.company_id',
|
||||
store=True, readonly=True, index=True)
|
||||
required = fields.Boolean(default=False, readonly=True)
|
||||
category_approver = fields.Boolean(compute='_compute_category_approver')
|
||||
can_edit = fields.Boolean(compute='_compute_can_edit')
|
||||
can_edit_user_id = fields.Boolean(compute='_compute_can_edit', help="Simple users should not be able to remove themselves as approvers because they will lose access to the record if they misclick.")
|
||||
def action_approve(self):
|
||||
self.request_id.action_approve(self)
|
||||
|
||||
def action_refuse(self):
|
||||
self.request_id.action_refuse(self)
|
||||
|
||||
def _create_activity(self):
|
||||
for approver in self:
|
||||
approver.request_id.activity_schedule(
|
||||
'approvals.mail_activity_data_approval',
|
||||
user_id=approver.user_id.id)
|
||||
|
||||
@api.depends('request_id.request_owner_id', 'request_id.approver_ids.user_id')
|
||||
def _compute_existing_request_user_ids(self):
|
||||
for approver in self:
|
||||
approver.existing_request_user_ids = \
|
||||
self.mapped('request_id.approver_ids.user_id')._origin \
|
||||
| self.request_id.request_owner_id._origin
|
||||
|
||||
@api.depends('category_approver', 'user_id')
|
||||
def _compute_category_approver(self):
|
||||
for approval in self:
|
||||
approval.category_approver = approval.user_id in approval.request_id.category_id.approver_ids.user_id
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('user_id', 'category_approver')
|
||||
def _compute_can_edit(self):
|
||||
is_user = self.env.user.has_group('approvals.group_approval_user')
|
||||
for approval in self:
|
||||
approval.can_edit = not approval.user_id or not approval.category_approver or is_user
|
||||
approval.can_edit_user_id = is_user or approval.request_id.request_owner_id == self.env.user or not approval.user_id
|
@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_approved_approval_request(self):
|
||||
"""
|
||||
Prevent attachment deletion for an approval request
|
||||
that is in the approved, refused or cancel state.
|
||||
"""
|
||||
approval_request_ids = [attachment.res_id for attachment in self if attachment.res_model == 'approval.request' and not attachment.res_field]
|
||||
if not approval_request_ids:
|
||||
return
|
||||
approval_requests = self.env['approval.request'].browse(approval_request_ids)
|
||||
for approval_request in approval_requests:
|
||||
if approval_request.request_status in ['approved', 'refused', 'cancel']:
|
||||
raise UserError(_("You cannot unlink an attachment which is linked to a validated, refused or cancelled approval request."))
|
@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class MailActivity(models.Model):
|
||||
_inherit = "mail.activity"
|
||||
|
||||
def _to_store(self, store: Store):
|
||||
super()._to_store(store)
|
||||
activity_type_approval_id = self.env.ref("approvals.mail_activity_data_approval")
|
||||
for activity in self.filtered(
|
||||
lambda activity: activity["res_model"] == "approval.request"
|
||||
and activity.activity_type_id == activity_type_approval_id
|
||||
):
|
||||
request = self.env["approval.request"].browse(activity["res_id"])
|
||||
approver = request.approver_ids.filtered(
|
||||
lambda approver: activity.user_id == approver.user_id
|
||||
)
|
||||
store.add(activity, {"approver_id": approver.id, "approver_status": approver.status})
|
@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_report_approval_request" model="ir.actions.report">
|
||||
<field name="name">Approval Request</field>
|
||||
<field name="model">approval.request</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">approvals.report_approval_request</field>
|
||||
<field name="report_file">approvals.report_approval_request</field>
|
||||
<field name="print_report_name">'Approval - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="model_approval_request"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,193 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.module.category" id="base.module_category_human_resources_approvals">
|
||||
<field name="sequence">9</field>
|
||||
</record>
|
||||
|
||||
<record id="group_approval_user" model="res.groups">
|
||||
<field name="name">Officer: Approve all requests</field>
|
||||
<field name="category_id" ref="base.module_category_human_resources_approvals"/>
|
||||
<field name="implied_ids" eval="[(6, 0, [ref('base.group_user')])]"/>
|
||||
<field name="comment">The user will be able to access all requests and approve/refuse them.</field>
|
||||
</record>
|
||||
|
||||
<record id="group_approval_manager" model="res.groups">
|
||||
<field name="name">Administrator</field>
|
||||
<field name="comment">The user will have access to the approvals configuration.</field>
|
||||
<field name="category_id" ref="base.module_category_human_resources_approvals"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_approval_user'))]"/>
|
||||
</record>
|
||||
|
||||
<data noupdate="1">
|
||||
<record id="base.default_user" model="res.users">
|
||||
<field name="groups_id" eval="[(4,ref('approvals.group_approval_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Regular User -->
|
||||
<record id="approval_request_request_approver_rule" model="ir.rule">
|
||||
<field name="name">Approval Request: request approver rule</field>
|
||||
<field name="model_id" ref="model_approval_request"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="domain_force">['|', '|',
|
||||
('request_owner_id', '=', user.id),
|
||||
('approver_ids.user_id','=', user.id),
|
||||
'&',
|
||||
('category_id.manager_approval', 'in', ['approver', 'required']),
|
||||
('request_owner_id.parent_id.user_id', '=', user.id)]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_unlink_request_owner_rule" model="ir.rule">
|
||||
<field name="name">Approval Request: unlink request owner rule</field>
|
||||
<field name="model_id" ref="model_approval_request"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="domain_force">[('request_owner_id', '=', user.id), ('request_status', 'in', ['cancel', 'new'])]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_user_read_own" model="ir.rule">
|
||||
<field name="name">Approval Approver: read own request</field>
|
||||
<field name="model_id" ref="model_approval_approver"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="domain_force">['|', ('request_id.request_owner_id', '=', user.id), ('user_id', '=', user.id)]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_user_unlink_own" model="ir.rule">
|
||||
<field name="name">Approval Approver: unlink own request</field>
|
||||
<field name="model_id" ref="model_approval_approver"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="domain_force">['|', ('request_id.request_owner_id', '=', user.id), ('user_id', '=', user.id), ('request_id.request_status', '=', 'new')]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_user_change_own" model="ir.rule">
|
||||
<field name="name">Approval Approver: change own</field>
|
||||
<field name="model_id" ref="model_approval_approver"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_user_create" model="ir.rule">
|
||||
<field name="name">Approval Approver: create</field>
|
||||
<field name="model_id" ref="model_approval_approver"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="domain_force">[('request_id.request_owner_id', '=', user.id), ('request_id.request_status', 'in', ('new', 'cancel'))]</field>
|
||||
</record>
|
||||
|
||||
<!-- Approval User -->
|
||||
<record id="approval_request_user" model="ir.rule">
|
||||
<field name="name">Approval Request: user</field>
|
||||
<field name="model_id" ref="model_approval_request"/>
|
||||
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_user_unlink" model="ir.rule">
|
||||
<field name="name">Approval Request: user unlink</field>
|
||||
<field name="model_id" ref="model_approval_request"/>
|
||||
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="domain_force">[('request_status','=','cancel')]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_manager" model="ir.rule">
|
||||
<field name="name">Approval Approver: manage all</field>
|
||||
<field name="model_id" ref="model_approval_approver"/>
|
||||
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_manager_unlink" model="ir.rule">
|
||||
<field name="name">Approval Approver: unlink unapproved</field>
|
||||
<field name="model_id" ref="model_approval_approver"/>
|
||||
<field name="groups" eval="[(4, ref('group_approval_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="domain_force">[('request_id.request_status', 'in', ['new', 'pending', 'cancel'])]</field>
|
||||
</record>
|
||||
|
||||
<!-- Approval Administrator -->
|
||||
<record id="approval_request_manager" model="ir.rule">
|
||||
<field name="name">Approval Request: manager</field>
|
||||
<field name="model_id" ref="model_approval_request"/>
|
||||
<field name="groups" eval="[(4, ref('group_approval_manager'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_approver_manager" model="ir.rule">
|
||||
<field name="name">Approval Approver: manager</field>
|
||||
<field name="model_id" ref="model_approval_approver"/>
|
||||
<field name="groups" eval="[(4, ref('group_approval_manager'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
|
||||
<!-- Multi-company rules -->
|
||||
<record model="ir.rule" id="approval_request_rule">
|
||||
<field name="name">approval_request multi-company</field>
|
||||
<field name="model_id" search="[('model','=','approval.request')]" model="ir.model"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="approval_category_rule">
|
||||
<field name="name">approval_category multi-company</field>
|
||||
<field name="model_id" search="[('model','=','approval.category')]" model="ir.model"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="approval_approver_rule">
|
||||
<field name="name">approval_approver multi-company</field>
|
||||
<field name="model_id" search="[('model','=','approval.approver')]" model="ir.model"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="approval_product_line_rule">
|
||||
<field name="name">approval_product_line multi-company</field>
|
||||
<field name="model_id" search="[('model','=','approval.product.line')]" model="ir.model"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<function model="ir.rule" name="write">
|
||||
<value eval="[ref('approvals.approval_request_unlink_request_owner_rule')]"/>
|
||||
<value eval="{'domain_force': '[(\'request_owner_id.id\', \'=\', user.id), (\'request_status\', \'in\', (\'new\', \'cancel\'))]'}"/>
|
||||
</function>
|
||||
</odoo>
|
@ -1,21 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_approval_request,access_approval_request,model_approval_request,base.group_user,1,1,1,1
|
||||
access_approval_request_user,access_approval_request,model_approval_request,group_approval_user,1,1,1,1
|
||||
access_approval_request_manager,access_approval_request,model_approval_request,group_approval_manager,1,1,1,1
|
||||
access_approval_category,access_approval_category,model_approval_category,base.group_user,1,0,0,0
|
||||
access_approval_category_user,access_approval_category,model_approval_category,group_approval_user,1,0,0,0
|
||||
access_approval_category_manager,access_approval_category,model_approval_category,group_approval_manager,1,1,1,1
|
||||
access_approval_category_approver,access_approval_category_approver,model_approval_category_approver,base.group_user,1,0,0,0
|
||||
access_approval_category_condition,access_approval_category_condition,model_approval_category_condition,base.group_user,1,1,1,1
|
||||
access_approval_category_approver_user,access_approval_category_approver,model_approval_category_approver,group_approval_user,1,0,0,0
|
||||
access_approval_category_approver_manager,access_approval_category_approver,model_approval_category_approver,group_approval_manager,1,1,1,1
|
||||
access_approval_approver,access_approval_approver,model_approval_approver,base.group_user,1,1,1,1
|
||||
access_approval_approver_user,access_approval_approver,model_approval_approver,group_approval_user,1,1,1,1
|
||||
access_approval_approver_manager,access_approval_approver,model_approval_approver,group_approval_manager,1,1,1,1
|
||||
access_approval_product_line_user,access_approval_product_line,model_approval_product_line,base.group_user,1,1,1,1
|
||||
access_approval_product_line_manager,access_approval_product_line,model_approval_product_line,group_approval_manager,1,1,1,1
|
||||
access_approval_promotion_line_user,access_approval_promotion_line,model_approval_promotion_line,base.group_user,1,1,1,1
|
||||
access_approval_promotion_line,access_approval_promotion_line,model_approval_promotion_line,base.group_user,1,1,1,1
|
||||
access_approval_promotion_line_user,access_approval_promotion_line,model_approval_promotion_line,base.group_user,1,1,1,1
|
||||
access_approval_promotion_line_manager,access_approval_promotion_line,model_approval_promotion_line,group_approval_manager,1,1,1,1
|
||||
|
|
Before Width: | Height: | Size: 2.2 KiB |
@ -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 |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB |
@ -1,52 +0,0 @@
|
||||
// .o_approvals_category_actions_field,
|
||||
// .o_approvals_category_kanban_view {
|
||||
// .o_kanban_ungrouped {
|
||||
// padding: 0;
|
||||
|
||||
// .o_kanban_record {
|
||||
// width: 30%;
|
||||
// margin: 0;
|
||||
|
||||
// &.o_kanban_ghost {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .o_approvals_category_actions_field .o_kanban_ghost {
|
||||
// display: none;
|
||||
// }
|
||||
|
||||
// .o_approvals_category_kanban_view {
|
||||
// .o_kanban_grouped .row {
|
||||
// flex-direction: column !important;
|
||||
// gap: 0.5rem !important;
|
||||
|
||||
// > * {
|
||||
// width: 100% !important;
|
||||
|
||||
// > * {
|
||||
// margin: 0 0.5rem !important;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .o_kanban_ungrouped .o_kanban_record .oe_kanban_global_click {
|
||||
// border-top: 0;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
|
||||
// .o_widget_web_ribbon {
|
||||
// align-self: flex-start;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .o_form_view .o_group .o_field_widget, .o_form_view .o_inner_group .o_field_widget {
|
||||
// width: 50%;
|
||||
// }
|
||||
// .o_approvals_category_error {
|
||||
// pre {
|
||||
// max-height: 25vh !important;
|
||||
// }
|
||||
// }
|
@ -1,41 +0,0 @@
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
|
||||
export class ApprovalCategoryKanbanController extends KanbanController {
|
||||
async setup() {
|
||||
super.setup();
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
OpenNewApprovalRequest() {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "approval.request",
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ApprovalKanbanRenderer extends KanbanRenderer {
|
||||
async archiveRecord(record, active) {
|
||||
if (active) {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: _t("Are you sure that you want to archive this record?"),
|
||||
confirmLabel: _t("Archive"),
|
||||
confirm: () => {
|
||||
record.archive();
|
||||
this.props.list.load();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
} else {
|
||||
record.unarchive();
|
||||
this.props.list.load();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="approvals.ApprovalsCategoryKanbanView.Buttons">
|
||||
<button type="button" class="btn btn-primary" t-on-click="OpenNewApprovalRequest">
|
||||
New Request
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
@ -1,13 +0,0 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
|
||||
import { ApprovalKanbanRenderer, ApprovalCategoryKanbanController } from "./approvals_category_kanban_controller";
|
||||
|
||||
export const approvalsCategoryKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: ApprovalCategoryKanbanController,
|
||||
Renderer: ApprovalKanbanRenderer,
|
||||
buttonTemplate: "approvals.ApprovalsCategoryKanbanView.Buttons",
|
||||
};
|
||||
|
||||
registry.category("views").add("approvals_category_kanban", approvalsCategoryKanbanView);
|
@ -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.activity.approval" activity="props.activity" onChange="props.reloadParentView"/>
|
||||
<t t-else="">$0</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
@ -1,38 +0,0 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {import("@mail/core/web/activity_model").Activity} activity
|
||||
* @extends {Component<Props, Env>}
|
||||
*/
|
||||
export class Approval extends Component {
|
||||
static template = "approvals.Approval";
|
||||
static props = {
|
||||
activity: Object,
|
||||
onChange: Function,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.store = useState(useService("mail.store"));
|
||||
}
|
||||
|
||||
async onClickApprove() {
|
||||
await this.env.services.orm.call("approval.approver", "action_approve", [
|
||||
this.props.activity.approval.id,
|
||||
]);
|
||||
this.props.activity.remove();
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
async onClickRefuse() {
|
||||
await this.env.services.orm.call("approval.approver", "action_refuse", [
|
||||
this.props.activity.approval.id,
|
||||
]);
|
||||
this.props.activity.remove();
|
||||
this.props.onChange();
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="approvals.Approval">
|
||||
<div t-if="props.activity.approval.status === 'pending'">
|
||||
<t t-if="props.activity.user_id[0] === store.self.userId">
|
||||
<button class="btn btn-success btn-link ps-0 pt-0" t-on-click="onClickApprove">
|
||||
<i class="fa fa-check"/> Approve
|
||||
</button>
|
||||
<button class="btn btn-danger btn-link pt-0" t-on-click="onClickRefuse">
|
||||
<i class="fa fa-times"/> Refuse
|
||||
</button>
|
||||
</t>
|
||||
<span t-else="" class="text-warning ps-0 pt-0">
|
||||
<i class="fa fa-pencil"/> To Approve
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
@ -1,137 +0,0 @@
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
assertSteps,
|
||||
click,
|
||||
contains,
|
||||
start,
|
||||
startServer,
|
||||
step,
|
||||
openFormView,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { serverState, onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { defineApprovalsModels } from "@approvals/../tests/approvals_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineApprovalsModels();
|
||||
|
||||
test("activity with approval to be made by logged user", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const requestId = pyEnv["approval.request"].create({});
|
||||
pyEnv["approval.approver"].create({
|
||||
request_id: requestId,
|
||||
status: "pending",
|
||||
user_id: serverState.userId,
|
||||
});
|
||||
pyEnv["mail.activity"].create({
|
||||
can_write: true,
|
||||
res_id: requestId,
|
||||
res_model: "approval.request",
|
||||
user_id: serverState.userId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("approval.request", requestId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-Activity-sidebar");
|
||||
await contains(".o-mail-Activity-user");
|
||||
await contains(".o-mail-Activity-note", { count: 0 });
|
||||
await contains(".o-mail-Activity-details", { count: 0 });
|
||||
await contains(".o-mail-Activity-mailTemplates", { count: 0 });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Edit" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Cancel" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Mark Done" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Upload Document" });
|
||||
await contains(".o-mail-Activity button", { text: "Approve" });
|
||||
await contains(".o-mail-Activity button", { text: "Refuse" });
|
||||
});
|
||||
|
||||
test("activity with approval to be made by another user", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const requestId = pyEnv["approval.request"].create({});
|
||||
const userId = pyEnv["res.users"].create({
|
||||
partner_id: pyEnv["res.partner"].create({}),
|
||||
});
|
||||
pyEnv["approval.approver"].create({
|
||||
request_id: requestId,
|
||||
status: "pending",
|
||||
user_id: userId,
|
||||
});
|
||||
pyEnv["mail.activity"].create({
|
||||
can_write: true,
|
||||
res_id: requestId,
|
||||
res_model: "approval.request",
|
||||
user_id: userId,
|
||||
});
|
||||
await start();
|
||||
await openFormView("approval.request", requestId);
|
||||
await contains(".o-mail-Activity");
|
||||
await contains(".o-mail-Activity-sidebar");
|
||||
await contains(".o-mail-Activity-user");
|
||||
await contains(".o-mail-Activity-note", { count: 0 });
|
||||
await contains(".o-mail-Activity-details", { count: 0 });
|
||||
await contains(".o-mail-Activity-mailTemplates", { count: 0 });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Edit" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Cancel" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Mark Done" });
|
||||
await contains(".o-mail-Activity .btn", { count: 0, text: "Upload Document" });
|
||||
await contains(".o-mail-Activity button", { count: 0, text: "Approve" });
|
||||
await contains(".o-mail-Activity button", { count: 0, text: "Refuse" });
|
||||
await contains(".o-mail-Activity span", { text: "To Approve" });
|
||||
});
|
||||
|
||||
test("approve approval", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const requestId = pyEnv["approval.request"].create({});
|
||||
pyEnv["approval.approver"].create({
|
||||
request_id: requestId,
|
||||
status: "pending",
|
||||
user_id: serverState.userId,
|
||||
});
|
||||
pyEnv["mail.activity"].create({
|
||||
can_write: true,
|
||||
res_id: requestId,
|
||||
res_model: "approval.request",
|
||||
user_id: serverState.userId,
|
||||
});
|
||||
const def = new Deferred();
|
||||
onRpc("approval.approver", "action_approve", (args) => {
|
||||
expect(args.args.length).toBe(1);
|
||||
expect(args.args[0]).toBe(requestId);
|
||||
step("action_approve");
|
||||
def.resolve();
|
||||
});
|
||||
await start();
|
||||
await openFormView("approval.request", requestId);
|
||||
await click(".o-mail-Activity button", { text: "Approve" });
|
||||
await def;
|
||||
assertSteps(["action_approve"]);
|
||||
});
|
||||
|
||||
test("refuse approval", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const requestId = pyEnv["approval.request"].create({});
|
||||
pyEnv["approval.approver"].create({
|
||||
request_id: requestId,
|
||||
status: "pending",
|
||||
user_id: serverState.userId,
|
||||
});
|
||||
pyEnv["mail.activity"].create({
|
||||
can_write: true,
|
||||
res_id: requestId,
|
||||
res_model: "approval.request",
|
||||
user_id: serverState.userId,
|
||||
});
|
||||
const def = new Deferred();
|
||||
onRpc("approval.approver", "action_refuse", (args) => {
|
||||
expect(args.args.length).toBe(1);
|
||||
expect(args.args[0]).toBe(requestId);
|
||||
step("action_refuse");
|
||||
def.resolve();
|
||||
});
|
||||
await start();
|
||||
await openFormView("approval.request", requestId);
|
||||
await click(".o-mail-Activity button", { text: "Refuse" });
|
||||
await def;
|
||||
assertSteps(["action_refuse"]);
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
import { defineModels } from "@web/../tests/web_test_helpers";
|
||||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { ApprovalRequest } from "@approvals/../tests/mock_server/mock_models/approval_request";
|
||||
import { ApprovalApprover } from "@approvals/../tests/mock_server/mock_models/approval_approver";
|
||||
import { MailActivity } from "@approvals/../tests/mock_server/mock_models/mail_activity";
|
||||
|
||||
export function defineApprovalsModels() {
|
||||
return defineModels(approvalsModels);
|
||||
}
|
||||
|
||||
export const approvalsModels = {
|
||||
...mailModels,
|
||||
ApprovalRequest,
|
||||
ApprovalApprover,
|
||||
MailActivity
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ApprovalApprover extends models.ServerModel {
|
||||
_name = "approval.approver";
|
||||
|
||||
action_approve() {
|
||||
return true;
|
||||
}
|
||||
|
||||
action_refuse() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { models } from "@web/../tests/web_test_helpers";
|
||||
import { DEFAULT_MAIL_SEARCH_ID, DEFAULT_MAIL_VIEW_ID } from "@mail/../tests/mock_server/mock_models/constants";
|
||||
|
||||
export class ApprovalRequest extends models.ServerModel {
|
||||
_name = "approval.request";
|
||||
_views = {
|
||||
[`search, ${DEFAULT_MAIL_SEARCH_ID}`]: /* xml */ `<search/>`,
|
||||
[`form,${DEFAULT_MAIL_VIEW_ID}`]: /* xml */ `
|
||||
<form>
|
||||
<chatter/>
|
||||
</form>`,
|
||||
};
|
||||
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
|
||||
export class MailActivity extends mailModels.MailActivity {
|
||||
/** @param {number[]} ids */
|
||||
_to_store(ids, store) {
|
||||
super._to_store(...arguments);
|
||||
for (const activity of this._filter([
|
||||
["id", "in", ids],
|
||||
["res_model", "=", "approval.request"],
|
||||
])) {
|
||||
// check on activity type being approval not done here for simplicity
|
||||
const [approver] = this.env["approval.approver"]._filter([
|
||||
["request_id", "=", activity.res_id],
|
||||
["user_id", "=", activity.user_id],
|
||||
]);
|
||||
if (approver) {
|
||||
store.add(this.browse(activity.id), {
|
||||
approver_id: approver.id,
|
||||
approver_status: approver.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("approvals_tour", {
|
||||
url: "/odoo",
|
||||
steps: () => [
|
||||
{
|
||||
trigger: '.o_app[data-menu-xmlid="approvals.approvals_menu_root"]',
|
||||
content: "open approvals app",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "button.oe_kanban_action:first",
|
||||
content: "create new request",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.o_field_widget[name="name"] input',
|
||||
content: "give name",
|
||||
run: "edit Business Trip To Berlin",
|
||||
},
|
||||
{
|
||||
trigger: '.o_field_widget[name="date_start"] input',
|
||||
content: "give start date",
|
||||
run: "edit 12/13/2018 13:00:00",
|
||||
},
|
||||
{
|
||||
trigger: '.o_field_widget[name="date_end"] input',
|
||||
content: "give end date",
|
||||
run: "edit 12/20/2018 13:00:00",
|
||||
},
|
||||
{
|
||||
trigger: '.o_field_widget[name="location"] input',
|
||||
content: "give location",
|
||||
run: "edit Berlin, Schulz Hotel",
|
||||
},
|
||||
{
|
||||
trigger: 'div[name="reason"] .odoo-editor-editable',
|
||||
content: "give description",
|
||||
run: "editor (We need to go, because reason (and also for beer)))",
|
||||
},
|
||||
{
|
||||
trigger: 'a:contains("Approver(s)"):first',
|
||||
content: "open approvers page",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_x2many_list_row_add > a",
|
||||
content: "add an approver",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "select an approver",
|
||||
trigger: ".o_selected_row .o_input_dropdown input",
|
||||
run: "edit Marc",
|
||||
},
|
||||
{
|
||||
isActive: ["auto"],
|
||||
trigger: ".ui-autocomplete > li > a:contains(Marc)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_form_button_save",
|
||||
content: "save the request",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "button[name=action_confirm]:enabled",
|
||||
content: "confirm the request",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o-mail-Activity button:contains('Approve')",
|
||||
content: "approve the request via activity",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button[name="action_withdraw"]',
|
||||
content: "withdraw approver",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button[name="action_refuse"]',
|
||||
content: "refuse request",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button[aria-checked="true"][data-value="refused"]',
|
||||
content: "wait the request status compute",
|
||||
},
|
||||
{
|
||||
trigger: 'button[name="action_cancel"]',
|
||||
content: "cancel request",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button[name="action_draft"]',
|
||||
content: "back the request to draft",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "button[name=action_confirm]:enabled",
|
||||
content: "confirm the request again",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button[name="action_approve"]',
|
||||
content: "approve request",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: 'button[name="action_withdraw"]',
|
||||
content: "wait the the request to be approved",
|
||||
},
|
||||
],
|
||||
});
|
@ -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
|
@ -1,202 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.tests import common, Form
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestRequest(common.TransactionCase):
|
||||
def test_compute_request_status(self):
|
||||
category_test = self.env.ref('approvals.approval_category_data_business_trip')
|
||||
requester_user = self.env.ref('base.user_admin')
|
||||
record = self.env['approval.request'].create({
|
||||
'name': 'test request',
|
||||
'request_owner_id': requester_user.id,
|
||||
'category_id': category_test.id,
|
||||
'date_start': fields.Datetime.now(),
|
||||
'date_end': fields.Datetime.now(),
|
||||
'location': 'testland'
|
||||
})
|
||||
first_approver = self.env['approval.approver'].create({
|
||||
'user_id': 1,
|
||||
'request_id': record.id,
|
||||
'status': 'new'})
|
||||
second_approver = self.env['approval.approver'].create({
|
||||
'user_id': 2,
|
||||
'request_id': record.id,
|
||||
'status': 'new'})
|
||||
record.approver_ids = (first_approver | second_approver)
|
||||
|
||||
self.assertEqual(record.request_status, 'new')
|
||||
|
||||
record.action_confirm()
|
||||
|
||||
# Test case 1: Min approval = 1
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_approve(first_approver)
|
||||
self.assertEqual(record.request_status, 'approved')
|
||||
record.action_approve(second_approver)
|
||||
self.assertEqual(record.request_status, 'approved')
|
||||
record.action_withdraw(first_approver)
|
||||
self.assertEqual(record.request_status, 'approved')
|
||||
record.action_refuse(first_approver)
|
||||
self.assertEqual(record.request_status, 'refused')
|
||||
|
||||
# Test case 2: Min approval = 1
|
||||
category_test.approval_minimum = 2
|
||||
record.action_withdraw(first_approver)
|
||||
record.action_withdraw(second_approver)
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_approve(first_approver)
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_approve(second_approver)
|
||||
self.assertEqual(record.request_status, 'approved')
|
||||
record.action_withdraw(second_approver)
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_refuse(second_approver)
|
||||
self.assertEqual(record.request_status, 'refused')
|
||||
|
||||
# Test case 3: Check that cancel is erasing the old validations
|
||||
record.action_cancel()
|
||||
self.assertEqual(first_approver.status, 'cancel')
|
||||
self.assertEqual(second_approver.status, 'cancel')
|
||||
self.assertEqual(record.request_status, 'cancel')
|
||||
|
||||
# Test case 4: Set the approval request to draft
|
||||
record.action_draft()
|
||||
self.assertEqual(first_approver.status, 'new')
|
||||
self.assertEqual(second_approver.status, 'new')
|
||||
self.assertEqual(record.request_status, 'new')
|
||||
|
||||
# Test case 5: Set min approval to an impossible value to reach
|
||||
category_test.approval_minimum = 3
|
||||
with self.assertRaises(UserError):
|
||||
record.action_confirm()
|
||||
self.assertEqual(record.request_status, 'new')
|
||||
|
||||
def test_compute_request_status_with_required(self):
|
||||
category_test = self.env.ref('approvals.approval_category_data_business_trip')
|
||||
requester_user = self.env.ref('base.user_admin')
|
||||
record = self.env['approval.request'].create({
|
||||
'name': 'test request',
|
||||
'request_owner_id': requester_user.id,
|
||||
'category_id': category_test.id,
|
||||
'date_start': fields.Datetime.now(),
|
||||
'date_end': fields.Datetime.now(),
|
||||
'location': 'testland'
|
||||
})
|
||||
first_approver = self.env['approval.approver'].create({
|
||||
'user_id': 1,
|
||||
'request_id': record.id,
|
||||
'status': 'new',
|
||||
'required': True})
|
||||
second_approver = self.env['approval.approver'].create({
|
||||
'user_id': 2,
|
||||
'request_id': record.id,
|
||||
'status': 'new'})
|
||||
record.approver_ids = (first_approver | second_approver)
|
||||
|
||||
self.assertEqual(record.request_status, 'new')
|
||||
|
||||
record.action_confirm()
|
||||
|
||||
# Min approval = 1 but first approver IS required
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_approve(second_approver)
|
||||
# Min approval is met but required approvals are not
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_approve(first_approver)
|
||||
self.assertEqual(record.request_status, 'approved')
|
||||
|
||||
# Min approval = 2
|
||||
category_test.approval_minimum = 2
|
||||
record.action_withdraw(first_approver)
|
||||
record.action_withdraw(second_approver)
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_approve(first_approver)
|
||||
# All required approvals are met but not the minimal approval count
|
||||
self.assertEqual(record.request_status, 'pending')
|
||||
record.action_approve(second_approver)
|
||||
self.assertEqual(record.request_status, 'approved')
|
||||
|
||||
def test_product_line_compute_uom(self):
|
||||
category_test = self.env.ref('approvals.approval_category_data_business_trip')
|
||||
uom = self.env.ref('uom.product_uom_dozen')
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'foo',
|
||||
'uom_id': uom.id,
|
||||
})
|
||||
approval = self.env['approval.request'].create({
|
||||
'category_id': category_test.id,
|
||||
'product_line_ids': [
|
||||
Command.create({'product_id': product.id})
|
||||
],
|
||||
})
|
||||
self.assertEqual(approval.product_line_ids.description, 'foo')
|
||||
self.assertEqual(approval.product_line_ids.product_uom_id, uom)
|
||||
|
||||
def test_unlink_approval(self):
|
||||
"""
|
||||
There is no error when unlinking a draft request with a document attached
|
||||
or a binary field filled.
|
||||
"""
|
||||
approval = self.env['approval.request'].create({
|
||||
'name': 'test request',
|
||||
'category_id': self.env.ref('approvals.approval_category_data_business_trip').id,
|
||||
'date_start': fields.Datetime.now(),
|
||||
'date_end': fields.Datetime.now(),
|
||||
'location': 'testland'
|
||||
})
|
||||
self.env['ir.attachment'].create({
|
||||
'name': 'test.file',
|
||||
'res_id': approval.id,
|
||||
'res_model': 'approval.request',
|
||||
})
|
||||
|
||||
self.env['ir.model.fields'].create({
|
||||
'name': 'x_test_field',
|
||||
'model_id': self.env.ref('approvals.model_approval_request').id,
|
||||
'ttype': 'binary',
|
||||
})
|
||||
approval.x_test_field = 'test'
|
||||
approval.unlink()
|
||||
|
||||
def test_unlink_multiple_approvals_with_product_line(self):
|
||||
"""
|
||||
There is no error when unlinking a multiple approval requests with a
|
||||
product line.
|
||||
"""
|
||||
approvals = self.env['approval.request'].create([{
|
||||
'name': 'Approval Request 1',
|
||||
'category_id': self.env.ref('approvals.approval_category_data_borrow_items').id,
|
||||
'date_start': fields.Datetime.now(),
|
||||
'date_end': fields.Datetime.now(),
|
||||
'location': 'testland',
|
||||
}, {
|
||||
'name': 'Approval Request 1',
|
||||
'category_id': self.env.ref('approvals.approval_category_data_borrow_items').id,
|
||||
'date_start': fields.Datetime.now(),
|
||||
'date_end': fields.Datetime.now(),
|
||||
'location': 'testitems',
|
||||
}])
|
||||
product_line = self.env['approval.product.line'].create({
|
||||
'approval_request_id': approvals[0].id,
|
||||
'description': "Description",
|
||||
})
|
||||
|
||||
approvals.unlink()
|
||||
self.assertFalse(product_line.exists())
|
||||
self.assertFalse(approvals.exists())
|
||||
|
||||
def test_request_with_automated_sequence(self):
|
||||
approval_category = self.env['approval.category'].create({
|
||||
'name': 'Test Category',
|
||||
'automated_sequence': True,
|
||||
'sequence_code': '1234',
|
||||
})
|
||||
|
||||
request_form = Form(self.env['approval.request'])
|
||||
request_form.category_id = approval_category
|
||||
approval_request = request_form.save()
|
||||
self.assertEqual( approval_request.name, '123400001')
|
@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestUi(HttpCaseWithUserDemo):
|
||||
|
||||
def test_ui(self):
|
||||
self.env.ref('approvals.approval_category_data_business_trip').write({
|
||||
'approver_ids': [(5, 0, 0), (0, 0, {'user_id': self.env.ref('base.user_admin').id})],
|
||||
})
|
||||
self.start_tour("/odoo", 'approvals_tour', login='admin')
|
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Approval Category Approver -->
|
||||
<record id="approval_category_approver_view_tree" model="ir.ui.view">
|
||||
<field name="name">approval.category.approver.view.list</field>
|
||||
<field name="model">approval.category.approver</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="existing_user_ids" column_invisible="True"/>
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="user_id" options="{'no_create': True}"/>
|
||||
<field name="job_title"/>
|
||||
<field name="required"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,175 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="approval_category_action_new_request" model="ir.actions.act_window">
|
||||
<field name="name">Dashboard</field>
|
||||
<field name="path">approvals</field>
|
||||
<field name="res_model">approval.category</field>
|
||||
<field name="view_mode">kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_action" model="ir.actions.act_window">
|
||||
<field name="name">Approvals Types</field>
|
||||
<field name="res_model">approval.category</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_action_to_review_category" model="ir.actions.act_window">
|
||||
<field name="name">Approvals to review</field>
|
||||
<field name="res_model">approval.request</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('approver_ids.user_id', '=', uid), ('request_status', '=', 'pending'), ('category_id', '=', active_id)]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No new approvals to review
|
||||
</p>
|
||||
</field>
|
||||
|
||||
</record>
|
||||
|
||||
<record id="approval_category_view_tree" model="ir.ui.view">
|
||||
<field name="name">approval.category.view.list</field>
|
||||
<field name="model">approval.category</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_view_search" model="ir.ui.view">
|
||||
<field name="name">approval.category.search</field>
|
||||
<field name="model">approval.category</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<separator/>
|
||||
<filter name="archived" string="Archived" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_view_form" model="ir.ui.view">
|
||||
<field name="name">approval.category.view.form</field>
|
||||
<field name="model">approval.category</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<field name="image" widget="image" class="oe_avatar" options='{"preview_image": "image", "size": [80, 80]}'/>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Approval Type"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="e.g. Procurement"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
<field name="model_id"/>
|
||||
<field name="model_name" invisible="1"/>
|
||||
<field name="approval_type" invisible="1"/>
|
||||
<field name="automated_sequence"/>
|
||||
<field name="sequence_code" invisible="not automated_sequence" required="automated_sequence"/>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Conditions">
|
||||
<group>
|
||||
<field name="conditions_ids" widget="one2many_list" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Options">
|
||||
<group string="Fields" name="option_settings">
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="requirer_document" string="Document" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_partner" string="Contact" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_date" string="Date" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_period" string="Period" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_product" string="Product" force_save="1" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_promotion" string="Promotion" force_save="1" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_quantity" string="Quantity" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_amount" string="Amount" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_reference" string="Reference" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="has_payment_method" string="Payment" widget="radio" options="{'horizontal': true}" invisible="1"/>
|
||||
<field name="has_location" string="Location" widget="radio" options="{'horizontal': true}"/>
|
||||
</group>
|
||||
|
||||
<!-- <group string="Approvers" name="approvers">
|
||||
<field name="manager_approval"/>
|
||||
<separator colspan="2"/>
|
||||
<field name="approver_ids"/>
|
||||
<field name="approver_sequence" invisible="approval_minimum == 0"/>
|
||||
<field name="approval_minimum"/>
|
||||
<field name="invalid_minimum" invisible="1"/>
|
||||
<div class="text-warning" colspan="2" invisible="not invalid_minimum">
|
||||
<span class="fa fa-warning" title="Invalid minimum approvals"/><field name="invalid_minimum_warning" nolabel="1"/>
|
||||
</div>
|
||||
</group> -->
|
||||
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_open_approval_category" model="ir.actions.server">
|
||||
<field name="name">Open Approval Category</field>
|
||||
<field name="model_id" ref="model_approval_category"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "approval.category",
|
||||
"views": [[False, "form"]],
|
||||
"target": "new",
|
||||
"res_id": records.id,
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_category_view_kanban" model="ir.ui.view">
|
||||
<field name="name">approval.category.views.kanban</field>
|
||||
<field name="model">approval.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban create="false" can_open="0" class="o_modules_kanban" js_class="approvals_category_kanban">
|
||||
<field name="active"/>
|
||||
<templates>
|
||||
<t t-name="menu" groups="approvals.group_approval_user">
|
||||
<a t-if="widget.editable"
|
||||
role="menuitem"
|
||||
type="action"
|
||||
name="%(approvals.action_open_approval_category)d"
|
||||
class="dropdown-item">Edit</a>
|
||||
<a t-if="record.active.raw_value"
|
||||
role="menuitem"
|
||||
type="archive"
|
||||
class="dropdown-item">Archive</a>
|
||||
<a t-if="!record.active.raw_value"
|
||||
role="menuitem"
|
||||
type="unarchive"
|
||||
class="dropdown-item">Unarchive</a>
|
||||
</t>
|
||||
<t t-name="card" class="flex-row">
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<aside>
|
||||
<field name="image" widget="image" class="mt-2"/>
|
||||
</aside>
|
||||
<main class="ms-4">
|
||||
<field name="name" class="fw-bold fs-5"/>
|
||||
<field name="description" class="text-muted lh-1 mb-1 mt-1"/>
|
||||
<footer t-if="!selection_mode">
|
||||
<button type="object" class="btn btn-primary btn-sm" name="create_request" context="{'category_id':id}">New Request</button>
|
||||
<button type="action" class="btn btn-sm btn-secondary ms-auto" name="%(approvals.approval_request_action_to_review_category)d" groups="approvals.group_approval_user">To Review: <field name="request_to_validate_count"/></button>
|
||||
</footer>
|
||||
</main>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="approval_condition_view_form" model="ir.ui.view">
|
||||
<field name="name">approval.category.condition.form</field>
|
||||
<field name="model">approval.category.condition</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group string="Conditions" name="conditions">
|
||||
<field name="model_id"/>
|
||||
<field name="model_name" invisible="1"/>
|
||||
<field name="domain" widget="domain" options="{'model': 'model_name'}"/>
|
||||
</group>
|
||||
<group string="Approvers" name="approvers">
|
||||
<field name="approver_ids" domain="domain"/>
|
||||
<field name="sequence"/>
|
||||
<field name="approver_sequence" invisible="approval_minimum == 0"/>
|
||||
<field name="approval_minimum"/>
|
||||
<field name="invalid_minimum" invisible = "1"/>
|
||||
<div class="text-warning" colspan="2" invisible="not invalid_minimum">
|
||||
<span class="fa fa-warning" title="Invalid minimum approvals"/>
|
||||
<field name="invalid_minimum_warning" nolabel="1"/>
|
||||
</div>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_condition_view_list" model="ir.ui.view">
|
||||
<field name="name">approval.category.condition.list</field>
|
||||
<field name="model">approval.category.condition</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="description" colspan="2"/>
|
||||
<field name="domain" colspan="3"/>
|
||||
<!-- <field name="approver_sequence" colspan="3"/>
|
||||
<field name="approval_minimum" colspan="3"/> -->
|
||||
<!-- <button name="delete_condition" type="object" class="btn fa fa-trash fa-xl px-3 ms-auto" title="Delete Condition" /> -->
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="approval_condition_action" model="ir.actions.act_window">
|
||||
<field name="name">Approvals Conditions</field>
|
||||
<field name="res_model">approval.category.condition</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,75 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="approval_product_line_view_tree_independent" model="ir.ui.view">
|
||||
<field name="name">approval.product.line.view.list</field>
|
||||
<field name="model">approval.product.line</field>
|
||||
<!-- This priority should be lower than the regular list view,
|
||||
so studio takes it instead of the regular view,
|
||||
which is embedded in a form view by default -->
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom" string="Products">
|
||||
<field name="approval_request_id"/>
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
<field name="product_id"/>
|
||||
<field name="description"/>
|
||||
<field name="quantity"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"
|
||||
options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="product_uom_category_id" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_product_line_view_tree" model="ir.ui.view">
|
||||
<field name="name">approval.product.line.view.list</field>
|
||||
<field name="model">approval.product.line</field>
|
||||
<field name="priority">15</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom" string="Products">
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
<field name="product_id" required="parent.has_product == 'required'" context="{'search_default_filter_to_purchase': 1, 'search_view_ref': 'product.product_template_search_view'}"/>
|
||||
<field name="description"/>
|
||||
<field name="quantity" column_invisible="parent.has_quantity == 'no'" required="parent.has_quantity == 'required' and parent.request_status != 'new'"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"
|
||||
options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="product_uom_category_id" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_product_line_view_form" model="ir.ui.view">
|
||||
<field name="name">approval.product.line.view.form</field>
|
||||
<field name="model">approval.product.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Products">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_id"/>
|
||||
<field name="description"/>
|
||||
<field name="quantity"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_product_kanban_mobile_view" model="ir.ui.view">
|
||||
<field name="name">approval.product.kanban.mobile</field>
|
||||
<field name="model">approval.product.line</field>
|
||||
<field name="priority">15</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban editable="bottom" string="Products">
|
||||
<templates>
|
||||
<t t-name="card" class="d-flex justify-content-between">
|
||||
<field name="product_id" class="fw-bolder fs-5"/>
|
||||
<field name="quantity" class="fw-bolder fs-5"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="product_template_action" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="search_view_id" ref="product.product_template_search_view"/>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="context">{'search_default_filter_to_purchase': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No product found. Let's create one!
|
||||
</p><p>
|
||||
Define the components and finished products you wish to use in
|
||||
bill of materials and manufacturing orders.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="product_product_action" model="ir.actions.act_window">
|
||||
<field name="name">Product Variants</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="view_mode">list,form,kanban,activity</field>
|
||||
<field name="search_view_id" ref="product.product_template_search_view"/>
|
||||
<field name="context">{'search_default_filter_to_purchase': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new product variant
|
||||
</p><p>
|
||||
You must define a product for everything you sell or purchase,
|
||||
whether it's a storable product, a consumable or a service.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="approval_promotion_line_view_tree" model="ir.ui.view">
|
||||
<field name="name">approval.promotion.line.list</field>
|
||||
<field name="model">approval.promotion.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom" string="Promotions">
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
<field name="promote_id"/>
|
||||
<field name="current_job" />
|
||||
<field name="new_designation"/>
|
||||
<field name="description"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_promotion_line_view_form" model="ir.ui.view">
|
||||
<field name="name">approval.promotion.line.form</field>
|
||||
<field name="model">approval.promotion.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Promotions">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="promote_id"/>
|
||||
<field name="description"/>
|
||||
<!-- <field name="current_job" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="new_designation" options="{'no_create': True, 'no_open': True}"/> -->
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_promotion_kanban_mobile_view" model="ir.ui.view">
|
||||
<field name="name">approval.promotion.kanban.mobile</field>
|
||||
<field name="model">approval.promotion.line</field>
|
||||
<field name="priority">15</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban editable="bottom" string="Promotion">
|
||||
<templates>
|
||||
<t t-name="card" class="d-flex justify-content-between">
|
||||
<field name="promote_id" class="fw-bolder fs-5"/>
|
||||
<field name="new_designation" class="fw-bolder fs-5"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="promotion_action" model="ir.actions.act_window">
|
||||
<field name="name">Promotions</field>
|
||||
<field name="res_model">hr.promote</field>
|
||||
<field name="search_view_id" ref="action_hr_promote"/>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No promotion found. Let's create one!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
@ -1,100 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="report_aprroval_request_document">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="o" t-value="o.with_context(lang=o.partner_id.lang)"/>
|
||||
<h5 class="mt-2" t-field="o.category_id.name">Test Category</h5>
|
||||
<div class="row mt-4">
|
||||
<div class="col-6">
|
||||
<div class="row">
|
||||
<h6 class="col-3 fw-bold">Request By:</h6>
|
||||
<h6 t-field="o.request_owner_id">Marc Demo</h6>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6 class="col-3 fw-bold">Approvers:</h6>
|
||||
<div>
|
||||
<h6 t-foreach="o.approver_ids" t-as="approver_id" class="mb-2">
|
||||
<span t-field="approver_id.user_id.name">Mitchell Admin</span>
|
||||
<span t-if="approver_id.status == 'approved'">(✔)</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6 class="col-3 fw-bold">Status:</h6>
|
||||
<h6 t-field="o.request_status">Approved</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="row mt-1" t-if="o.date">
|
||||
<h6 class="col-2 fw-bold">Date:</h6>
|
||||
<h6 t-field="o.date">07/07/2023 10:00:00</h6>
|
||||
</div>
|
||||
<div class="row mt-1" t-if="o.date_start">
|
||||
<h6 class="col-2 fw-bold">Period:</h6>
|
||||
<h6>
|
||||
<span t-field="o.date_start">07/07/2023 10:00:00</span> <span class="fw-bold">To</span> <span t-field="o.date_end">07/08/2023 10:00:00</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="row mt-1" t-if="o.location">
|
||||
<h6 class="col-2 fw-bold">Location:</h6>
|
||||
<h6 t-field="o.location">Brussels</h6>
|
||||
</div>
|
||||
<div class="row mt-1" t-if="o.partner_id">
|
||||
<h6 class="col-2 fw-bold">Contact:</h6>
|
||||
<h6 t-field="o.partner_id">Mitchell Admin</h6>
|
||||
</div>
|
||||
<div class="row mt-1" t-if="o.amount">
|
||||
<h6 class="col-2 fw-bold">Amount:</h6>
|
||||
<h6 t-field="o.amount">1200</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page mt-4" t-if="o.product_line_ids">
|
||||
<div class="oe_structure"/>
|
||||
<table class="table table-sm mt-4">
|
||||
<thead class="o_black_border">
|
||||
<tr>
|
||||
<th name="th_description" class="text-start fw-bold">Product Description</th>
|
||||
<th name="th_expected_date" class="text-center fw-bold">Quantity</th>
|
||||
<th name="th_uom" class="text-center fw-bold">UoM</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="o.product_line_ids" t-as="product_line">
|
||||
<td><span t-field="product_line.product_id">Test Product</span></td>
|
||||
<td class="text-center"><span t-field="product_line.quantity">2</span></td>
|
||||
<td class="text-center"><span t-field="product_line.product_uom_id">Units</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
<div t-if="o.reason" class="mt-4">
|
||||
<h6>
|
||||
<span class="fw-bold">Description:</span>
|
||||
<span t-field="o.reason">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="row mt-4" t-if="o.reference">
|
||||
<div class="col-6" >
|
||||
<h5>Reference:</h5>
|
||||
<h6 t-field="o.reference">Test Reference</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4" t-if="o.attachment_ids.ids">
|
||||
<div class="col-6" >
|
||||
<h5>Documents:</h5>
|
||||
<h6 t-foreach="o.attachment_ids" t-as="attachment" t-out="attachment.name">Test Document</h6>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_approval_request">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="approvals.report_aprroval_request_document" t-lang="o.partner_id.lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
@ -1,378 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!--Request-->
|
||||
|
||||
<record id="approval_request_action" model="ir.actions.act_window">
|
||||
<field name="name">My Requests</field>
|
||||
<field name="res_model">approval.request</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="domain">[('request_owner_id','=',uid)]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No Approvals Requests
|
||||
</p>
|
||||
<p>
|
||||
Let's go to the <a type="action" class="text-primary" name="%(approvals.approval_category_action_new_request)d">new request</a> menu
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_action_to_review" model="ir.actions.act_window">
|
||||
<field name="name">Approvals to Review</field>
|
||||
<field name="res_model">approval.request</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">['&',('approver_ids.user_id','=',uid),('request_status','=','pending')]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No new approvals to review
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_action_all" model="ir.actions.act_window">
|
||||
<field name="name">All Approvals</field>
|
||||
<field name="res_model">approval.request</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No Approvals
|
||||
</p>
|
||||
<p>
|
||||
Let's go to the <a type="action" class="text-primary" name="%(approvals.approval_category_action_new_request)d">new request</a> menu
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_view_tree" model="ir.ui.view">
|
||||
<field name="name">approval.request.view.list</field>
|
||||
<field name="model">approval.request</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<list create="false" sample="1" decoration-info="request_status == 'new'">
|
||||
<field name="name"/>
|
||||
<field name="request_owner_id" widget="many2one_avatar_user"/>
|
||||
<field name="category_id"/>
|
||||
<field name="date_start" string="Request Start" optional="show"/>
|
||||
<field name="date_end" string="Request To" optional="show"/>
|
||||
<field name="date_confirmed" string="Confirm Date" optional="hide"/>
|
||||
<field name="location" string="Location" optional="hide"/>
|
||||
<field name="activity_ids" widget="list_activity" optional="hide"/>
|
||||
<field name="user_ids" string="Approvers" widget="many2many_avatar_user" optional="hide"/>
|
||||
<field name="request_status" decoration-info="request_status == 'new'" decoration-warning="request_status == 'pending'" decoration-success="request_status == 'approved'" decoration-danger="request_status == 'refused'" widget="badge"/>
|
||||
<field name="company_id" readonly="True" optional="hide" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_search_view_search" model="ir.ui.view">
|
||||
<field name="name">approval.request.search</field>
|
||||
<field name="model">approval.request</field>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<filter string="My Request" name="filter_my_request" domain="[('request_owner_id', '=', uid)]"/>
|
||||
<filter string="My Approvals to Review" name="filter_approvals_to_review"
|
||||
domain="[('approver_ids.user_id', '=', uid), ('request_status', '=', 'pending')]"/>
|
||||
<separator/>
|
||||
<filter string="Approved" name="filter_approved_requests" domain="[('request_status', '=', 'approved')]"/>
|
||||
<filter string="Refused" name="filter_refused_requests" domain="[('request_status', '=', 'refused')]"/>
|
||||
<separator/>
|
||||
<filter string="Canceled" name="filter_canceled_requests" domain="[('request_status', '=', 'cancel')]"/>
|
||||
<group string="Group By">
|
||||
<filter string="Request Owner" name="groupby_request_owner"
|
||||
context="{'group_by': 'request_owner_id'}"/>
|
||||
<filter string="Category" name="groupby_category_id"
|
||||
context="{'group_by': 'category_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_view_form" model="ir.ui.view">
|
||||
<field name="name">approval.request.view.form</field>
|
||||
<field name="model">approval.request</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Request" create="false">
|
||||
<header>
|
||||
<field name="user_status" invisible="1"/>
|
||||
<field name="has_access_to_request" invisible="1"/>
|
||||
<widget name="attach_document" string="Attach Document" action="message_post" highlight="1" invisible="(attachment_number >= 1 and request_owner_id == uid) or request_owner_id != uid"/>
|
||||
<button name="action_confirm" string="Submit" type="object" invisible="attachment_number < 1 or not approver_ids or request_status != 'new'" class="btn-primary" data-hotkey="q"/>
|
||||
<button name="action_confirm" string="Submit" type="object" invisible="attachment_number >= 1 or not approver_ids or request_status != 'new'" data-hotkey="q"/>
|
||||
<button name="action_approve" string="Approve" type="object" invisible="(attachment_number < 1 and request_owner_id == uid) or request_status in ['approved', 'refused', 'cancel'] or user_status != 'pending'" class="btn-primary" data-hotkey="v"/>
|
||||
<button name="action_approve" string="Approve" type="object" invisible="attachment_number >= 1 or request_owner_id != uid or request_status in ['approved', 'refused', 'cancel'] or user_status != 'pending'" class="btn-primary" data-hotkey="v"/>
|
||||
<button name="action_refuse" string="Refuse" type="object" invisible="request_status in ['approved', 'refused', 'cancel'] or user_status != 'pending'" data-hotkey="z"/>
|
||||
<button name="action_withdraw" string="Withdraw" type="object" invisible="request_status == 'new' or user_status in ['waiting', 'pending', 'cancel'] or not user_status" data-hotkey="y"/>
|
||||
<button name="action_draft" string="Back To Draft" type="object" invisible="request_status != 'cancel' or not user_status and not has_access_to_request" data-hotkey="w"/>
|
||||
<button name="action_cancel" string="Cancel" type="object" invisible="request_status in ['new', 'cancel'] or not user_status and not has_access_to_request or request_owner_id != uid" data-hotkey="x"/>
|
||||
<widget name="attach_document" string="Attach Document" action="message_post" invisible="(attachment_number < 1 and request_owner_id == uid)"/>
|
||||
<button string="Print" invisible="request_status != 'approved' or approval_type !='purchase'" type="action" name="%(action_report_approval_request)d"/>
|
||||
<field name="request_status" widget="statusbar" statusbar_visible="new,pending,approved,refused" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="has_date" invisible="1"/>
|
||||
<field name="has_period" invisible="1"/>
|
||||
<field name="has_quantity" invisible="1"/>
|
||||
<field name="has_amount" invisible="1"/>
|
||||
<field name="has_reference" invisible="1"/>
|
||||
<field name="has_partner" invisible="1"/>
|
||||
<field name="has_payment_method" invisible="1"/>
|
||||
<field name="has_location" invisible="1"/>
|
||||
<field name="has_product" invisible="1"/>
|
||||
<field name="has_promotion" invisible="1"/>
|
||||
<field name="requirer_document" invisible="1"/>
|
||||
<field name="approval_minimum" invisible="1"/>
|
||||
<field name="approval_type" invisible="1"/>
|
||||
<field name="change_request_owner" invisible="1"/>
|
||||
<div class="o_not_full oe_button_box" name="button_box">
|
||||
<button name="action_get_attachment_view"
|
||||
class="oe_stat_button"
|
||||
icon="fa-book"
|
||||
type="object"
|
||||
invisible="attachment_number == 0">
|
||||
<field name="attachment_number" widget="statinfo" string="Documents" options="{'reload_on_button': true}"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_avatar">
|
||||
<field name="category_image" widget="image" options="{'preview_image': 'category_image', 'size': [80, 80]}" class="bg-view" />
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="automated_sequence" invisible="1"/>
|
||||
<field name="name" placeholder="E.g: Expenses Paris business trip"
|
||||
required="not automated_sequence"
|
||||
readonly="automated_sequence"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group name="request_main">
|
||||
<field name="request_owner_id" domain="[('share', '=', False), ('company_ids', 'in', company_id)]" readonly="not change_request_owner or request_status != 'new'" widget="many2one_avatar_user"/>
|
||||
<field name="category_id" readonly="request_status != 'new'" force_save="1"/>
|
||||
<field name="date_confirmed" groups="base.group_no_one" readonly="1"/>
|
||||
<label for="date" string="Date" invisible="has_date == 'no'"/>
|
||||
<div invisible="has_date == 'no'">
|
||||
<div>
|
||||
<field name="date" class="oe_inline" readonly="request_status != 'new'" required="has_date == 'required'"/>
|
||||
</div>
|
||||
</div>
|
||||
<label for="date_start" string="Period" invisible="has_period == 'no'"/>
|
||||
<div invisible="has_period == 'no'">
|
||||
<div>
|
||||
<label for="date_start" class="col-lg-2" string="From:"/>
|
||||
<field name="date_start" class="oe_inline" readonly="request_status != 'new'" required="has_period == 'required'"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="date_end" class="col-lg-2" string="to:"/>
|
||||
<field name="date_end" class="oe_inline" readonly="request_status != 'new'" required="has_period == 'required'"/>
|
||||
</div>
|
||||
</div>
|
||||
<field name="location" invisible="has_location == 'no'" readonly="request_status != 'new'" required="has_location == 'required'" placeholder="e.g. Brussels" />
|
||||
<field name="partner_id" invisible="has_partner == 'no'" readonly="request_status != 'new'" required="has_partner == 'required'"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group name="request_details">
|
||||
<field name="quantity" invisible="has_quantity == 'no' or has_product != 'no'" readonly="request_status != 'new'" required="has_quantity == 'required' and has_product == 'no'"/>
|
||||
<field name="amount" invisible="has_amount == 'no'" readonly="request_status != 'new'" required="has_amount == 'required'"/>
|
||||
<field name="reference" invisible="has_reference == 'no'" required="has_reference == 'required'"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Products" name="products"
|
||||
invisible="has_product == 'no'">
|
||||
<field name="product_line_ids"
|
||||
context="{'list_view_ref': 'approvals.approval_product_line_view_tree', 'kanban_view_ref': 'approvals.approval_product_kanban_mobile_view'}"
|
||||
readonly="request_status != 'new'"/>
|
||||
</page>
|
||||
<page string="Promotions" name="promotions"
|
||||
invisible="has_promotion == 'no'">
|
||||
<field name="promotion_line_ids"
|
||||
context="{'list_view_ref': 'approvals.approval_promotion_line_view_tree', 'kanban_view_ref': 'approvals.approval_promotion_kanban_mobile_view'}"
|
||||
readonly="request_status != 'new'"/>
|
||||
</page>
|
||||
<page string="Description" name="description">
|
||||
<field name="reason" readonly="request_status != 'new'"/>
|
||||
</page>
|
||||
<page string="Approver(s)" name="approvers">
|
||||
<field name="approver_ids" mode="list,kanban" readonly="request_status != 'new'">
|
||||
<list editable="bottom" decoration-success="status=='approved'" decoration-warning="status in ['pending', 'waiting']" decoration-danger="status=='refused'" no_open="1">
|
||||
<field name="existing_request_user_ids" column_invisible="True"/>
|
||||
<field name="sequence" column_invisible="True"/>
|
||||
<field name="can_edit" column_invisible="True"/>
|
||||
<field name="can_edit_user_id" column_invisible="True"/>
|
||||
<field name="user_id" string="Approver" readonly="not can_edit_user_id or status != 'new'" force_save="1" />
|
||||
<field name="required" readonly="not can_edit" force_save="1"/>
|
||||
<field name="status"/>
|
||||
<field name="company_id" column_invisible="True"/>
|
||||
</list>
|
||||
<kanban class="o_kanban_mobile">
|
||||
<templates>
|
||||
<t t-name="card" class="flex-row">
|
||||
<field name="user_id"/>
|
||||
<t t-if="record.status.raw_value">
|
||||
<t t-set="classname" t-value="{'approved': 'text-bg-success', 'pending': 'text-bg-warning', 'refused': 'text-bg-danger'}[record.status.raw_value] || 'text-bg-light'"/>
|
||||
<span t-attf-class="ms-auto rounded-pill {{ classname }}">
|
||||
<field name="status"/>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<group>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="existing_request_user_ids" invisible="1"/>
|
||||
<field name="user_id"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="o_attachment_preview"/>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="approval_request_view_kanban" model="ir.ui.view">
|
||||
<field name="name">approval.request.view.kanban</field>
|
||||
<field name="model">approval.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban create="false" class="o_modules_kanban" default_group_by="request_status" default_order="create_date desc"
|
||||
group_create="false" group_edit="false" group_delete="false" sample="1">
|
||||
<field name="request_status"/>
|
||||
<field name="user_status"/>
|
||||
<templates>
|
||||
<t t-name="menu">
|
||||
<a t-if="widget.editable" role="menuitem" type="open" class="dropdown-item">Edit Request</a>
|
||||
<a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a>
|
||||
<a name="action_approve" type="object" invisible="user_status != 'pending'"
|
||||
role="menuitem" class="dropdown-item">Approve</a>
|
||||
<a name="action_refuse" type="object" invisible="user_status != 'pending'"
|
||||
role="menuitem" class="dropdown-item">Refuse</a>
|
||||
<a name="action_withdraw" type="object" invisible="request_status == 'new' or user_status in ['waiting', 'pending', 'cancel']"
|
||||
role="menuitem" class="dropdown-item">Withdraw</a>
|
||||
</t>
|
||||
<t t-name="card">
|
||||
<div class="ribbon ribbon-top-right" invisible="user_status not in ['pending', 'approved', 'refused', 'cancel']">
|
||||
<field name="user_status" t-if="record.user_status.raw_value == 'pending'" class="text-bg-info"/>
|
||||
<field name="user_status" t-if="record.user_status.raw_value == 'approved'" class="text-bg-success"/>
|
||||
<field name="user_status" t-if="record.user_status.raw_value == 'refused'" class="text-bg-danger"/>
|
||||
<field name="user_status" t-if="record.user_status.raw_value == 'cancel'" class="text-bg-warning"/>
|
||||
</div>
|
||||
<field name="name" class="fw-bolder fs-5"/>
|
||||
<field name="category_id" class="text-muted"/>
|
||||
<div class="d-flex text-muted">
|
||||
<field name="date_start" widget="date" class="me-1"/>-<field name="date_end" widget="date" class="ms-1"/>
|
||||
</div>
|
||||
<footer class="pt-0">
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
<field name="request_owner_id" widget="many2one_avatar_user" class="ms-auto"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--MenuItem-->
|
||||
|
||||
<menuitem
|
||||
id="approvals_menu_root"
|
||||
name="Approvals"
|
||||
web_icon="approvals,static/description/icon.png"
|
||||
action="approval_category_action_new_request"
|
||||
sequence="255"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_category_menu_new"
|
||||
parent="approvals_menu_root"
|
||||
name="Dashboard"
|
||||
action="approval_category_action_new_request"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_approval_menu"
|
||||
parent="approvals_menu_root"
|
||||
name="My Approvals"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_request_menu_my"
|
||||
parent="approvals_approval_menu"
|
||||
name="My Requests"
|
||||
action="approval_request_action"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_menu_manager"
|
||||
parent="approvals_menu_root"
|
||||
name="Manager"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_approval_menu_to_review"
|
||||
parent="approvals_menu_manager"
|
||||
name="Approvals to Review"
|
||||
action="approval_request_action_to_review"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_approval_menu_all"
|
||||
parent="approvals_menu_manager"
|
||||
name="All Approvals"
|
||||
action="approval_request_action_all"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_menu_config"
|
||||
parent="approvals_menu_root"
|
||||
name="Configuration"
|
||||
groups="group_approval_manager"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_category_menu_config"
|
||||
parent="approvals_menu_config"
|
||||
name="Approvals Types"
|
||||
action="approval_category_action"
|
||||
groups="group_approval_manager"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_menu_product"
|
||||
parent="approvals_menu_config"
|
||||
name="Products"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_menu_product_template"
|
||||
parent="approvals_menu_product"
|
||||
name="Products"
|
||||
action="product_template_action"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem
|
||||
id="approvals_menu_product_variant"
|
||||
parent="approvals_menu_product"
|
||||
name="Product Variants"
|
||||
action="product_product_action"
|
||||
groups="product.group_product_variant"
|
||||
sequence="20"/>
|
||||
<!-- <menuitem
|
||||
id="approvals_menu_employee"
|
||||
parent="approvals_menu_config"
|
||||
name="Employees"
|
||||
sequence="40"/>
|
||||
<menuitem
|
||||
id="approvals_menu_promotion_template"
|
||||
parent="approvals_menu_employee"
|
||||
name="promotion"
|
||||
action="promotion_action"
|
||||
sequence="10"/> -->
|
||||
</odoo>
|