business travel expenses
This commit is contained in:
parent
064bd90c58
commit
12ec001b38
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
'name': 'Business Travel & Expense Management',
|
||||||
|
'version': '1.0',
|
||||||
|
'summary': 'Enterprise Business Travel & Expense Management',
|
||||||
|
'description': """
|
||||||
|
Business Travel (Trips) & Expense Management Module.
|
||||||
|
- Pre-approved Trips
|
||||||
|
- Trip lifecycle management
|
||||||
|
- Expense tracking per Trip
|
||||||
|
- Manager & Finance approvals
|
||||||
|
- Reimbursement workflow
|
||||||
|
""",
|
||||||
|
'category': 'Human Resources',
|
||||||
|
'author': 'Karuna',
|
||||||
|
'depends': ['base', 'hr'],
|
||||||
|
'data': [
|
||||||
|
'security/travel_groups.xml',
|
||||||
|
'security/travel_trip_rules.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
# 'data/users.xml',
|
||||||
|
'data/trip_sequence.xml',
|
||||||
|
'wizard/trip_reject_wizard_view.xml',
|
||||||
|
'views/hr_job_view.xml', # 👈 hr extension BEFORE menus
|
||||||
|
'views/travel_trip_views.xml',
|
||||||
|
'views/travel_city_category_views.xml',
|
||||||
|
'views/travel_group_view.xml',
|
||||||
|
'views/travel_stay_policy_view.xml', # 👈 ADD HERE
|
||||||
|
'views/travel_daily_allowance_view.xml',
|
||||||
|
'views/travel_mode_policy_view.xml',
|
||||||
|
'views/travel_expense_views.xml',
|
||||||
|
'views/travel_activity_views.xml',
|
||||||
|
'views/travel_menu.xml',
|
||||||
|
|
||||||
|
],
|
||||||
|
'images': ['static/description/banner.png'],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="seq_travel_trip" model="ir.sequence">
|
||||||
|
<field name="name">Travel Trip</field>
|
||||||
|
<field name="code">travel.trip</field>
|
||||||
|
<field name="prefix">TRIP/%(year)s/</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!--<odoo>-->
|
||||||
|
<!-- <record id="user_travel_finance" model="res.users">-->
|
||||||
|
<!-- <field name="name">Finance User</field>-->
|
||||||
|
<!-- <field name="login">finance@test.com</field>-->
|
||||||
|
<!-- <field name="email">finance@test.com</field>-->
|
||||||
|
|
||||||
|
<!-- <!– Attach Travel - Finance group –>-->
|
||||||
|
<!-- <field name="groups_id" eval="[(4, ref('business_travel_expense_management.group_travel_finance'))]"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from . import travel_trip
|
||||||
|
from . import travel_expense
|
||||||
|
from . import travel_activity
|
||||||
|
from . import travel_city_category
|
||||||
|
from . import travel_group
|
||||||
|
from . import hr_job
|
||||||
|
from . import travel_stay_policy
|
||||||
|
from . import travel_daily_allowance
|
||||||
|
from . import travel_mode_policy
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class HrJob(models.Model):
|
||||||
|
_inherit = 'hr.job'
|
||||||
|
|
||||||
|
designation_level = fields.Selection([
|
||||||
|
('a', 'Level A'),
|
||||||
|
('b', 'Level B'),
|
||||||
|
('c', 'Level C'),
|
||||||
|
], string="Designation Level")
|
||||||
|
|
||||||
|
travel_group_id = fields.Many2one(
|
||||||
|
'travel.group',
|
||||||
|
compute='_compute_travel_group',
|
||||||
|
store=True,
|
||||||
|
readonly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('designation_level')
|
||||||
|
def _compute_travel_group(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.designation_level:
|
||||||
|
group = self.env['travel.group'].search([
|
||||||
|
('level_code', '=', rec.designation_level)
|
||||||
|
], limit=1)
|
||||||
|
rec.travel_group_id = group.id
|
||||||
|
else:
|
||||||
|
rec.travel_group_id = False
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class TravelActivity(models.Model):
|
||||||
|
_name = 'travel.activity'
|
||||||
|
_description = 'Travel Activity'
|
||||||
|
_order = 'sequence, id'
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# BASIC
|
||||||
|
# --------------------
|
||||||
|
sequence = fields.Integer(default=10)
|
||||||
|
name = fields.Char(string="Activity Title", required=True)
|
||||||
|
|
||||||
|
trip_id = fields.Many2one(
|
||||||
|
'travel.trip',
|
||||||
|
required=True,
|
||||||
|
ondelete='cascade'
|
||||||
|
)
|
||||||
|
|
||||||
|
activity_type = fields.Selection([
|
||||||
|
('travel', 'Travel'),
|
||||||
|
('stay', 'Stay'),
|
||||||
|
('meeting', 'Meeting'),
|
||||||
|
('local', 'Local Travel'),
|
||||||
|
], required=True)
|
||||||
|
|
||||||
|
start_datetime = fields.Datetime("Start Time")
|
||||||
|
end_datetime = fields.Datetime("End Time")
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# COMPUTED GROUP (VERY IMPORTANT)
|
||||||
|
# --------------------
|
||||||
|
travel_group_id = fields.Many2one(
|
||||||
|
'travel.group',
|
||||||
|
string="Travel Group",
|
||||||
|
compute="_compute_travel_group",
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('trip_id')
|
||||||
|
def _compute_travel_group(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.travel_group_id = rec.trip_id.travel_group_id
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# TRAVEL MODE (FILTERED BY GROUP)
|
||||||
|
# --------------------
|
||||||
|
travel_mode_policy_id = fields.Many2one(
|
||||||
|
'travel.mode.policy',
|
||||||
|
string="Travel Mode",
|
||||||
|
domain="""
|
||||||
|
[
|
||||||
|
('travel_group_id', '=', travel_group_id),
|
||||||
|
('mode_type', '=', activity_type),
|
||||||
|
('active', '=', True)
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
from_location = fields.Char()
|
||||||
|
to_location = fields.Char()
|
||||||
|
travel_details = fields.Char()
|
||||||
|
|
||||||
|
# --------------------
|
||||||
|
# OTHER FIELDS
|
||||||
|
# --------------------
|
||||||
|
stay_type = fields.Selection([
|
||||||
|
('hotel', 'Hotel'),
|
||||||
|
('guest', 'Guest House'),
|
||||||
|
])
|
||||||
|
|
||||||
|
hotel_name = fields.Char()
|
||||||
|
city = fields.Char()
|
||||||
|
# city_category_id = fields.Many2one(
|
||||||
|
# 'travel.city.category',
|
||||||
|
# string="City Category"
|
||||||
|
# )
|
||||||
|
checkin = fields.Datetime()
|
||||||
|
checkout = fields.Datetime()
|
||||||
|
|
||||||
|
meeting_title = fields.Char()
|
||||||
|
meeting_location = fields.Char()
|
||||||
|
notes = fields.Text()
|
||||||
|
|
||||||
|
local_travel_mode = fields.Selection([
|
||||||
|
('cab', 'Cab'),
|
||||||
|
('own', 'Own Vehicle'),
|
||||||
|
], string="Local Travel Mode")
|
||||||
|
|
||||||
|
attachment_ids = fields.Many2many(
|
||||||
|
'ir.attachment',
|
||||||
|
'travel_activity_attachment_rel',
|
||||||
|
'activity_id',
|
||||||
|
'attachment_id',
|
||||||
|
string="Documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
expense_ids = fields.One2many(
|
||||||
|
'travel.expense',
|
||||||
|
'activity_id',
|
||||||
|
string="Expenses"
|
||||||
|
)
|
||||||
|
|
||||||
|
total_amount = fields.Float(
|
||||||
|
string='Activity Total',
|
||||||
|
compute='_compute_total_amount',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('expense_ids.amount')
|
||||||
|
def _compute_total_amount(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.total_amount = sum(rec.expense_ids.mapped('amount'))
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class TravelCityCategory(models.Model):
|
||||||
|
_name = 'travel.city.category'
|
||||||
|
_description = 'Travel City Category'
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="City Category",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
code = fields.Char(
|
||||||
|
string="Code",
|
||||||
|
help="Short code like AP_TG_HYD, AP_TG_OTHER, ROI"
|
||||||
|
)
|
||||||
|
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class TravelDailyAllowance(models.Model):
|
||||||
|
_name = 'travel.daily.allowance'
|
||||||
|
_description = 'Daily Allowance Policy'
|
||||||
|
_rec_name = 'travel_group_id'
|
||||||
|
|
||||||
|
travel_group_id = fields.Many2one(
|
||||||
|
'travel.group',
|
||||||
|
string="Travel Group",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
city_category_id = fields.Many2one(
|
||||||
|
'travel.city.category',
|
||||||
|
string="City Category",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = fields.Float(string="Allowance Amount")
|
||||||
|
|
||||||
|
actuals_allowed = fields.Boolean(string="Actuals Allowed")
|
||||||
|
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TravelExpense(models.Model):
|
||||||
|
_name = 'travel.expense'
|
||||||
|
_description = 'Travel Expense'
|
||||||
|
_order = 'expense_date desc, id desc'
|
||||||
|
|
||||||
|
name = fields.Char(string="Expense Description", required=True)
|
||||||
|
expense_date = fields.Date(default=fields.Date.today)
|
||||||
|
amount = fields.Monetary(required=True)
|
||||||
|
|
||||||
|
activity_id = fields.Many2one(
|
||||||
|
'travel.activity',
|
||||||
|
string="Activity",
|
||||||
|
required=True,
|
||||||
|
ondelete='cascade'
|
||||||
|
)
|
||||||
|
receipt = fields.Binary()
|
||||||
|
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
'res.currency',
|
||||||
|
default=lambda self: self.env.company.currency_id
|
||||||
|
)
|
||||||
|
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('submitted', 'Submitted'),
|
||||||
|
('approved', 'Approved'),
|
||||||
|
('rejected', 'Rejected'),
|
||||||
|
], default='draft')
|
||||||
|
|
||||||
|
# ---------------- Actions ----------------
|
||||||
|
|
||||||
|
@api.depends('expense_ids.amount')
|
||||||
|
def _compute_total_amount(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.total_amount = sum(rec.expense_ids.mapped('amount'))
|
||||||
|
|
||||||
|
@api.onchange('expense_ids')
|
||||||
|
def _onchange_expense_ids(self):
|
||||||
|
self.total_amount = sum(self.expense_ids.mapped('amount'))
|
||||||
|
|
||||||
|
def action_submit(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state != 'draft':
|
||||||
|
raise UserError("Only Draft expenses can be submitted.")
|
||||||
|
rec.state = 'submitted'
|
||||||
|
rec.message_post(body="🟡 Expense submitted.")
|
||||||
|
|
||||||
|
def action_approve(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state != 'submitted':
|
||||||
|
raise UserError("Only Submitted expenses can be approved.")
|
||||||
|
|
||||||
|
manager_user = rec.activity_id.trip_id.manager_id.sudo().user_id
|
||||||
|
if manager_user != self.env.user:
|
||||||
|
raise UserError("Only the reporting manager can approve.")
|
||||||
|
|
||||||
|
rec.state = 'approved'
|
||||||
|
rec.message_post(body="🟢 Expense approved.")
|
||||||
|
|
||||||
|
def action_mark_reimbursed(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state != 'approved':
|
||||||
|
raise UserError("Only Approved expenses can be reimbursed.")
|
||||||
|
|
||||||
|
if not self.env.user.has_group(
|
||||||
|
'business_travel_expense_management.group_travel_finance'
|
||||||
|
):
|
||||||
|
raise UserError("Only Finance can reimburse.")
|
||||||
|
|
||||||
|
rec.state = 'reimbursed'
|
||||||
|
rec.message_post(body="💰 Expense reimbursed.")
|
||||||
|
|
||||||
|
@api.constrains('amount', 'activity_id')
|
||||||
|
def _check_stay_policy(self):
|
||||||
|
for record in self:
|
||||||
|
|
||||||
|
activity = record.activity_id
|
||||||
|
|
||||||
|
if not activity or activity.activity_type != 'stay':
|
||||||
|
continue
|
||||||
|
|
||||||
|
trip = activity.trip_id
|
||||||
|
group = trip.travel_group_id
|
||||||
|
city_category = trip.city_category_id # 👈 NOW FROM TRIP
|
||||||
|
|
||||||
|
if not group:
|
||||||
|
raise ValidationError("Trip must have a Travel Group.")
|
||||||
|
|
||||||
|
if not city_category:
|
||||||
|
raise ValidationError("Trip must have a City Category selected.")
|
||||||
|
|
||||||
|
policy = self.env['travel.stay.policy'].search([
|
||||||
|
('travel_group_id', '=', group.id),
|
||||||
|
('city_category_id', '=', city_category.id),
|
||||||
|
('active', '=', True)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not policy:
|
||||||
|
raise ValidationError(
|
||||||
|
f"No Stay Policy configured for Group '{group.name}' and City '{city_category.name}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
if record.amount > policy.max_amount:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Stay expense exceeds allowed limit of {policy.max_amount}."
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class TravelGroup(models.Model):
|
||||||
|
_name = 'travel.group'
|
||||||
|
_description = 'Travel Group'
|
||||||
|
|
||||||
|
name = fields.Char(string='Travel Group', required=True)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
||||||
|
job_ids = fields.One2many(
|
||||||
|
'hr.job',
|
||||||
|
'travel_group_id',
|
||||||
|
string="Designations"
|
||||||
|
)
|
||||||
|
|
||||||
|
level_code = fields.Selection([
|
||||||
|
('a', 'Level A'),
|
||||||
|
('b', 'Level B'),
|
||||||
|
('c', 'Level C'),
|
||||||
|
], required=True)
|
||||||
|
|
||||||
|
allowed_travel_mode_ids = fields.Many2many(
|
||||||
|
'travel.mode',
|
||||||
|
string="Allowed Travel Modes"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class TravelModePolicy(models.Model):
|
||||||
|
_name = 'travel.mode.policy'
|
||||||
|
_description = 'Travel Mode Policy'
|
||||||
|
_rec_name = 'travel_mode' # This makes dropdown show Flight/2AC etc
|
||||||
|
|
||||||
|
travel_group_id = fields.Many2one(
|
||||||
|
'travel.group',
|
||||||
|
string="Travel Group",
|
||||||
|
required=True,
|
||||||
|
ondelete='cascade'
|
||||||
|
)
|
||||||
|
|
||||||
|
mode_type = fields.Selection([
|
||||||
|
('travel', 'Travel'),
|
||||||
|
('local', 'Local Travel'),
|
||||||
|
], string="Mode Type", required=True)
|
||||||
|
|
||||||
|
travel_mode = fields.Selection([
|
||||||
|
('flight', 'Flight'),
|
||||||
|
('2ac', 'II AC'),
|
||||||
|
('3ac', 'III AC'),
|
||||||
|
('1st_class', '1st Class'),
|
||||||
|
('car', 'Car'),
|
||||||
|
('taxi', 'Taxi'),
|
||||||
|
('auto', 'Auto'),
|
||||||
|
], string="Travel Mode", required=True)
|
||||||
|
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class TravelStayPolicy(models.Model):
|
||||||
|
_name = 'travel.stay.policy'
|
||||||
|
_description = 'Travel Stay Policy'
|
||||||
|
|
||||||
|
travel_group_id = fields.Many2one(
|
||||||
|
'travel.group',
|
||||||
|
string='Travel Group',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
city_category_id = fields.Many2one(
|
||||||
|
'travel.city.category',
|
||||||
|
string='City Category',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
min_amount = fields.Float(string='Min Amount')
|
||||||
|
max_amount = fields.Float(string='Max Amount')
|
||||||
|
|
||||||
|
is_actuals = fields.Boolean(
|
||||||
|
string='Actuals Allowed',
|
||||||
|
help='If checked, actual hotel cost is allowed'
|
||||||
|
)
|
||||||
|
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class TravelTrip(models.Model):
|
||||||
|
_name = 'travel.trip'
|
||||||
|
_description = 'Business Travel Trip'
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_order = 'id desc'
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string='Trip Reference',
|
||||||
|
required=True,
|
||||||
|
copy=False,
|
||||||
|
readonly=True,
|
||||||
|
default='New',
|
||||||
|
tracking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string='Employee',
|
||||||
|
required=True,
|
||||||
|
tracking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
department_id = fields.Many2one(
|
||||||
|
'hr.department',
|
||||||
|
compute='_compute_emp_details',
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
tracking=True,
|
||||||
|
compute_sudo=True, # IMPORTANT
|
||||||
|
)
|
||||||
|
|
||||||
|
manager_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
compute='_compute_emp_details',
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
tracking=True,
|
||||||
|
compute_sudo=True, # IMPORTANT
|
||||||
|
)
|
||||||
|
|
||||||
|
purpose = fields.Text(tracking=True)
|
||||||
|
from_location = fields.Char(tracking=True)
|
||||||
|
to_location = fields.Char(tracking=True)
|
||||||
|
start_date = fields.Date(tracking=True)
|
||||||
|
end_date = fields.Date(tracking=True)
|
||||||
|
estimated_cost = fields.Float(tracking=True)
|
||||||
|
|
||||||
|
reject_reason = fields.Text(string="Reject Reason", tracking=True)
|
||||||
|
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('submitted', 'Submitted'),
|
||||||
|
('approved', 'Approved'),
|
||||||
|
('completed', 'Completed'),
|
||||||
|
('reimbursed', 'Reimbursed'),
|
||||||
|
], default='draft', tracking=True)
|
||||||
|
|
||||||
|
# expense_ids = fields.One2many(
|
||||||
|
# 'travel.expense',
|
||||||
|
# 'trip_id',
|
||||||
|
# string='Expenses'
|
||||||
|
# )
|
||||||
|
|
||||||
|
trave_activity_ids = fields.One2many(
|
||||||
|
'travel.activity', # child model
|
||||||
|
'trip_id', # inverse field in travel.activity
|
||||||
|
string="Activities"
|
||||||
|
)
|
||||||
|
total_expense = fields.Float(
|
||||||
|
string='Activity Total',
|
||||||
|
compute='_compute_total_expense',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
travel_group_id = fields.Many2one(
|
||||||
|
'travel.group',
|
||||||
|
string='Travel Group',
|
||||||
|
related='employee_id.job_id.travel_group_id',
|
||||||
|
store=True,
|
||||||
|
readonly=True
|
||||||
|
)
|
||||||
|
city_category_id = fields.Many2one(
|
||||||
|
'travel.city.category',
|
||||||
|
string="City Category",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('trave_activity_ids.total_amount')
|
||||||
|
def _compute_total_expense(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.total_expense = sum(rec.trave_activity_ids.mapped('total_amount'))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- COMPUTE ----------------
|
||||||
|
|
||||||
|
@api.depends('employee_id')
|
||||||
|
def _compute_emp_details(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.employee_id:
|
||||||
|
emp = rec.employee_id.sudo()
|
||||||
|
rec.department_id = emp.department_id
|
||||||
|
rec.manager_id = emp.parent_id
|
||||||
|
else:
|
||||||
|
rec.department_id = False
|
||||||
|
rec.manager_id = False
|
||||||
|
|
||||||
|
# ---------------- CREATE ----------------
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
if vals.get('name', 'New') == 'New':
|
||||||
|
vals['name'] = self.env['ir.sequence'].next_by_code('travel.trip') or 'New'
|
||||||
|
return super().create(vals)
|
||||||
|
|
||||||
|
# ---------------- ACTIONS ----------------
|
||||||
|
|
||||||
|
def action_submit(self):
|
||||||
|
for rec in self:
|
||||||
|
# Submit all DRAFT expenses inside ALL activities
|
||||||
|
activities = rec.trave_activity_ids
|
||||||
|
|
||||||
|
expenses = activities.mapped('expense_ids').filtered(
|
||||||
|
lambda e: e.state == 'draft'
|
||||||
|
)
|
||||||
|
|
||||||
|
expenses.write({'state': 'submitted'})
|
||||||
|
|
||||||
|
rec.state = 'submitted'
|
||||||
|
|
||||||
|
def action_approve(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state != 'submitted':
|
||||||
|
raise UserError('Only Submitted trips can be approved.')
|
||||||
|
|
||||||
|
# Only reporting manager or admin can approve
|
||||||
|
manager_user = rec.manager_id.sudo().user_id
|
||||||
|
if not self.env.is_admin() and (not manager_user or manager_user != self.env.user):
|
||||||
|
raise UserError("Only the reporting manager can approve this trip.")
|
||||||
|
|
||||||
|
rec.state = 'approved'
|
||||||
|
|
||||||
|
rec.message_post(
|
||||||
|
body=f"Trip <b>{rec.name}</b> has been approved by {self.env.user.name}.",
|
||||||
|
subtype_xmlid="mail.mt_comment"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def action_mark_completed(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.state != 'approved':
|
||||||
|
raise UserError('Only Approved trips can be marked as Completed.')
|
||||||
|
|
||||||
|
rec.state = 'completed'
|
||||||
|
|
||||||
|
rec.message_post(
|
||||||
|
body=f"Trip <b>{rec.name}</b> has been marked as Completed.",
|
||||||
|
subtype_xmlid="mail.mt_comment"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_current_user_manager = fields.Boolean(
|
||||||
|
compute="_compute_is_current_user_manager",
|
||||||
|
store=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_is_current_user_manager(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.is_current_user_manager = (
|
||||||
|
rec.manager_id
|
||||||
|
and rec.manager_id.sudo().user_id
|
||||||
|
and rec.manager_id.sudo().user_id.id == self.env.user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_travel_trip,travel.trip,model_travel_trip,base.group_user,1,1,1,1
|
||||||
|
access_travel_expense,travel.expense,model_travel_expense,base.group_user,1,1,1,1
|
||||||
|
access_trip_reject_wizard,trip.reject.wizard,model_trip_reject_wizard,base.group_user,1,1,1,1
|
||||||
|
access_travel_activity_employee,travel.activity employee,model_travel_activity,base.group_user,1,1,1,1
|
||||||
|
access_travel_activity_user,travel.activity user,model_travel_activity,base.group_user,1,1,1,1
|
||||||
|
access_travel_city_category,travel.city.category,model_travel_city_category,base.group_user,1,1,1,1
|
||||||
|
access_travel_group_user,travel.group user,model_travel_group,base.group_user,1,1,1,1
|
||||||
|
access_travel_stay_policy,travel.stay.policy,model_travel_stay_policy,base.group_user,1,1,1,1
|
||||||
|
access_travel_daily_allowance_user,access_travel_daily_allowance_user,model_travel_daily_allowance,base.group_user,1,1,1,1
|
||||||
|
access_travel_mode_policy_user,access_travel_mode_policy_user,model_travel_mode_policy,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<odoo>
|
||||||
|
<record id="group_travel_employee" model="res.groups">
|
||||||
|
<field name="name">Travel - Employee</field>
|
||||||
|
<field name="category_id" ref="base.module_category_human_resources"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_travel_manager" model="res.groups">
|
||||||
|
<field name="name">Travel - Manager</field>
|
||||||
|
<field name="category_id" ref="base.module_category_human_resources"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_travel_employee'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_travel_finance" model="res.groups">
|
||||||
|
<field name="name">Travel - Finance</field>
|
||||||
|
<field name="category_id" ref="base.module_category_human_resources"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_travel_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- Employees: only their own trips -->
|
||||||
|
<record id="travel_trip_rule_employee_own" model="ir.rule">
|
||||||
|
<field name="name">Travel Trip: Employee Own</field>
|
||||||
|
<field name="model_id" ref="model_travel_trip"/>
|
||||||
|
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('business_travel_expense_management.group_travel_employee'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Managers: trips of their team -->
|
||||||
|
<record id="travel_trip_rule_manager_team" model="ir.rule">
|
||||||
|
<field name="name">Travel Trip: Manager Team</field>
|
||||||
|
<field name="model_id" ref="model_travel_trip"/>
|
||||||
|
<field name="domain_force">[('manager_id.user_id', '=', user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('business_travel_expense_management.group_travel_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- System / Admin: everything -->
|
||||||
|
<record id="travel_trip_rule_admin_all" model="ir.rule">
|
||||||
|
<field name="name">Travel Trip: Admin All</field>
|
||||||
|
<field name="model_id" ref="model_travel_trip"/>
|
||||||
|
<field name="domain_force">[(1,'=',1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
|
|
@ -0,0 +1,16 @@
|
||||||
|
<odoo>
|
||||||
|
<record id="view_hr_job_form_inherit_travel_group" model="ir.ui.view">
|
||||||
|
<field name="name">hr.job.form.inherit.travel.group</field>
|
||||||
|
<field name="model">hr.job</field>
|
||||||
|
<field name="inherit_id" ref="hr.view_hr_job_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<!-- Insert Travel Group below Department -->
|
||||||
|
<xpath expr="//field[@name='department_id']" position="after">
|
||||||
|
<field name="designation_level"/>
|
||||||
|
<field name="travel_group_id" readonly="1"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_travel_activity_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.activity.form</field>
|
||||||
|
<field name="model">travel.activity</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<form string="Activity">
|
||||||
|
|
||||||
|
<sheet>
|
||||||
|
|
||||||
|
<!-- TITLE -->
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Activity title"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ACTIVITY TYPE -->
|
||||||
|
<group>
|
||||||
|
<field name="activity_type"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- TRAVEL DETAILS -->
|
||||||
|
<group string="Travel Details"
|
||||||
|
invisible="activity_type != 'travel'">
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="travel_mode_policy_id"/>
|
||||||
|
<field name="from_location"/>
|
||||||
|
<field name="to_location"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="start_datetime"/>
|
||||||
|
<field name="end_datetime"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- <field name="travel_details" colspan="2"/>-->
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- STAY DETAILS -->
|
||||||
|
<group string="Accommodation Details"
|
||||||
|
invisible="activity_type != 'stay'">
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="stay_type"/>
|
||||||
|
<field name="hotel_name"/>
|
||||||
|
<field name="city"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="checkin"/>
|
||||||
|
<field name="checkout"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- MEETING DETAILS -->
|
||||||
|
<group string="Meeting Details"
|
||||||
|
invisible="activity_type != 'meeting'">
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="meeting_title"/>
|
||||||
|
<field name="meeting_location"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="start_datetime"/>
|
||||||
|
<field name="end_datetime"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- <field name="notes" colspan="2"/>-->
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- LOCAL TRAVEL -->
|
||||||
|
<group string="Local Commute Details"
|
||||||
|
invisible="activity_type != 'local'">
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="local_travel_mode"/>
|
||||||
|
<field name="from_location"/>
|
||||||
|
<field name="to_location"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="start_datetime"/>
|
||||||
|
<field name="end_datetime"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- DOCUMENTS -->
|
||||||
|
<separator string="Documents"/>
|
||||||
|
|
||||||
|
<field name="attachment_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="datas"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- EXPENSES -->
|
||||||
|
<separator string="Expenses"/>
|
||||||
|
|
||||||
|
<field name="expense_ids"
|
||||||
|
context="{'default_activity_id': id}">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="expense_date"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<!-- TOTAL -->
|
||||||
|
<group>
|
||||||
|
<field name="total_amount" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="action_travel_activity" model="ir.actions.act_window">
|
||||||
|
<field name="name">Activity</field>
|
||||||
|
<field name="res_model">travel.activity</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="view_travel_activity_form"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<record id="view_travel_city_category_tree" model="ir.ui.view">
|
||||||
|
<field name="name">travel.city.category.tree</field>
|
||||||
|
<field name="model">travel.city.category</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_travel_city_category_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.city.category.form</field>
|
||||||
|
<field name="model">travel.city.category</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="City Category">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="code"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_travel_city_category" model="ir.actions.act_window">
|
||||||
|
<field name="name">City Categories</field>
|
||||||
|
<field name="res_model">travel.city.category</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<!-- <menuitem id="menu_travel_config_root"-->
|
||||||
|
<!-- name="Travel Configuration"-->
|
||||||
|
<!-- parent="menu_travel_root"-->
|
||||||
|
<!-- sequence="50"/>-->
|
||||||
|
|
||||||
|
<!-- <menuitem id="menu_travel_city_category"-->
|
||||||
|
<!-- name="City Categories"-->
|
||||||
|
<!-- parent="menu_travel_config_root"-->
|
||||||
|
<!-- action="action_travel_city_category"-->
|
||||||
|
<!-- sequence="10"/>-->
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- TREE VIEW -->
|
||||||
|
<record id="view_travel_daily_allowance_tree" model="ir.ui.view">
|
||||||
|
<field name="name">travel.daily.allowance.tree</field>
|
||||||
|
<field name="model">travel.daily.allowance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="travel_group_id"/>
|
||||||
|
<field name="city_category_id"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="actuals_allowed"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- FORM VIEW -->
|
||||||
|
<record id="view_travel_daily_allowance_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.daily.allowance.form</field>
|
||||||
|
<field name="model">travel.daily.allowance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="travel_group_id"/>
|
||||||
|
<field name="city_category_id"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="actuals_allowed"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ACTION -->
|
||||||
|
<record id="action_travel_daily_allowance" model="ir.actions.act_window">
|
||||||
|
<field name="name">Daily Allowance Policies</field>
|
||||||
|
<field name="res_model">travel.daily.allowance</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_travel_expense_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.expense.form</field>
|
||||||
|
<field name="model">travel.expense</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Expense">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<button name="action_submit"
|
||||||
|
string="Submit"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="state != 'draft'"/>
|
||||||
|
|
||||||
|
<button name="action_approve"
|
||||||
|
string="Approve"
|
||||||
|
type="object"
|
||||||
|
class="btn-success"
|
||||||
|
invisible="state != 'submitted'"/>
|
||||||
|
|
||||||
|
<button name="action_mark_reimbursed"
|
||||||
|
string="Reimburse"
|
||||||
|
type="object"
|
||||||
|
class="btn-success"
|
||||||
|
invisible="state != 'approved'"/>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<sheet>
|
||||||
|
|
||||||
|
<div class="oe_title"
|
||||||
|
style="display:flex; justify-content:space-between;">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Expense Description"/>
|
||||||
|
</h1>
|
||||||
|
<field name="state"
|
||||||
|
widget="statusbar"
|
||||||
|
statusbar_visible="draft,submitted,approved,reimbursed"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="activity_id"/>
|
||||||
|
<field name="expense_date"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="amount"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Documents">
|
||||||
|
<field name="receipt" widget="binary"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</sheet>
|
||||||
|
|
||||||
|
<!-- <chatter>-->
|
||||||
|
<!-- <field name="message_ids"/>-->
|
||||||
|
<!-- </chatter>-->
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- Tree View -->
|
||||||
|
<record id="view_travel_group_tree" model="ir.ui.view">
|
||||||
|
<field name="name">travel.group.tree</field>
|
||||||
|
<field name="model">travel.group</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_travel_group_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.group.form</field>
|
||||||
|
<field name="model">travel.group</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="level_code"/> <!-- ADD THIS -->
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
<group string="Designations">
|
||||||
|
<field name="job_ids" widget="many2many_tags" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_travel_group" model="ir.actions.act_window">
|
||||||
|
<field name="name">Travel Groups</field>
|
||||||
|
<field name="res_model">travel.group</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ROOT MENU (MUST HAVE ACTION) -->
|
||||||
|
<menuitem id="menu_travel_root"
|
||||||
|
name="Business Travel"
|
||||||
|
action="action_travel_trip"
|
||||||
|
sequence="50"
|
||||||
|
web_icon="business_travel_expense_management,static/decription/icon.png"
|
||||||
|
groups="base.group_user"/>
|
||||||
|
|
||||||
|
<!-- TRIPS -->
|
||||||
|
<menuitem id="menu_travel_trip"
|
||||||
|
name="Trips"
|
||||||
|
parent="menu_travel_root"
|
||||||
|
action="action_travel_trip"
|
||||||
|
sequence="10"
|
||||||
|
groups="base.group_user"/>
|
||||||
|
|
||||||
|
<!-- CONFIG ROOT -->
|
||||||
|
<menuitem id="menu_travel_config_root"
|
||||||
|
name="Travel Configuration"
|
||||||
|
parent="menu_travel_root"
|
||||||
|
sequence="50"
|
||||||
|
groups="base.group_user"/>
|
||||||
|
|
||||||
|
<!-- CITY CATEGORY -->
|
||||||
|
<menuitem id="menu_travel_city_category"
|
||||||
|
name="City Categories"
|
||||||
|
parent="menu_travel_config_root"
|
||||||
|
action="action_travel_city_category"
|
||||||
|
sequence="10"
|
||||||
|
groups="base.group_user"/>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<menuitem id="menu_travel_group"
|
||||||
|
name="Travel Groups"
|
||||||
|
parent="menu_travel_config_root"
|
||||||
|
action="action_travel_group"
|
||||||
|
sequence="20"
|
||||||
|
groups="base.group_user"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_travel_stay_policy"
|
||||||
|
name="Stay Policies"
|
||||||
|
parent="menu_travel_config_root"
|
||||||
|
action="action_travel_stay_policy"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_travel_daily_allowance"
|
||||||
|
name="Daily Allowance Policies"
|
||||||
|
parent="menu_travel_config_root"
|
||||||
|
action="action_travel_daily_allowance"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_travel_mode_policy"
|
||||||
|
name="Travel Mode Policies"
|
||||||
|
parent="menu_travel_config_root"
|
||||||
|
action="action_travel_mode_policy"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- TREE -->
|
||||||
|
<record id="view_travel_mode_policy_tree" model="ir.ui.view">
|
||||||
|
<field name="name">travel.mode.policy.tree</field>
|
||||||
|
<field name="model">travel.mode.policy</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="travel_group_id"/>
|
||||||
|
<field name="mode_type"/>
|
||||||
|
<field name="travel_mode"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- FORM -->
|
||||||
|
<record id="view_travel_mode_policy_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.mode.policy.form</field>
|
||||||
|
<field name="model">travel.mode.policy</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="travel_group_id"/>
|
||||||
|
<field name="mode_type"/>
|
||||||
|
<field name="travel_mode"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_travel_mode_policy" model="ir.actions.act_window">
|
||||||
|
<field name="name">Travel Mode Policies</field>
|
||||||
|
<field name="res_model">travel.mode.policy</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<odoo>
|
||||||
|
<record id="view_travel_stay_policy_tree" model="ir.ui.view">
|
||||||
|
<field name="name">travel.stay.policy.tree</field>
|
||||||
|
<field name="model">travel.stay.policy</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="travel_group_id"/>
|
||||||
|
<field name="city_category_id"/>
|
||||||
|
<field name="min_amount"/>
|
||||||
|
<field name="max_amount"/>
|
||||||
|
<field name="is_actuals"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_travel_stay_policy_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.stay.policy.form</field>
|
||||||
|
<field name="model">travel.stay.policy</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="travel_group_id"/>
|
||||||
|
<field name="city_category_id"/>
|
||||||
|
<field name="is_actuals"/>
|
||||||
|
<field name="min_amount"/>
|
||||||
|
<field name="max_amount"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="action_travel_stay_policy" model="ir.actions.act_window">
|
||||||
|
<field name="name">Stay Policies</field>
|
||||||
|
<field name="res_model">travel.stay.policy</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- Trip Form View -->
|
||||||
|
<record id="view_travel_trip_form" model="ir.ui.view">
|
||||||
|
<field name="name">travel.trip.form</field>
|
||||||
|
<field name="model">travel.trip</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Trip">
|
||||||
|
<header>
|
||||||
|
|
||||||
|
<!-- Employee -->
|
||||||
|
<button name="action_submit"
|
||||||
|
string="Submit Trip"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="state != 'draft'"/>
|
||||||
|
|
||||||
|
<!-- Manager only -->
|
||||||
|
<button name="action_approve"
|
||||||
|
string="Approve"
|
||||||
|
type="object"
|
||||||
|
class="btn-success"
|
||||||
|
invisible="state != 'submitted' or not is_current_user_manager"/>
|
||||||
|
|
||||||
|
<button name="%(action_trip_reject_wizard)d"
|
||||||
|
string="Reject"
|
||||||
|
type="action"
|
||||||
|
class="btn-danger"
|
||||||
|
invisible="state != 'submitted' or not is_current_user_manager"/>
|
||||||
|
|
||||||
|
<!-- Employee after approval -->
|
||||||
|
<button name="action_mark_completed"
|
||||||
|
string="Mark Completed"
|
||||||
|
type="object"
|
||||||
|
class="btn-secondary"
|
||||||
|
invisible="state != 'approved'"/>
|
||||||
|
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="draft,submitted,approved,completed,reimbursed,rejected"/>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name" readonly="1"/>
|
||||||
|
<field name="employee_id" readonly="state != 'draft'"/>
|
||||||
|
<xpath expr="//field[@name='employee_id']" position="after">
|
||||||
|
<field name="travel_group_id" readonly="1"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<field name="department_id" readonly="1"/>
|
||||||
|
<field name="manager_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="from_location" readonly="state != 'draft'"/>
|
||||||
|
<field name="to_location" readonly="state != 'draft'"/>
|
||||||
|
<field name="start_date" readonly="state != 'draft'"/>
|
||||||
|
<field name="end_date" readonly="state != 'draft'"/>
|
||||||
|
<field name="estimated_cost" readonly="state != 'draft'"/>
|
||||||
|
<field name="total_expense" string="Actual Cost"/>
|
||||||
|
<field name="city_category_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="purpose" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- Show reject reason only when present -->
|
||||||
|
<group invisible="not reject_reason">
|
||||||
|
<field name="reject_reason" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<!-- <page string="Expenses">-->
|
||||||
|
<!-- <field name="expense_ids"-->
|
||||||
|
<!-- readonly="state != 'draft'"-->
|
||||||
|
<!-- context="{'form_view_ref': 'business_travel_expense_management.view_travel_expense_form'}">-->
|
||||||
|
|
||||||
|
<!-- <!– Only LIST here –>-->
|
||||||
|
<!-- <list>-->
|
||||||
|
<!-- <field name="name"/>-->
|
||||||
|
<!-- <field name="category"/>-->
|
||||||
|
<!-- <field name="transport_detail"/>-->
|
||||||
|
<!-- <field name="distance_km"/>-->
|
||||||
|
<!-- <field name="amount"/>-->
|
||||||
|
<!-- <field name="expense_date"/>-->
|
||||||
|
<!-- <field name="state"/>-->
|
||||||
|
<!-- </list>-->
|
||||||
|
|
||||||
|
<!-- </field>-->
|
||||||
|
<page string="Activities">
|
||||||
|
<field name="trave_activity_ids"
|
||||||
|
context="{'default_trip_id': id}">
|
||||||
|
<list>
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name" string="Activity"/>
|
||||||
|
<field name="activity_type"/>
|
||||||
|
<field name="start_datetime"/>
|
||||||
|
<field name="end_datetime"/>
|
||||||
|
<!-- <field name="currency_id" invisible="1"/>-->
|
||||||
|
|
||||||
|
<field name="total_amount"
|
||||||
|
string="Activity Total"
|
||||||
|
widget="monetary"/>
|
||||||
|
<!-- options="{'currency_field': 'currency_id'}"/>-->
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
|
||||||
|
</notebook>
|
||||||
|
<!-- <group>-->
|
||||||
|
<!-- <field name="total_amount"-->
|
||||||
|
<!-- widget="monetary"-->
|
||||||
|
<!-- options="{'currency_field': 'currency_id'}"-->
|
||||||
|
<!-- readonly="1"/>-->
|
||||||
|
<!-- </group>-->
|
||||||
|
|
||||||
|
|
||||||
|
<chatter>
|
||||||
|
<field name="message_follower_ids"/>
|
||||||
|
<!-- <field name="activity_ids"/>-->
|
||||||
|
<field name="message_ids"/>
|
||||||
|
</chatter>
|
||||||
|
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Tree View -->
|
||||||
|
<record id="view_travel_trip_tree" model="ir.ui.view">
|
||||||
|
<field name="name">travel.trip.tree</field>
|
||||||
|
<field name="model">travel.trip</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="from_location"/>
|
||||||
|
<field name="to_location"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="estimated_cost"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_travel_trip" model="ir.actions.act_window">
|
||||||
|
<field name="name">Trips</field>
|
||||||
|
<field name="res_model">travel.trip</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- <!– Menu –>-->
|
||||||
|
<!-- <menuitem id="menu_travel_root"-->
|
||||||
|
<!-- name="Business Travel"-->
|
||||||
|
<!-- sequence="50"/>-->
|
||||||
|
|
||||||
|
<!-- <menuitem id="menu_travel_trip"-->
|
||||||
|
<!-- name="Trips"-->
|
||||||
|
<!-- parent="menu_travel_root"-->
|
||||||
|
<!-- action="action_travel_trip"-->
|
||||||
|
<!-- sequence="10"/>-->
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import trip_reject_wizard
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class TripRejectWizard(models.TransientModel):
|
||||||
|
_name = 'trip.reject.wizard'
|
||||||
|
_description = 'Reject Trip Wizard'
|
||||||
|
|
||||||
|
reason = fields.Text(string="Reason for Rejection", required=True)
|
||||||
|
|
||||||
|
def action_confirm_reject(self):
|
||||||
|
trip = self.env['travel.trip'].browse(self.env.context.get('active_id'))
|
||||||
|
if not trip:
|
||||||
|
raise UserError("No Trip found.")
|
||||||
|
|
||||||
|
trip.write({
|
||||||
|
'state': 'draft',
|
||||||
|
'reject_reason': self.reason
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_trip_reject_wizard" model="ir.ui.view">
|
||||||
|
<field name="name">trip.reject.wizard.form</field>
|
||||||
|
<field name="model">trip.reject.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Reject Trip">
|
||||||
|
<group>
|
||||||
|
<field name="reason"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button string="Confirm Reject"
|
||||||
|
type="object"
|
||||||
|
name="action_confirm_reject"
|
||||||
|
class="btn-danger"/>
|
||||||
|
<button string="Cancel" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_trip_reject_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Reject Trip</field>
|
||||||
|
<field name="res_model">trip.reject.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue