business travel expenses

This commit is contained in:
pranaysaidurga 2026-06-12 15:00:28 +05:30
parent 064bd90c58
commit 12ec001b38
32 changed files with 1402 additions and 0 deletions

View File

@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@ -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,
}

View File

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

View File

@ -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>-->
<!-- &lt;!&ndash; Attach Travel - Finance group &ndash;&gt;-->
<!-- <field name="groups_id" eval="[(4, ref('business_travel_expense_management.group_travel_finance'))]"/>-->
<!-- </record>-->

View File

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

View File

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

View File

@ -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'))

View File

@ -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)

View File

@ -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)

View File

@ -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}."
)

View File

@ -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"
)

View File

@ -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)

View File

@ -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)

View File

@ -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
)

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_travel_trip travel.trip model_travel_trip base.group_user 1 1 1 1
3 access_travel_expense travel.expense model_travel_expense base.group_user 1 1 1 1
4 access_trip_reject_wizard trip.reject.wizard model_trip_reject_wizard base.group_user 1 1 1 1
5 access_travel_activity_employee travel.activity employee model_travel_activity base.group_user 1 1 1 1
6 access_travel_activity_user travel.activity user model_travel_activity base.group_user 1 1 1 1
7 access_travel_city_category travel.city.category model_travel_city_category base.group_user 1 1 1 1
8 access_travel_group_user travel.group user model_travel_group base.group_user 1 1 1 1
9 access_travel_stay_policy travel.stay.policy model_travel_stay_policy base.group_user 1 1 1 1
10 access_travel_daily_allowance_user access_travel_daily_allowance_user model_travel_daily_allowance base.group_user 1 1 1 1
11 access_travel_mode_policy_user access_travel_mode_policy_user model_travel_mode_policy base.group_user 1 1 1 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'}">-->
<!-- &lt;!&ndash; Only LIST here &ndash;&gt;-->
<!-- <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>
<!-- &lt;!&ndash; Menu &ndash;&gt;-->
<!-- <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>

View File

@ -0,0 +1 @@
from . import trip_reject_wizard

View File

@ -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
})

View File

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