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