diff --git a/budget/__init__.py b/budget/__init__.py new file mode 100644 index 00000000000..c536983e2b2 --- /dev/null +++ b/budget/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard \ No newline at end of file diff --git a/budget/__manifest__.py b/budget/__manifest__.py new file mode 100644 index 00000000000..cacfc76c51d --- /dev/null +++ b/budget/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'Budget', + 'version': '1.0', + 'author': 'DhruvKumar Nagar', + 'summary': 'Manage budgets effectively', + 'description': """ + A custom module for creating and managing budgets in Odoo. + """, + 'depends': ['base', 'account'], + 'data': [ + 'security/ir.model.access.csv', + 'views/budget_line_view.xml', + 'views/budget_menu_view.xml', + 'wizard/budget_wizard_view.xml', + 'views/budget_views.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + "license": "LGPL-3" +} diff --git a/budget/models/__init__.py b/budget/models/__init__.py new file mode 100644 index 00000000000..9f682b84f24 --- /dev/null +++ b/budget/models/__init__.py @@ -0,0 +1,3 @@ +from . import budget +from . import budget_line +from . import account_analytic_line \ No newline at end of file diff --git a/budget/models/account_analytic_line.py b/budget/models/account_analytic_line.py new file mode 100644 index 00000000000..247680dad75 --- /dev/null +++ b/budget/models/account_analytic_line.py @@ -0,0 +1,38 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + budget_line_id = fields.Many2one(comodel_name="budget.management.budget.lines") + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + budget_line = self.env["budget.management.budget.lines"].browse( + vals.get("budget_line_id") + ) + budget = budget_line.budget_id + if budget.on_over_budget == "restriction": + if sum(budget_line.analytic_line_ids.mapped("amount"))+vals.get("amount") > budget_line.budget_amount: + raise ValidationError( + "You cannot create a budget line because it exceeds the allowed budget!" + ) + return super(AccountAnalyticLine, self).create(vals_list) + + def write(self, vals): + if "amount" in vals: + for record in self: + old_amount = record.amount + new_amount = vals.get("amount") + print(old_amount,new_amount) + total_amount = sum(record.budget_line_id.analytic_line_ids.mapped("amount")) + new_amount - old_amount + + budget_line = record.budget_line_id + budget = budget_line.budget_id + if budget.on_over_budget == "restriction" and total_amount > budget_line.budget_amount: + raise ValidationError( + "You cannot update this budget line because it exceeds the allowed budget!" + ) + return super(AccountAnalyticLine, self).write(vals) diff --git a/budget/models/budget.py b/budget/models/budget.py new file mode 100644 index 00000000000..d7873509acc --- /dev/null +++ b/budget/models/budget.py @@ -0,0 +1,136 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError, UserError + +class Budget(models.Model): + _name = "budget.budget" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Budget Management" + + name = fields.Char(compute="_compute_budget_name", store=True, readonly=True) + active = fields.Boolean(default=True) + is_favorite = fields.Boolean(default=False) + color = fields.Integer(string="Color Index") + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("confirmed", "Confirmed"), + ("revised", "Revised"), + ("done", "Done"), + ], + required=True, + default="draft", + tracking=True, + ) + on_over_budget = fields.Selection( + selection=[("warning", "Warning"), ("restriction", "Restriction")], + tracking=True, + ) + responsible = fields.Many2one( + comodel_name="res.users", + string="Responsible", + tracking=True, + ) + revision_id = fields.Many2one( + comodel_name="budget.budget", # This should point to the same budget model + tracking=True, + readonly=True, + ) + date_start = fields.Date(string="Start Date", required=True) + date_end = fields.Date(string="Expiration Date", required=True, index=True) + company_id = fields.Many2one( + "res.company", + string="Company", + default=lambda self: self.env.company, + ) + budget_line_ids = fields.One2many( + comodel_name="budget.management.budget.lines", inverse_name="budget_id" + ) + warnings = fields.Text(readonly=True) + currency_id = fields.Many2one( + comodel_name="res.currency", + string="Currency", + required=True, + default=lambda self: self.env.company.currency_id, + ) + + @api.depends("date_start", "date_end") + def _compute_budget_name(self): + for record in self: + if record.date_start and record.date_end: + start_date = record.date_start.strftime("%Y-%m") + end_date = record.date_end.strftime("%Y-%m") + record.name = f"Budget {start_date} to {end_date}" + else: + record.name = "Unknown Budget" + + @api.constrains("date_start", "date_end") + def _check_period_overlap(self): + for record in self: + overlapping_budgets = self.search( + [ + ("id", "!=", record.id), + ("date_start", "<=", record.date_start), + ("date_end", ">=", record.date_end), + ("company_id", "=", record.company_id.id), + ] + ) + if overlapping_budgets: + raise ValidationError( + "Cannot create overlapping budgets for the same period and company." + ) + + def onclick_reset_to_draft(self): + for record in self: + if record.state != "draft": + record.state = "draft" + + def onclick_confirmed(self): + for record in self: + if record.state == "draft": + record.state = "confirmed" + + def onclick_revise(self): + for record in self: + if record.state != "confirmed": + raise UserError("Only confirmed budgets can be revised.") + + # Archive the current record and set its state to 'revised' + record.sudo().write({ + 'state': 'revised', + 'active': False, # Archive the original record + 'revision_id': record.id, # Set the revision_id to refer to the original budget + }) + + # Create a duplicate budget (this will be the revised budget) + duplicate = record.copy() + + # Manually copy related budget lines to the new budget (duplicate) + for line in record.budget_line_ids: + self.env["budget.management.budget.lines"].create({ + 'name': line.name, + 'budget_id': duplicate.id, # Link to the duplicated budget + 'budget_amount': line.budget_amount, + 'analytic_account_id': line.analytic_account_id.id, + # Add any other necessary fields here + }) + + # Update the duplicate record's state and fields + duplicate.sudo().write({ + 'state': 'draft', # Set the duplicate budget's state to draft + 'active': True, # Set the new record as active + 'responsible': self.env.user.id, # Set the current user as responsible for the revised budget + 'revision_id': record.id, # Set the revision_id to point to the original budget (parent budget) + 'name': f"{record.name} (Revised)", # Adjust the name for clarity + }) + + # Log a message for traceability + record.message_post( + body="The budget has been revised. A new draft budget has been created.", + message_type="notification", + ) + + + def onclick_done(self): + for record in self: + if record.state in ["confirmed", "revised"]: + record.state = "done" diff --git a/budget/models/budget_line.py b/budget/models/budget_line.py new file mode 100644 index 00000000000..b90b67990e1 --- /dev/null +++ b/budget/models/budget_line.py @@ -0,0 +1,104 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError, UserError + +class BudgetLine(models.Model): + _name = "budget.management.budget.lines" + _description = "Budget Management Budget Lines" + + name = fields.Char(string="Name") + budget_id = fields.Many2one( + comodel_name="budget.budget", string="Budget", required=True + ) + state = fields.Selection(related="budget_id.state") + budget_amount = fields.Monetary( + string="Budget Amount", + default=0.0, + currency_field="currency_id", + help="The total allocated budget for this budget line.", + ) + achieved_amount = fields.Monetary( + string="Achieved Amount", + default=0.0, + compute="_compute_achieved_amount", + store=True, + currency_field="currency_id", + ) + achieved_percentage = fields.Float( + string="Achieved (%)", + compute="_compute_achieved_amount", + store=True, + readonly=True, + help="Percentage of the budget achieved based on analytic lines.", + ) + analytic_account_id = fields.Many2one( + "account.analytic.account", string="Analytic Account", required=True + ) + analytic_line_ids = fields.One2many( + comodel_name="account.analytic.line", + inverse_name="budget_line_id", + string="Analytic Lines", + ) + over_budget = fields.Monetary( + string="Over Budget", + compute="_compute_achieved_amount", + store=True, + help="The amount by which the budget line exceeds its allocated budget.", + currency_field="currency_id", + ) + currency_id = fields.Many2one( + comodel_name="res.currency", + related="budget_id.currency_id", + string="Currency", + readonly=True, + ) + + # Add these fields for the Gantt view + date_start = fields.Date(string="Start Date", required=True) + date_end = fields.Date(string="End Date", required=True) + + @api.depends("analytic_line_ids.amount") + def _compute_achieved_amount(self): + for record in self: + record.achieved_amount = sum(record.analytic_line_ids.mapped("amount")) + record.achieved_percentage = ( + (record.achieved_amount / record.budget_amount) * 100 + if record.budget_amount > 0 + else 0.0 + ) + record.over_budget = max(0.0, record.achieved_amount - record.budget_amount) + + if ( + record.budget_id.on_over_budget == "warning" + and record.achieved_amount > record.budget_amount + ): + record.budget_id.warnings = "Achieved amount is more than your budget!" + else: + record.budget_id.warnings = False + + @api.constrains("budget_amount") + def _check_budget_amount(self): + for record in self: + if record.budget_amount < 0: + raise ValidationError("Budget amount cannot be negative.") + + @api.model_create_multi + def create(self, vals_list): + active_budget = None + if self.env.context.get("active_id"): + active_budget = self.env["budget.budget"].browse(self.env.context.get("active_id")) + if active_budget.state != "draft": + raise UserError("Budget lines can only be created when the state is 'draft'.") + else: + for vals in vals_list: + budget_id = vals.get("budget_id") + if budget_id: + active_budget = self.env["budget.budget"].browse(budget_id) + break + + if not active_budget: + raise UserError("No budget found in context or record.") + + if active_budget.state != "draft": + raise UserError("Budget lines can only be created when the state is 'draft'.") + + return super(BudgetLine, self).create(vals_list) diff --git a/budget/security/ir.model.access.csv b/budget/security/ir.model.access.csv new file mode 100644 index 00000000000..873e5995949 --- /dev/null +++ b/budget/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + +access_budget_line,budget.line,model_budget_management_budget_lines,,1,1,1,1 +access_budget,budget,model_budget_budget,,1,1,1,1 +access_budget_wizard,budget.wizard,model_add_budget_wizard,,1,1,1,1 diff --git a/budget/views/budget_line_view.xml b/budget/views/budget_line_view.xml new file mode 100644 index 00000000000..70ed5be29ee --- /dev/null +++ b/budget/views/budget_line_view.xml @@ -0,0 +1,64 @@ + + + budget.line.tree + budget.management.budget.lines + + + + + + + + + + + + + + budget.lines.graph + budget.management.budget.lines + + + + + + + + + + + budget.lines.pivot + budget.management.budget.lines + + + + + + + + + budget.lines.gantt + budget.management.budget.lines + + + + + + + + + + Budget Lines + budget.management.budget.lines + graph,pivot,gantt + {} + + + + + \ No newline at end of file diff --git a/budget/views/budget_menu_view.xml b/budget/views/budget_menu_view.xml new file mode 100644 index 00000000000..4adf66dead1 --- /dev/null +++ b/budget/views/budget_menu_view.xml @@ -0,0 +1,44 @@ + + + Budgets + budget.budget + kanban,form + +

+ No records available. +

+

+ Please check your filters or create new records. +

+
+
+ + + Budgets Lines + budget.management.budget.lines + list + + {'default_budget_id': active_id} + [('budget_id', '=', context.get('default_budget_id'))] + + + + + + + + Analytic Lines + account.analytic.line + list + {'default_budget_line_id': active_id} + [('budget_line_id', '=', context.get('default_budget_line_id'))] + +
\ No newline at end of file diff --git a/budget/views/budget_views.xml b/budget/views/budget_views.xml new file mode 100644 index 00000000000..b10dd8083e1 --- /dev/null +++ b/budget/views/budget_views.xml @@ -0,0 +1,152 @@ + + + budget.budget.kanban + budget.budget + + + + +
+ +
+
+ +
+
+ +

+ +

+
+
+
+ + + + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ + + budget.budget.form + budget.budget + + +
+ + +
+
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +