diff --git a/addons_extensions/business_travel_expense_management/__init__.py b/addons_extensions/business_travel_expense_management/__init__.py
new file mode 100644
index 000000000..3f3abeb49
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/addons_extensions/business_travel_expense_management/__manifest__.py b/addons_extensions/business_travel_expense_management/__manifest__.py
new file mode 100644
index 000000000..1f612318e
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/__manifest__.py
@@ -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,
+}
diff --git a/addons_extensions/business_travel_expense_management/data/trip_sequence.xml b/addons_extensions/business_travel_expense_management/data/trip_sequence.xml
new file mode 100644
index 000000000..07447e1c7
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/data/trip_sequence.xml
@@ -0,0 +1,9 @@
+
+
+
+ Travel Trip
+ travel.trip
+ TRIP/%(year)s/
+ 4
+
+
diff --git a/addons_extensions/business_travel_expense_management/data/users.xml b/addons_extensions/business_travel_expense_management/data/users.xml
new file mode 100644
index 000000000..8e0e658f2
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/data/users.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/models/__init__.py b/addons_extensions/business_travel_expense_management/models/__init__.py
new file mode 100644
index 000000000..246afe4cc
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/__init__.py
@@ -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
diff --git a/addons_extensions/business_travel_expense_management/models/hr_job.py b/addons_extensions/business_travel_expense_management/models/hr_job.py
new file mode 100644
index 000000000..5b78fa389
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/hr_job.py
@@ -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
diff --git a/addons_extensions/business_travel_expense_management/models/travel_activity.py b/addons_extensions/business_travel_expense_management/models/travel_activity.py
new file mode 100644
index 000000000..5eb9b1504
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_activity.py
@@ -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'))
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/models/travel_city_category.py b/addons_extensions/business_travel_expense_management/models/travel_city_category.py
new file mode 100644
index 000000000..88b52b9b7
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_city_category.py
@@ -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)
diff --git a/addons_extensions/business_travel_expense_management/models/travel_daily_allowance.py b/addons_extensions/business_travel_expense_management/models/travel_daily_allowance.py
new file mode 100644
index 000000000..913092244
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_daily_allowance.py
@@ -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)
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/models/travel_expense.py b/addons_extensions/business_travel_expense_management/models/travel_expense.py
new file mode 100644
index 000000000..bf48811e5
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_expense.py
@@ -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}."
+ )
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/models/travel_group.py b/addons_extensions/business_travel_expense_management/models/travel_group.py
new file mode 100644
index 000000000..37835298e
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_group.py
@@ -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"
+ )
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/models/travel_mode_policy.py b/addons_extensions/business_travel_expense_management/models/travel_mode_policy.py
new file mode 100644
index 000000000..231e9a37b
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_mode_policy.py
@@ -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)
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/models/travel_stay_policy.py b/addons_extensions/business_travel_expense_management/models/travel_stay_policy.py
new file mode 100644
index 000000000..f7666e71f
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_stay_policy.py
@@ -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)
diff --git a/addons_extensions/business_travel_expense_management/models/travel_trip.py b/addons_extensions/business_travel_expense_management/models/travel_trip.py
new file mode 100644
index 000000000..3a1fcdf47
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/models/travel_trip.py
@@ -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 {rec.name} 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 {rec.name} 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
+ )
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/security/ir.model.access.csv b/addons_extensions/business_travel_expense_management/security/ir.model.access.csv
new file mode 100644
index 000000000..2a6a06ea3
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/security/ir.model.access.csv
@@ -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
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/security/travel_groups.xml b/addons_extensions/business_travel_expense_management/security/travel_groups.xml
new file mode 100644
index 000000000..6ea3e36fc
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/security/travel_groups.xml
@@ -0,0 +1,18 @@
+
+
+ Travel - Employee
+
+
+
+
+ Travel - Manager
+
+
+
+
+
+ Travel - Finance
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/security/travel_trip_rules.xml b/addons_extensions/business_travel_expense_management/security/travel_trip_rules.xml
new file mode 100644
index 000000000..ab38fc66d
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/security/travel_trip_rules.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ Travel Trip: Employee Own
+
+ [('employee_id.user_id', '=', user.id)]
+
+
+
+
+
+ Travel Trip: Manager Team
+
+ [('manager_id.user_id', '=', user.id)]
+
+
+
+
+
+ Travel Trip: Admin All
+
+ [(1,'=',1)]
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/static/decription/banner.png b/addons_extensions/business_travel_expense_management/static/decription/banner.png
new file mode 100644
index 000000000..234fa7fa3
Binary files /dev/null and b/addons_extensions/business_travel_expense_management/static/decription/banner.png differ
diff --git a/addons_extensions/business_travel_expense_management/static/decription/icon.png b/addons_extensions/business_travel_expense_management/static/decription/icon.png
new file mode 100644
index 000000000..234fa7fa3
Binary files /dev/null and b/addons_extensions/business_travel_expense_management/static/decription/icon.png differ
diff --git a/addons_extensions/business_travel_expense_management/views/hr_job_view.xml b/addons_extensions/business_travel_expense_management/views/hr_job_view.xml
new file mode 100644
index 000000000..2a9471f5f
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/hr_job_view.xml
@@ -0,0 +1,16 @@
+
+
+ hr.job.form.inherit.travel.group
+ hr.job
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/views/travel_activity_views.xml b/addons_extensions/business_travel_expense_management/views/travel_activity_views.xml
new file mode 100644
index 000000000..67672bb3f
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_activity_views.xml
@@ -0,0 +1,134 @@
+
+
+
+
+ travel.activity.form
+ travel.activity
+
+
+
+
+
+
+
+ Activity
+ travel.activity
+ form
+
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/views/travel_city_category_views.xml b/addons_extensions/business_travel_expense_management/views/travel_city_category_views.xml
new file mode 100644
index 000000000..aae34a044
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_city_category_views.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+ travel.city.category.tree
+ travel.city.category
+
+
+
+
+
+
+
+
+
+
+
+ travel.city.category.form
+ travel.city.category
+
+
+
+
+
+
+
+ City Categories
+ travel.city.category
+ list,form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/views/travel_daily_allowance_view.xml b/addons_extensions/business_travel_expense_management/views/travel_daily_allowance_view.xml
new file mode 100644
index 000000000..e557ea59a
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_daily_allowance_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ travel.daily.allowance.tree
+ travel.daily.allowance
+
+
+
+
+
+
+
+
+
+
+
+
+
+ travel.daily.allowance.form
+ travel.daily.allowance
+
+
+
+
+
+
+
+ Daily Allowance Policies
+ travel.daily.allowance
+ list,form
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/views/travel_expense_views.xml b/addons_extensions/business_travel_expense_management/views/travel_expense_views.xml
new file mode 100644
index 000000000..1fb6fa0b4
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_expense_views.xml
@@ -0,0 +1,66 @@
+
+
+
+
+ travel.expense.form
+ travel.expense
+
+
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/views/travel_group_view.xml b/addons_extensions/business_travel_expense_management/views/travel_group_view.xml
new file mode 100644
index 000000000..1ae366959
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_group_view.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ travel.group.tree
+ travel.group
+
+
+
+
+
+
+
+
+
+
+ travel.group.form
+ travel.group
+
+
+
+
+
+
+
+ Travel Groups
+ travel.group
+ list,form
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/views/travel_menu.xml b/addons_extensions/business_travel_expense_management/views/travel_menu.xml
new file mode 100644
index 000000000..cd4758eda
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_menu.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/views/travel_mode_policy_view.xml b/addons_extensions/business_travel_expense_management/views/travel_mode_policy_view.xml
new file mode 100644
index 000000000..f4b822c3f
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_mode_policy_view.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ travel.mode.policy.tree
+ travel.mode.policy
+
+
+
+
+
+
+
+
+
+
+
+
+ travel.mode.policy.form
+ travel.mode.policy
+
+
+
+
+
+
+ Travel Mode Policies
+ travel.mode.policy
+ list,form
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/business_travel_expense_management/views/travel_stay_policy_view.xml b/addons_extensions/business_travel_expense_management/views/travel_stay_policy_view.xml
new file mode 100644
index 000000000..01f83ecd4
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_stay_policy_view.xml
@@ -0,0 +1,42 @@
+
+
+ travel.stay.policy.tree
+ travel.stay.policy
+
+
+
+
+
+
+
+
+
+
+
+
+
+ travel.stay.policy.form
+ travel.stay.policy
+
+
+
+
+
+
+
+ Stay Policies
+ travel.stay.policy
+ list,form
+
+
diff --git a/addons_extensions/business_travel_expense_management/views/travel_trip_views.xml b/addons_extensions/business_travel_expense_management/views/travel_trip_views.xml
new file mode 100644
index 000000000..ffd780953
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/views/travel_trip_views.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+ travel.trip.form
+ travel.trip
+
+
+
+
+
+
+
+ travel.trip.tree
+ travel.trip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Trips
+ travel.trip
+ list,form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/business_travel_expense_management/wizard/__init__.py b/addons_extensions/business_travel_expense_management/wizard/__init__.py
new file mode 100644
index 000000000..d05c3e001
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/wizard/__init__.py
@@ -0,0 +1 @@
+from . import trip_reject_wizard
diff --git a/addons_extensions/business_travel_expense_management/wizard/trip_reject_wizard.py b/addons_extensions/business_travel_expense_management/wizard/trip_reject_wizard.py
new file mode 100644
index 000000000..11338a0a4
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/wizard/trip_reject_wizard.py
@@ -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
+ })
diff --git a/addons_extensions/business_travel_expense_management/wizard/trip_reject_wizard_view.xml b/addons_extensions/business_travel_expense_management/wizard/trip_reject_wizard_view.xml
new file mode 100644
index 000000000..cacb308d7
--- /dev/null
+++ b/addons_extensions/business_travel_expense_management/wizard/trip_reject_wizard_view.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ trip.reject.wizard.form
+ trip.reject.wizard
+
+
+
+
+
+
+ Reject Trip
+ trip.reject.wizard
+ form
+ new
+
+
+
+